The descriptor protocol works fine but I still have one issue I would like to resolve.
I have a descriptor:
class Field(object):
def __init__(self, type_, name, value=None, required=False):
self.type = type_
self.name = "_" + name
self.required = required
self._value = value
def __get__(self, instance, owner):
return getattr(instance, self.name, self.value)
def __set__(self, instance, value):
if value:
self._check(value)
setattr(instance, self.name, value)
else:
setattr(instance, self.name, None)
def __delete__(self, instance):
raise AttributeError("Can't delete attribute")
@property
def value(self):
return self._value
@value.setter
def value(self, value):
self._value = value if value else self.type()
@property
def _types(self):
raise NotImplementedError
def _check(self, value):
if not isinstance(value, tuple(self._types)):
raise TypeError("This is bad")
This is subclassed:
class CharField(Field):
def __init__(self, name, value=None, min_length=0, max_length=0, strip=False):
super(CharField, self).__init__(unicode, name, value=value)
self.min_length = min_length
self.max_length = max_length
self.strip = strip
@property
def _types(self):
return [unicode, str]
def __set__(self, instance, value):
if self.strip:
value = value.strip()
super(CharField, self).__set__(instance, value)
And then used is a model class:
class Country(BaseModel):
name = CharField("name")
country_code_2 = CharField("country_code_2", min_length=2, max_length=2)
country_code_3 = CharField("country_code_3", min_length=3, max_length=3)
def __init__(self, name, country_code_2, country_code_3):
self.name = name
self.country_code_2 = country_code_2
self.country_code_3 = country_code_3
So far, so good, this works just as expected.
The only issue I have here is that we have to give a field name every time a field is declared. e.g. "country_code_2"
for the country_code_2
field.
How would it be possible to get the attribute name of the model class and use it in the field class?
There is a simple way, and there is a hard way.
The simple way is to use Python 3.6 (or newer), and give your descriptor an additional object.__set_name__()
method:
def __set_name__(self, owner, name):
self.name = '_' + name
When a class is created, Python automatically will call that method on any descriptors you set on the class, passing in the class object and the attribute name.
For earlier Python versions, the best next option is to use a metaclass; it'll be called for every subclass that is created, and given a handy dictionary mapping attribute name to attribute value (including you descriptor instances). You can then use this opportunity to pass that name to the descriptor:
class BaseModelMeta(type):
def __new__(mcls, name, bases, attrs):
cls = super(BaseModelMeta, mcls).__new__(mcls, name, bases, attrs)
for attr, obj in attrs.items():
if isinstance(obj, Field):
obj.__set_name__(cls, attr)
return cls
This calls the same __set_name__()
method on the field, that Python 3.6 supports natively. Then use that as the metaclass for BaseModel
:
class BaseModel(object, metaclass=BaseModelMeta):
# Python 3
or
class BaseModel(object):
__metaclass__ = BaseModelMeta
# Python 2
You could also use a class decorator to do the __set_name__
calls for any class you decorate it with, but that requires you to decorate every class. A metaclass is automatically propagated through the inheritance hierarchy instead.