summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Pope <nick.pope@flightdataservices.com>2017-12-11 15:36:33 +0000
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2019-08-20 09:22:25 +0200
commit21e559495b8255bba1e8a4429cd083246ab90457 (patch)
tree7c28928a42b896f05be9282461dcc0e1db08c03e
parentb10d322c41f66dc7c77c36f90a3532269b25ea93 (diff)
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.
-rw-r--r--django/db/backends/base/operations.py3
-rw-r--r--django/db/backends/oracle/operations.py7
-rw-r--r--django/db/models/fields/__init__.py214
-rw-r--r--docs/releases/3.0.txt11
-rw-r--r--tests/invalid_models_tests/test_ordinary_fields.py15
-rw-r--r--tests/model_fields/test_autofield.py42
-rw-r--r--tests/model_fields/test_integerfield.py2
-rw-r--r--tests/validation/models.py5
8 files changed, 171 insertions, 128 deletions
diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py
index d17dc8dcce..76abc6dcb2 100644
--- a/django/db/backends/base/operations.py
+++ b/django/db/backends/base/operations.py
@@ -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',
diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py
index 016d4c9a59..3cad02fa41 100644
--- a/django/db/backends/oracle/operations.py
+++ b/django/db/backends/oracle/operations.py
@@ -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'}
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 1aad845470..6e924a4adf 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -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)
diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt
index 2e345f6fe5..5bb0691363 100644
--- a/docs/releases/3.0.txt
+++ b/docs/releases/3.0.txt
@@ -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`
---------------------------
diff --git a/tests/invalid_models_tests/test_ordinary_fields.py b/tests/invalid_models_tests/test_ordinary_fields.py
index a3231ee36a..5bb1847a70 100644
--- a/tests/invalid_models_tests/test_ordinary_fields.py
+++ b/tests/invalid_models_tests/test_ordinary_fields.py
@@ -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):
diff --git a/tests/model_fields/test_autofield.py b/tests/model_fields/test_autofield.py
index 73220ae62b..db8e79e34b 100644
--- a/tests/model_fields/test_autofield.py
+++ b/tests/model_fields/test_autofield.py
@@ -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))
diff --git a/tests/model_fields/test_integerfield.py b/tests/model_fields/test_integerfield.py
index c72cb48015..c0bb7627cf 100644
--- a/tests/model_fields/test_integerfield.py
+++ b/tests/model_fields/test_integerfield.py
@@ -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)
diff --git a/tests/validation/models.py b/tests/validation/models.py
index 953370bc37..e8e18cfbce 100644
--- a/tests/validation/models.py
+++ b/tests/validation/models.py
@@ -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."
+)