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:
parent
b10d322c41
commit
21e559495b
|
@ -26,6 +26,9 @@ class BaseDatabaseOperations:
|
||||||
'BigIntegerField': (-9223372036854775808, 9223372036854775807),
|
'BigIntegerField': (-9223372036854775808, 9223372036854775807),
|
||||||
'PositiveSmallIntegerField': (0, 32767),
|
'PositiveSmallIntegerField': (0, 32767),
|
||||||
'PositiveIntegerField': (0, 2147483647),
|
'PositiveIntegerField': (0, 2147483647),
|
||||||
|
'SmallAutoField': (-32768, 32767),
|
||||||
|
'AutoField': (-2147483648, 2147483647),
|
||||||
|
'BigAutoField': (-9223372036854775808, 9223372036854775807),
|
||||||
}
|
}
|
||||||
set_operators = {
|
set_operators = {
|
||||||
'union': 'UNION',
|
'union': 'UNION',
|
||||||
|
|
|
@ -16,13 +16,18 @@ from .utils import BulkInsertMapper, InsertVar, Oracle_datetime
|
||||||
|
|
||||||
|
|
||||||
class DatabaseOperations(BaseDatabaseOperations):
|
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 = {
|
integer_field_ranges = {
|
||||||
'SmallIntegerField': (-99999999999, 99999999999),
|
'SmallIntegerField': (-99999999999, 99999999999),
|
||||||
'IntegerField': (-99999999999, 99999999999),
|
'IntegerField': (-99999999999, 99999999999),
|
||||||
'BigIntegerField': (-9999999999999999999, 9999999999999999999),
|
'BigIntegerField': (-9999999999999999999, 9999999999999999999),
|
||||||
'PositiveSmallIntegerField': (0, 99999999999),
|
'PositiveSmallIntegerField': (0, 99999999999),
|
||||||
'PositiveIntegerField': (0, 99999999999),
|
'PositiveIntegerField': (0, 99999999999),
|
||||||
|
'SmallAutoField': (-99999, 99999),
|
||||||
|
'AutoField': (-99999999999, 99999999999),
|
||||||
|
'BigAutoField': (-9999999999999999999, 9999999999999999999),
|
||||||
}
|
}
|
||||||
set_operators = {**BaseDatabaseOperations.set_operators, 'difference': 'MINUS'}
|
set_operators = {**BaseDatabaseOperations.set_operators, 'difference': 'MINUS'}
|
||||||
|
|
||||||
|
|
|
@ -898,110 +898,6 @@ class Field(RegisterLookupMixin):
|
||||||
return getattr(obj, self.attname)
|
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):
|
class BooleanField(Field):
|
||||||
empty_strings_allowed = False
|
empty_strings_allowed = False
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -2395,3 +2291,113 @@ class UUIDField(Field):
|
||||||
'form_class': forms.UUIDField,
|
'form_class': forms.UUIDField,
|
||||||
**kwargs,
|
**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)
|
||||||
|
|
|
@ -304,6 +304,12 @@ Models
|
||||||
a certain (database-dependent) limit. Values from ``1`` to ``32767`` are safe
|
a certain (database-dependent) limit. Values from ``1`` to ``32767`` are safe
|
||||||
in all databases supported by Django.
|
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`.
|
* :attr:`.FileField.upload_to` now supports :class:`pathlib.Path`.
|
||||||
|
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
|
@ -402,6 +408,11 @@ backends.
|
||||||
* ``DatabaseOperations.return_insert_id()`` now requires an additional
|
* ``DatabaseOperations.return_insert_id()`` now requires an additional
|
||||||
``field`` argument with the model field.
|
``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`
|
:mod:`django.contrib.admin`
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
|
|
@ -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')
|
@isolate_apps('invalid_models_tests')
|
||||||
class BinaryFieldTests(SimpleTestCase):
|
class BinaryFieldTests(SimpleTestCase):
|
||||||
|
|
|
@ -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 .models import AutoModel, BigAutoModel, SmallAutoModel
|
||||||
|
from .test_integerfield import (
|
||||||
|
BigIntegerFieldTests, IntegerFieldTests, SmallIntegerFieldTests,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AutoFieldTests(TestCase):
|
class AutoFieldTests(IntegerFieldTests):
|
||||||
model = AutoModel
|
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(BigIntegerFieldTests):
|
||||||
class BigAutoFieldTests(AutoFieldTests):
|
|
||||||
model = BigAutoModel
|
model = BigAutoModel
|
||||||
|
|
||||||
|
|
||||||
class SmallAutoFieldTests(AutoFieldTests):
|
class SmallAutoFieldTests(SmallIntegerFieldTests):
|
||||||
model = SmallAutoModel
|
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))
|
||||||
|
|
|
@ -125,7 +125,7 @@ class IntegerFieldTests(TestCase):
|
||||||
ranged_value_field.run_validators(max_backend_value + 1)
|
ranged_value_field.run_validators(max_backend_value + 1)
|
||||||
|
|
||||||
def test_types(self):
|
def test_types(self):
|
||||||
instance = self.model(value=0)
|
instance = self.model(value=1)
|
||||||
self.assertIsInstance(instance.value, int)
|
self.assertIsInstance(instance.value, int)
|
||||||
instance.save()
|
instance.save()
|
||||||
self.assertIsInstance(instance.value, int)
|
self.assertIsInstance(instance.value, int)
|
||||||
|
|
|
@ -130,4 +130,7 @@ try:
|
||||||
auto2 = models.AutoField(primary_key=True)
|
auto2 = models.AutoField(primary_key=True)
|
||||||
except AssertionError as exc:
|
except AssertionError as exc:
|
||||||
assertion_error = 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."
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue