Fixed #29979, Refs #17337 -- Extracted AutoField field logic into a mixin and refactored AutoFields.

This reduces duplication by allowing AutoField, BigAutoField and
SmallAutoField to inherit from IntegerField, BigIntegerField and
SmallIntegerField respectively. Doing so also allows for enabling the
max_length warning check and minimum/maximum value validation for auto
fields, as well as providing a mixin that can be used for other possible
future auto field types such as a theoretical UUIDAutoField.
This commit is contained in:
Nick Pope 2017-12-11 15:36:33 +00:00 committed by Mariusz Felisiak
parent b10d322c41
commit 21e559495b
8 changed files with 171 additions and 128 deletions

View File

@ -26,6 +26,9 @@ class BaseDatabaseOperations:
'BigIntegerField': (-9223372036854775808, 9223372036854775807),
'PositiveSmallIntegerField': (0, 32767),
'PositiveIntegerField': (0, 2147483647),
'SmallAutoField': (-32768, 32767),
'AutoField': (-2147483648, 2147483647),
'BigAutoField': (-9223372036854775808, 9223372036854775807),
}
set_operators = {
'union': 'UNION',

View File

@ -16,13 +16,18 @@ from .utils import BulkInsertMapper, InsertVar, Oracle_datetime
class DatabaseOperations(BaseDatabaseOperations):
# Oracle uses NUMBER(11) and NUMBER(19) for integer fields.
# Oracle uses NUMBER(5), NUMBER(11), and NUMBER(19) for integer fields.
# SmallIntegerField uses NUMBER(11) instead of NUMBER(5), which is used by
# SmallAutoField, to preserve backward compatibility.
integer_field_ranges = {
'SmallIntegerField': (-99999999999, 99999999999),
'IntegerField': (-99999999999, 99999999999),
'BigIntegerField': (-9999999999999999999, 9999999999999999999),
'PositiveSmallIntegerField': (0, 99999999999),
'PositiveIntegerField': (0, 99999999999),
'SmallAutoField': (-99999, 99999),
'AutoField': (-99999999999, 99999999999),
'BigAutoField': (-9999999999999999999, 9999999999999999999),
}
set_operators = {**BaseDatabaseOperations.set_operators, 'difference': 'MINUS'}

View File

@ -898,110 +898,6 @@ class Field(RegisterLookupMixin):
return getattr(obj, self.attname)
class AutoField(Field):
description = _("Integer")
empty_strings_allowed = False
default_error_messages = {
'invalid': _('%(value)s” value must be an integer.'),
}
def __init__(self, *args, **kwargs):
kwargs['blank'] = True
super().__init__(*args, **kwargs)
def check(self, **kwargs):
return [
*super().check(**kwargs),
*self._check_primary_key(),
]
def _check_primary_key(self):
if not self.primary_key:
return [
checks.Error(
'AutoFields must set primary_key=True.',
obj=self,
id='fields.E100',
),
]
else:
return []
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs['blank']
kwargs['primary_key'] = True
return name, path, args, kwargs
def get_internal_type(self):
return "AutoField"
def to_python(self, value):
if value is None:
return value
try:
return int(value)
except (TypeError, ValueError):
raise exceptions.ValidationError(
self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def rel_db_type(self, connection):
return IntegerField().db_type(connection=connection)
def validate(self, value, model_instance):
pass
def get_db_prep_value(self, value, connection, prepared=False):
if not prepared:
value = self.get_prep_value(value)
value = connection.ops.validate_autopk_value(value)
return value
def get_prep_value(self, value):
from django.db.models.expressions import OuterRef
value = super().get_prep_value(value)
if value is None or isinstance(value, OuterRef):
return value
try:
return int(value)
except (TypeError, ValueError) as e:
raise e.__class__(
"Field '%s' expected a number but got %r." % (self.name, value),
) from e
def contribute_to_class(self, cls, name, **kwargs):
assert not cls._meta.auto_field, "Model %s can't have more than one AutoField." % cls._meta.label
super().contribute_to_class(cls, name, **kwargs)
cls._meta.auto_field = self
def formfield(self, **kwargs):
return None
class BigAutoField(AutoField):
description = _("Big (8 byte) integer")
def get_internal_type(self):
return "BigAutoField"
def rel_db_type(self, connection):
return BigIntegerField().db_type(connection=connection)
class SmallAutoField(AutoField):
description = _('Small integer')
def get_internal_type(self):
return 'SmallAutoField'
def rel_db_type(self, connection):
return SmallIntegerField().db_type(connection=connection)
class BooleanField(Field):
empty_strings_allowed = False
default_error_messages = {
@ -2395,3 +2291,113 @@ class UUIDField(Field):
'form_class': forms.UUIDField,
**kwargs,
})
class AutoFieldMixin:
def __init__(self, *args, **kwargs):
kwargs['blank'] = True
super().__init__(*args, **kwargs)
def check(self, **kwargs):
return [
*super().check(**kwargs),
*self._check_primary_key(),
]
def _check_primary_key(self):
if not self.primary_key:
return [
checks.Error(
'AutoFields must set primary_key=True.',
obj=self,
id='fields.E100',
),
]
else:
return []
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs['blank']
kwargs['primary_key'] = True
return name, path, args, kwargs
def validate(self, value, model_instance):
pass
def get_db_prep_value(self, value, connection, prepared=False):
if not prepared:
value = self.get_prep_value(value)
value = connection.ops.validate_autopk_value(value)
return value
def get_prep_value(self, value):
from django.db.models.expressions import OuterRef
return value if isinstance(value, OuterRef) else super().get_prep_value(value)
def contribute_to_class(self, cls, name, **kwargs):
assert not cls._meta.auto_field, (
"Model %s can't have more than one auto-generated field."
% cls._meta.label
)
super().contribute_to_class(cls, name, **kwargs)
cls._meta.auto_field = self
def formfield(self, **kwargs):
return None
class AutoFieldMeta(type):
"""
Metaclass to maintain backward inheritance compatibility for AutoField.
It is intended that AutoFieldMixin become public API when it is possible to
create a non-integer automatically-generated field using column defaults
stored in the database.
In many areas Django also relies on using isinstance() to check for an
automatically-generated field as a subclass of AutoField. A new flag needs
to be implemented on Field to be used instead.
When these issues have been addressed, this metaclass could be used to
deprecate inheritance from AutoField and use of isinstance() with AutoField
for detecting automatically-generated fields.
"""
@property
def _subclasses(self):
return (BigAutoField, SmallAutoField)
def __instancecheck__(self, instance):
return isinstance(instance, self._subclasses) or super().__instancecheck__(instance)
def __subclasscheck__(self, subclass):
return subclass in self._subclasses or super().__subclasscheck__(subclass)
class AutoField(AutoFieldMixin, IntegerField, metaclass=AutoFieldMeta):
def get_internal_type(self):
return 'AutoField'
def rel_db_type(self, connection):
return IntegerField().db_type(connection=connection)
class BigAutoField(AutoFieldMixin, BigIntegerField):
def get_internal_type(self):
return 'BigAutoField'
def rel_db_type(self, connection):
return BigIntegerField().db_type(connection=connection)
class SmallAutoField(AutoFieldMixin, SmallIntegerField):
def get_internal_type(self):
return 'SmallAutoField'
def rel_db_type(self, connection):
return SmallIntegerField().db_type(connection=connection)

View File

@ -304,6 +304,12 @@ Models
a certain (database-dependent) limit. Values from ``1`` to ``32767`` are safe
in all databases supported by Django.
* :class:`~django.db.models.AutoField`,
:class:`~django.db.models.BigAutoField`, and
:class:`~django.db.models.SmallAutoField` now inherit from
``IntegerField``, ``BigIntegerField`` and ``SmallIntegerField`` respectively.
System checks and validators are now also properly inherited.
* :attr:`.FileField.upload_to` now supports :class:`pathlib.Path`.
Requests and Responses
@ -402,6 +408,11 @@ backends.
* ``DatabaseOperations.return_insert_id()`` now requires an additional
``field`` argument with the model field.
* Entries for ``AutoField``, ``BigAutoField``, and ``SmallAutoField`` are added
to ``DatabaseOperations.integer_field_ranges`` to support the integer range
validators on these field types. Third-party backends may need to customize
the default entries.
:mod:`django.contrib.admin`
---------------------------

View File

@ -38,6 +38,21 @@ class AutoFieldTests(SimpleTestCase):
),
])
def test_max_length_warning(self):
class Model(models.Model):
auto = models.AutoField(primary_key=True, max_length=2)
field = Model._meta.get_field('auto')
self.assertEqual(field.check(), [
DjangoWarning(
"'max_length' is ignored when used with %s."
% field.__class__.__name__,
hint="Remove 'max_length' from field",
obj=field,
id='fields.W122',
),
])
@isolate_apps('invalid_models_tests')
class BinaryFieldTests(SimpleTestCase):

