diff --git a/plume/plume.py b/plume/plume.py index 3c333a0..d71be47 100644 --- a/plume/plume.py +++ b/plume/plume.py @@ -242,7 +242,7 @@ def where(self, *args): def select(self, *args): # Allow to filter Select-Query on columns. - self._fields = args + self._fields = [str(field) for field in args] return self @@ -383,23 +383,26 @@ def __get__(self, instance, owner): class BaseModel(type): def __new__(cls, clsname, bases, attrs): - fieldnames = [] + fieldnames = set() # Collect all field names from the base classes. for base in bases: - fieldnames.extend(getattr(base, '_fieldnames', [])) + fieldnames.update(getattr(base, '_fieldnames', [])) + + fieldnames.add('pk') + attrs['pk'] = PrimaryKeyField() related_fields = [] for attr_name, attr_value in attrs.items(): # Provide to each Field subclass the name of its attribute. if isinstance(attr_value, Field): attr_value.name = attr_name - fieldnames.append(attr_name) + fieldnames.add(attr_name) # Keep track of each RelatedField. if isinstance(attr_value, ForeignKeyField): related_fields.append((attr_name, attr_value)) - # Add the list of field names as attribute of the Model class. - attrs['_fieldnames'] = fieldnames + # Add the tuple of field names as attribute of the Model class. + attrs['_fieldnames'] = tuple(fieldnames) # Add instance factory class attrs['_factory'] = namedtuple('InstanceFactory', fieldnames) @@ -413,24 +416,30 @@ def __new__(cls, clsname, bases, attrs): #Add a Manager instance as an attribute of the Model class. setattr(new_class, 'objects', Manager(new_class)) + # Each field of the class knows its related model class name. + for fieldname in new_class._fieldnames: + getattr(new_class, fieldname).model_name = clsname.lower() + # Add a Manager to each related Model. for attr_name, attr_value in related_fields: setattr(attr_value.related_model, attr_value.related_field, RelatedManager(new_class)) + return new_class class Field(Node): - __slots__ = ('value', 'name', 'required', 'unique', 'default') + __slots__ = ('default', 'model_name', 'name', 'required', 'unique', 'value') internal_type = None sqlite_datatype = None def __init__(self, required=True, unique=False, default=None): - self.value = None + self.default = default + self.model_name = None self.name = None self.required = required self.unique = unique - self.default = None + self.value = None if default is not None and self.is_valid(default): self.default = default @@ -462,7 +471,7 @@ def __set__(self, instance, value): instance._values._replace(**{self.name: value}) def __str__(self): - return self.name + return '.'.join((self.model_name, self.name)) def is_valid(self, value): """Return True if the provided value match the internal field.""" @@ -563,7 +572,6 @@ def sql(self): class Model(metaclass=BaseModel): - pk = PrimaryKeyField() def __init__(self, **kwargs): # Each value for the current instance is stored in a hidden dictionary. diff --git a/tests/test_clause.py b/tests/test_clause.py index 6e25a99..bacd6d1 100644 --- a/tests/test_clause.py +++ b/tests/test_clause.py @@ -8,12 +8,12 @@ class TestClause: def test_allows_or_operator_between_two_clauses(self): result = str((Pokemon.name == 'Charamander') | (Pokemon.name == 'Bulbasaur')) - expected = "name = 'Charamander' OR name = 'Bulbasaur'" + expected = "pokemon.name = 'Charamander' OR pokemon.name = 'Bulbasaur'" assert result == expected def test_allows_and_operator_between_two_clauses(self): result = str((Pokemon.name == 'Charamander') & (Pokemon.level == 18)) - expected = "name = 'Charamander' AND level = 18" + expected = "pokemon.name = 'Charamander' AND pokemon.level = 18" assert result == expected def test_or_operator_has_lower_precedence_than_and_operator(self): @@ -21,7 +21,10 @@ def test_or_operator_has_lower_precedence_than_and_operator(self): (Pokemon.name == 'Charamander') | (Pokemon.name == 'Bulbasaur') & (Pokemon.level > 18) ) - expected = "name = 'Charamander' OR name = 'Bulbasaur' AND level > 18" + expected = ( + "pokemon.name = 'Charamander' OR pokemon.name = 'Bulbasaur'" + " AND pokemon.level > 18" + ) assert result == expected def test_bracket_has_higher_precedence_than_and_operator(self): @@ -29,5 +32,8 @@ def test_bracket_has_higher_precedence_than_and_operator(self): ((Pokemon.name == 'Charamander') | (Pokemon.name == 'Bulbasaur')) & (Pokemon.level > 18) ) - expected = "name = 'Charamander' OR name = 'Bulbasaur' AND level > 18" + expected = ( + "pokemon.name = 'Charamander' OR pokemon.name = 'Bulbasaur'" + " AND pokemon.level > 18" + ) assert result == expected diff --git a/tests/test_fields.py b/tests/test_fields.py index 03a6975..d9e21d6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -14,16 +14,13 @@ def test_is_slotted(self): Field().__dict__ def test_field_value_is_required_by_default(self): - a = Field() - assert a.required is True + assert Field().required is True def test_field_value_is_not_unique_by_default(self): - a = Field() - assert a.unique is False + assert Field().unique is False def test_default_field_value_is_not_defined(self): - a = Field() - assert a.default is None + assert Field().default is None def test_class_access_returns_Field_class(self): class User(Model): @@ -34,13 +31,15 @@ class User(Model): def test_instance_access_returns_field_value(self): class User(Model): field = Field() - user = User(field='value') assert user.field == 'value' class TestFloatField: + class User(Model): + field = FloatField() + def test_is_slotted(self): with pytest.raises(AttributeError): FloatField().__dict__ @@ -63,47 +62,32 @@ def test_default_value_needs_to_be_a_float(self): field = FloatField(default=42) def test_allows_equal_operator(self): - field = FloatField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field == 6.66) - assert str(criterion) == "field = 6.66" + criterion = (self.User.field == 6.66) + assert str(criterion) == "user.field = 6.66" def test_allows_not_equal_operator(self): - field = FloatField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field != 6.66) - assert str(criterion) == "field != 6.66" + criterion = (self.User.field != 6.66) + assert str(criterion) == "user.field != 6.66" def test_allows_in_operator(self): - field = FloatField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field << [6.66, 42.0]) - - assert str(criterion) == "field IN (6.66, 42.0)" + criterion = (self.User.field << [6.66, 42.0]) + assert str(criterion) == "user.field IN (6.66, 42.0)" def test_allows_lower_than_operator(self): - field = FloatField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field < 6.66) - assert str(criterion) == "field < 6.66" + criterion = (self.User.field < 6.66) + assert str(criterion) == "user.field < 6.66" def test_allows_lower_than_equals_operator(self): - field = FloatField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field <= 6.66) - assert str(criterion) == "field <= 6.66" + criterion = (self.User.field <= 6.66) + assert str(criterion) == "user.field <= 6.66" def test_allows_greater_than_operator(self): - field = FloatField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field > 6.66) - assert str(criterion) == "field > 6.66" + criterion = (self.User.field > 6.66) + assert str(criterion) == "user.field > 6.66" def test_allows_greater_than_equals_operator(self): - field = FloatField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field >= 6.66) - assert str(criterion) == "field >= 6.66" + criterion = (self.User.field >= 6.66) + assert str(criterion) == "user.field >= 6.66" class TestForeignKeyField: @@ -128,6 +112,9 @@ def test_for_create_table_query_sql_output_a_list_of_keywords(self): class TestIntegerField: + class User(Model): + field = IntegerField() + def test_is_slotted(self): with pytest.raises(AttributeError): IntegerField().__dict__ @@ -150,47 +137,32 @@ def test_default_value_needs_to_be_an_integer(self): field = IntegerField(default=6.66) def test_allows_equal_operator(self): - field = IntegerField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field == 42) - assert str(criterion) == "field = 42" + criterion = (self.User.field == 42) + assert str(criterion) == "user.field = 42" def test_allows_not_equal_operator(self): - field = IntegerField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field != 42) - assert str(criterion) == "field != 42" + criterion = (self.User.field != 42) + assert str(criterion) == "user.field != 42" def test_allows_in_operator(self): - field = IntegerField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field << [42, 666]) - - assert str(criterion) == "field IN (42, 666)" + criterion = (self.User.field << [42, 666]) + assert str(criterion) == "user.field IN (42, 666)" def test_allows_lower_than_operator(self): - field = IntegerField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field < 42) - assert str(criterion) == "field < 42" + criterion = (self.User.field < 42) + assert str(criterion) == "user.field < 42" def test_allows_lower_than_equals_operator(self): - field = IntegerField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field <= 42) - assert str(criterion) == "field <= 42" + criterion = (self.User.field <= 42) + assert str(criterion) == "user.field <= 42" def test_allows_greater_than_operator(self): - field = IntegerField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field > 42) - assert str(criterion) == "field > 42" + criterion = (self.User.field > 42) + assert str(criterion) == "user.field > 42" def test_allows_greater_than_equals_operator(self): - field = IntegerField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field >= 42) - assert str(criterion) == "field >= 42" + criterion = (self.User.field >= 42) + assert str(criterion) == "user.field >= 42" class TestPrimaryKeyField: @@ -220,6 +192,9 @@ def test_default_value_needs_to_be_an_integer(self): class TestTextField: + class User(Model): + field = TextField() + def test_is_slotted(self): with pytest.raises(AttributeError): TextField().__dict__ @@ -242,20 +217,13 @@ def test_default_value_needs_to_be_a_string(self): field = TextField(default=42) def test_allows_equal_operator(self): - field = TextField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field == 'value') - assert str(criterion) == "field = 'value'" + criterion = (self.User.field == 'value') + assert str(criterion) == "user.field = 'value'" def test_allows_not_equal_operator(self): - field = TextField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field != 'value') - assert str(criterion) == "field != 'value'" + criterion = (self.User.field != 'value') + assert str(criterion) == "user.field != 'value'" def test_allows_in_operator(self): - field = TextField() - field.name = 'field' # Automatically done in BaseModel. - criterion = (field << ['value1', 'value2']) - - assert str(criterion) == "field IN ('value1', 'value2')" + criterion = (self.User.field << ['value1', 'value2']) + assert str(criterion) == "user.field IN ('value1', 'value2')" diff --git a/tests/test_selectquery.py b/tests/test_selectquery.py index e6d17e2..da1785b 100644 --- a/tests/test_selectquery.py +++ b/tests/test_selectquery.py @@ -6,7 +6,7 @@ class Base: - + TRAINERS = { 'Giovanni': { 'name': 'Giovanni', @@ -21,7 +21,7 @@ class Base: 'age': 17 }, } - + POKEMONS = { 'Kangaskhan': { 'name': 'Kangaskhan', @@ -39,66 +39,71 @@ class Base: 'trainer': 3 }, } - + def setup_method(self): db = Database(DB_NAME) db.register(Trainer, Pokemon) - + def add_trainer(self, names): try: names = names.split() except: pass - + for name in names: Trainer.objects.create(**self.TRAINERS[name]) - + def add_pokemon(self, names): try: names = names.split() except: pass - + for name in names: Pokemon.objects.create(**self.POKEMONS[name]) class TestSelectQueryAPI(Base): - + def test_output_selectquery_as_string(self): result = str(Trainer.objects.where(Trainer.age > 18, Trainer.name != 'Giovanni')) - expected = "(SELECT * FROM trainer WHERE name != 'Giovanni' AND age > 18)" + expected = "(SELECT * FROM trainer WHERE trainer.name != 'Giovanni' AND trainer.age > 18)" assert result == expected - + def test_output_selectquery_with_nested_query(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) self.add_pokemon(['Kangaskhan', 'Koffing', 'Wobbuffet']) - - trainer_pks = Trainer.objects.select('pk').where(Trainer.name != 'Jessie') - pokemons_names = Pokemon.objects.select('name').where(Pokemon.trainer << trainer_pks) - - assert str(trainer_pks) == "(SELECT pk FROM trainer WHERE name != 'Jessie')" - assert str(pokemons_names) == "(SELECT name FROM pokemon WHERE trainer IN (SELECT pk FROM trainer WHERE name != 'Jessie'))" + + trainer_pks = Trainer.objects.select(Trainer.pk).where(Trainer.name != 'Jessie') + pokemons_names = Pokemon.objects.select(Pokemon.name).where(Pokemon.trainer << trainer_pks) + + assert str(trainer_pks) == ( + "(SELECT trainer.pk FROM trainer WHERE trainer.name != 'Jessie')" + ) + assert str(pokemons_names) == ( + "(SELECT pokemon.name FROM pokemon WHERE pokemon.trainer IN " + "(SELECT trainer.pk FROM trainer WHERE trainer.name != 'Jessie'))" + ) def test_is_slotted(self): with pytest.raises(AttributeError): SelectQuery(Model).__dict__ - + class TestSelectQuerySlice(Base): - + def test_indexed_access_to_first_element_returns_a_model_instance(self): self.add_trainer('Giovanni') trainer = Trainer.objects.where()[0] assert trainer.name == 'Giovanni' assert trainer.age == 42 - + def test_indexed_access_to_random_element_returns_a_model_instance(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) trainer = Trainer.objects.where()[2] assert trainer.name == 'Jessie' assert trainer.age == 17 - + def test_slice_access_with_start_and_stop_value_returns_a_model_instance_list(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) trainers = Trainer.objects.where()[1:3] @@ -107,7 +112,7 @@ def test_slice_access_with_start_and_stop_value_returns_a_model_instance_list(se assert trainers[0].age == 21 assert trainers[1].name == 'Jessie' assert trainers[1].age == 17 - + def test_slice_access_with_offset_only_returns_a_model_instance_list(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) trainers = Trainer.objects.where()[1:3] @@ -116,7 +121,7 @@ def test_slice_access_with_offset_only_returns_a_model_instance_list(self): assert trainers[0].age == 21 assert trainers[1].name == 'Jessie' assert trainers[1].age == 17 - + def test_slice_access_with_offset_only_returns_a_pokemon_list(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) trainers = Trainer.objects.where()[:2] @@ -128,40 +133,40 @@ def test_slice_access_with_offset_only_returns_a_pokemon_list(self): class TestSelectQueryResults(Base): - + def test_select_from_one_table_with_all_fields(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) result = list(Trainer.objects.where()) - + assert len(result) == 3 giovanni, james, jessie = result - + assert giovanni.name == 'Giovanni' assert giovanni.age == 42 - + assert james.name == 'James' assert james.age == 21 - + assert jessie.name == 'Jessie' assert jessie.age == 17 - + def test_select_from_one_table_with_one_criterion(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) result = list(Trainer.objects.where(Trainer.age > 18)) assert len(result) == 2 - + giovanni, james = result assert giovanni.name == 'Giovanni' assert giovanni.age == 42 - + assert james.name == 'James' assert james.age == 21 - + def test_select_from_one_table_with_ANDs_criteria_operator(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) result = list(Trainer.objects.where((Trainer.age > 18) & (Trainer.name != 'Giovanni'))) assert len(result) == 1 - + james = result[0] assert james.name == 'James' assert james.age == 21 @@ -170,41 +175,41 @@ def test_select_from_one_table_with_ANDs_criteria_list(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) result = list(Trainer.objects.where(Trainer.age > 18, Trainer.name != 'Giovanni')) assert len(result) == 1 - + james = result[0] assert james.name == 'James' assert james.age == 21 - + def test_select_from_one_table_with_chained_filters(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) - selectquery = Trainer.objects.where(Trainer.age > 18) + selectquery = Trainer.objects.where(Trainer.age > 18) selectquery.where(Trainer.name != 'Giovanni') result = list(selectquery) assert len(result) == 1 - + james = result[0] assert james.name == 'James' assert james.age == 21 - + def test_select_from_one_table_with_in_operator(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) result = list(Trainer.objects.where(Trainer.age << [17, 21])) assert len(result) == 2 - + james, jessie = result assert james.name == 'James' assert james.age == 21 - + assert jessie.name == 'Jessie' assert jessie.age == 17 - + def test_filter_on_one_field_must_returns_a_list_of_field_values(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) result = Trainer.objects.select('name').tuples() expected = [('Giovanni',), ('James',), ('Jessie',)] - + assert result == expected - + def test_filter_on_several_fields_must_returns_a_list_of_tuples(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) result = Trainer.objects.select('name', 'age').tuples() @@ -219,15 +224,15 @@ def test_filter_on_several_fields_must_returns_a_list_of_tuples(self): def test_filter_on_table_with_related_field(self): self.add_trainer('Giovanni') self.add_pokemon('Kangaskhan') - + result = list(Pokemon.objects.where()) assert len(result) == 1 - + pokemon = result[0] assert pokemon.name == 'Kangaskhan' assert pokemon.level == 29 assert isinstance(result[0].trainer, Trainer) is True - + trainer = pokemon.trainer assert trainer.name == 'Giovanni' assert trainer.age == 42 @@ -236,13 +241,13 @@ def test_filter_on_table_with_related_field(self): def test_filter_with_nested_query(self): self.add_trainer(['Giovanni', 'James', 'Jessie']) self.add_pokemon(['Kangaskhan', 'Koffing', 'Wobbuffet']) - + trainer_pks = Trainer.objects.select('pk').where(Trainer.name != 'Jessie') - + result = Pokemon.objects.select('name').where(Pokemon.trainer << trainer_pks).tuples() - + assert len(result) == 2 assert result[0][0] == 'Kangaskhan' assert result[1][0] == 'Koffing' - + diff --git a/tests/utils.py b/tests/utils.py index 985b269..061aed0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,7 +7,7 @@ class Trainer(Model): name = TextField() age = IntegerField() - + class Pokemon(Model): name = TextField()