Fixed #30657 -- Allowed customizing Field's descriptors with a descriptor_class attribute.
Allows model fields to override the descriptor class used on the model instance attribute.
This commit is contained in:
parent
93ffa81bc5
commit
5ed20b3aa3
|
@ -123,6 +123,8 @@ class Field(RegisterLookupMixin):
|
||||||
one_to_one = None
|
one_to_one = None
|
||||||
related_model = None
|
related_model = None
|
||||||
|
|
||||||
|
descriptor_class = DeferredAttribute
|
||||||
|
|
||||||
# Generic field type description, usually overridden by subclasses
|
# Generic field type description, usually overridden by subclasses
|
||||||
def _description(self):
|
def _description(self):
|
||||||
return _('Field of type: %(field_type)s') % {
|
return _('Field of type: %(field_type)s') % {
|
||||||
|
@ -738,7 +740,7 @@ class Field(RegisterLookupMixin):
|
||||||
# if you have a classmethod and a field with the same name, then
|
# if you have a classmethod and a field with the same name, then
|
||||||
# such fields can't be deferred (we don't have a check for this).
|
# such fields can't be deferred (we don't have a check for this).
|
||||||
if not getattr(cls, self.attname, None):
|
if not getattr(cls, self.attname, None):
|
||||||
setattr(cls, self.attname, DeferredAttribute(self))
|
setattr(cls, self.attname, self.descriptor_class(self))
|
||||||
if self.choices is not None:
|
if self.choices is not None:
|
||||||
setattr(cls, 'get_%s_display' % self.name,
|
setattr(cls, 'get_%s_display' % self.name,
|
||||||
partialmethod(cls._get_FIELD_display, field=self))
|
partialmethod(cls._get_FIELD_display, field=self))
|
||||||
|
|
|
@ -1793,6 +1793,16 @@ Field API reference
|
||||||
|
|
||||||
where the arguments are interpolated from the field's ``__dict__``.
|
where the arguments are interpolated from the field's ``__dict__``.
|
||||||
|
|
||||||
|
.. attribute:: descriptor_class
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
|
||||||
|
A class implementing the :py:ref:`descriptor protocol <descriptors>`
|
||||||
|
that is instantiated and assigned to the model instance attribute. The
|
||||||
|
constructor must accept a single argument, the ``Field`` instance.
|
||||||
|
Overriding this class attribute allows for customizing the get and set
|
||||||
|
behavior.
|
||||||
|
|
||||||
To map a ``Field`` to a database-specific type, Django exposes several
|
To map a ``Field`` to a database-specific type, Django exposes several
|
||||||
methods:
|
methods:
|
||||||
|
|
||||||
|
|
|
@ -283,6 +283,10 @@ Models
|
||||||
:class:`~django.db.models.Index` now support app label and class
|
:class:`~django.db.models.Index` now support app label and class
|
||||||
interpolation using the ``'%(app_label)s'`` and ``'%(class)s'`` placeholders.
|
interpolation using the ``'%(app_label)s'`` and ``'%(class)s'`` placeholders.
|
||||||
|
|
||||||
|
* The new :attr:`.Field.descriptor_class` attribute allows model fields to
|
||||||
|
customize the get and set behavior by overriding their
|
||||||
|
:py:ref:`descriptors <descriptors>`.
|
||||||
|
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,26 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models.query_utils import DeferredAttribute
|
||||||
|
|
||||||
|
|
||||||
class CustomTypedField(models.TextField):
|
class CustomTypedField(models.TextField):
|
||||||
def db_type(self, connection):
|
def db_type(self, connection):
|
||||||
return 'custom_field'
|
return 'custom_field'
|
||||||
|
|
||||||
|
|
||||||
|
class CustomDeferredAttribute(DeferredAttribute):
|
||||||
|
def __get__(self, instance, cls=None):
|
||||||
|
self._count_call(instance, 'get')
|
||||||
|
return super().__get__(instance, cls)
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
self._count_call(instance, 'set')
|
||||||
|
instance.__dict__[self.field.attname] = value
|
||||||
|
|
||||||
|
def _count_call(self, instance, get_or_set):
|
||||||
|
count_attr = '_%s_%s_count' % (self.field.attname, get_or_set)
|
||||||
|
count = getattr(instance, count_attr, 0)
|
||||||
|
setattr(instance, count_attr, count + 1)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomDescriptorField(models.CharField):
|
||||||
|
descriptor_class = CustomDeferredAttribute
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.db import connection
|
from django.db import connection, models
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
from .fields import CustomTypedField
|
from .fields import CustomDescriptorField, CustomTypedField
|
||||||
|
|
||||||
|
|
||||||
class TestDbType(SimpleTestCase):
|
class TestDbType(SimpleTestCase):
|
||||||
|
@ -9,3 +9,26 @@ class TestDbType(SimpleTestCase):
|
||||||
def test_db_parameters_respects_db_type(self):
|
def test_db_parameters_respects_db_type(self):
|
||||||
f = CustomTypedField()
|
f = CustomTypedField()
|
||||||
self.assertEqual(f.db_parameters(connection)['type'], 'custom_field')
|
self.assertEqual(f.db_parameters(connection)['type'], 'custom_field')
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptorClassTest(SimpleTestCase):
|
||||||
|
def test_descriptor_class(self):
|
||||||
|
class CustomDescriptorModel(models.Model):
|
||||||
|
name = CustomDescriptorField(max_length=32)
|
||||||
|
|
||||||
|
m = CustomDescriptorModel()
|
||||||
|
self.assertFalse(hasattr(m, '_name_get_count'))
|
||||||
|
# The field is set to its default in the model constructor.
|
||||||
|
self.assertEqual(m._name_set_count, 1)
|
||||||
|
m.name = 'foo'
|
||||||
|
self.assertFalse(hasattr(m, '_name_get_count'))
|
||||||
|
self.assertEqual(m._name_set_count, 2)
|
||||||
|
self.assertEqual(m.name, 'foo')
|
||||||
|
self.assertEqual(m._name_get_count, 1)
|
||||||
|
self.assertEqual(m._name_set_count, 2)
|
||||||
|
m.name = 'bar'
|
||||||
|
self.assertEqual(m._name_get_count, 1)
|
||||||
|
self.assertEqual(m._name_set_count, 3)
|
||||||
|
self.assertEqual(m.name, 'bar')
|
||||||
|
self.assertEqual(m._name_get_count, 2)
|
||||||
|
self.assertEqual(m._name_set_count, 3)
|
||||||
|
|
Loading…
Reference in New Issue