View File

@ -1,32 +1,32 @@
from django.test import TestCase
from django.db import models
from django.test import SimpleTestCase
from .models import AutoModel, BigAutoModel, SmallAutoModel
from .test_integerfield import (
BigIntegerFieldTests, IntegerFieldTests, SmallIntegerFieldTests,
)
class AutoFieldTests(TestCase):
class AutoFieldTests(IntegerFieldTests):
model = AutoModel
def test_invalid_value(self):
tests = [
(TypeError, ()),
(TypeError, []),
(TypeError, {}),
(TypeError, set()),
(TypeError, object()),
(TypeError, complex()),
(ValueError, 'non-numeric string'),
(ValueError, b'non-numeric byte-string'),
]
for exception, value in tests:
with self.subTest(value=value):
msg = "Field 'value' expected a number but got %r." % (value,)
with self.assertRaisesMessage(exception, msg):
self.model.objects.create(value=value)
class BigAutoFieldTests(AutoFieldTests):
class BigAutoFieldTests(BigIntegerFieldTests):
model = BigAutoModel
class SmallAutoFieldTests(AutoFieldTests):
class SmallAutoFieldTests(SmallIntegerFieldTests):
model = SmallAutoModel
class AutoFieldInheritanceTests(SimpleTestCase):
def test_isinstance_of_autofield(self):
for field in (models.BigAutoField, models.SmallAutoField):
with self.subTest(field.__name__):
self.assertIsInstance(field(), models.AutoField)
def test_issubclass_of_autofield(self):
for field in (models.BigAutoField, models.SmallAutoField):
with self.subTest(field.__name__):
self.assertTrue(issubclass(field, models.AutoField))

View File

@ -125,7 +125,7 @@ class IntegerFieldTests(TestCase):
ranged_value_field.run_validators(max_backend_value + 1)
def test_types(self):
instance = self.model(value=0)
instance = self.model(value=1)
self.assertIsInstance(instance.value, int)
instance.save()
self.assertIsInstance(instance.value, int)

View File

@ -130,4 +130,7 @@ try:
auto2 = models.AutoField(primary_key=True)
except AssertionError as exc:
assertion_error = exc
assert str(assertion_error) == "Model validation.MultipleAutoFields can't have more than one AutoField."
assert str(assertion_error) == (
"Model validation.MultipleAutoFields can't have more than one "
"auto-generated field."
)