Skip to content
This repository has been archived by the owner on Apr 24, 2020. It is now read-only.

Commit

Permalink
[#14] Field instance knows its Model name.
Browse files Browse the repository at this point in the history
  • Loading branch information
ducdetronquito committed Jan 13, 2017
1 parent 63fed82 commit 4385f42
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 143 deletions.
30 changes: 19 additions & 11 deletions plume/plume.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 10 additions & 4 deletions tests/test_clause.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,32 @@ 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):
result = str(
(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):
result = str(
((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
124 changes: 46 additions & 78 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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__
Expand All @@ -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:
Expand All @@ -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__
Expand All @@ -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:
Expand Down Expand Up @@ -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__
Expand All @@ -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')"
Loading

0 comments on commit 4385f42

Please sign in to comment.