Fixed #31007 -- Allowed specifying type of auto-created primary keys.

This also changes the default type of auto-created primary keys
for new apps and projects to BigAutoField.
This commit is contained in:
Tom Forbes 2020-07-12 13:59:57 +01:00 committed by Mariusz Felisiak
parent b960e4ed72
commit b5e12d490a
28 changed files with 415 additions and 11 deletions

View File

@ -5,6 +5,7 @@ from importlib import import_module
from django.core.exceptions import ImproperlyConfigured
from django.utils.deprecation import RemovedInDjango41Warning
from django.utils.functional import cached_property
from django.utils.module_loading import import_string, module_has_submodule
APPS_MODULE_NAME = 'apps'
@ -55,6 +56,15 @@ class AppConfig:
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.label)
@cached_property
def default_auto_field(self):
from django.conf import settings
return settings.DEFAULT_AUTO_FIELD
@property
def _is_default_auto_field_overridden(self):
return self.__class__.default_auto_field is not AppConfig.default_auto_field
def _path_from_module(self, module):
"""Attempt to determine app's filesystem path from its module."""
# See #21874 for extended discussion of the behavior of this method in

View File

@ -2,4 +2,5 @@ from django.apps import AppConfig
class {{ camel_case_app_name }}Config(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = '{{ app_name }}'

View File

@ -414,6 +414,9 @@ THOUSAND_SEPARATOR = ','
DEFAULT_TABLESPACE = ''
DEFAULT_INDEX_TABLESPACE = ''
# Default primary key field type.
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Default X-Frame-Options header value
X_FRAME_OPTIONS = 'DENY'

View File

@ -118,3 +118,8 @@ USE_TZ = True
# https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
STATIC_URL = '/static/'
# Default primary key field type
# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
class SimpleAdminConfig(AppConfig):
"""Simple AppConfig which does not do automatic discovery."""
default_auto_field = 'django.db.models.AutoField'
default_site = 'django.contrib.admin.sites.AdminSite'
name = 'django.contrib.admin'
verbose_name = _("Administration")

View File

@ -11,6 +11,7 @@ from .signals import user_logged_in
class AuthConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.auth'
verbose_name = _("Authentication and Authorization")

View File

@ -12,6 +12,7 @@ from .management import (
class ContentTypesConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.contenttypes'
verbose_name = _("Content Types")

View File

@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _
class FlatPagesConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.flatpages'
verbose_name = _("Flat Pages")

View File

@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
class GISConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.gis'
verbose_name = _("GIS")

View File

@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _
class RedirectsConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.redirects'
verbose_name = _("Redirects")

View File

@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _
class SiteMapsConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.sitemaps'
verbose_name = _("Site Maps")

View File

@ -8,6 +8,7 @@ from .management import create_default_site
class SitesConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.sites'
verbose_name = _("Sites")

View File

@ -1290,10 +1290,35 @@ class Model(metaclass=ModelBase):
*cls._check_indexes(databases),
*cls._check_ordering(),
*cls._check_constraints(databases),
*cls._check_default_pk(),
]
return errors
@classmethod
def _check_default_pk(cls):
if (
cls._meta.pk.auto_created and
not settings.is_overridden('DEFAULT_AUTO_FIELD') and
not cls._meta.app_config._is_default_auto_field_overridden
):
return [
checks.Warning(
f"Auto-created primary key used when not defining a "
f"primary key type, by default "
f"'{settings.DEFAULT_AUTO_FIELD}'.",
hint=(
f"Configure the DEFAULT_AUTO_FIELD setting or the "
f"{cls._meta.app_config.__class__.__qualname__}."
f"default_auto_field attribute to point to a subclass "
f"of AutoField, e.g. 'django.db.models.BigAutoField'."
),
obj=cls,
id='models.W042',
),
]
return []
@classmethod
def _check_swappable(cls):
"""Check if the swapped model exists."""

View File

@ -5,12 +5,13 @@ from collections import defaultdict
from django.apps import apps
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db import connections
from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint
from django.db.models.query_utils import PathInfo
from django.utils.datastructures import ImmutableList, OrderedSet
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.utils.text import camel_case_to_spaces, format_lazy
from django.utils.translation import override
@ -217,6 +218,37 @@ class Options:
new_objs.append(obj)
return new_objs
def _get_default_pk_class(self):
pk_class_path = getattr(
self.app_config,
'default_auto_field',
settings.DEFAULT_AUTO_FIELD,
)
if self.app_config and self.app_config._is_default_auto_field_overridden:
app_config_class = type(self.app_config)
source = (
f'{app_config_class.__module__}.'
f'{app_config_class.__qualname__}.default_auto_field'
)
else:
source = 'DEFAULT_AUTO_FIELD'
if not pk_class_path:
raise ImproperlyConfigured(f'{source} must not be empty.')
try:
pk_class = import_string(pk_class_path)
except ImportError as e:
msg = (
f"{source} refers to the module '{pk_class_path}' that could "
f"not be imported."
)
raise ImproperlyConfigured(msg) from e
if not issubclass(pk_class, AutoField):
raise ValueError(
f"Primary key '{pk_class_path}' referred by {source} must "
f"subclass AutoField."
)
return pk_class
def _prepare(self, model):
if self.order_with_respect_to:
# The app registry will not be ready at this point, so we cannot
@ -250,7 +282,8 @@ class Options:
field.primary_key = True
self.setup_pk(field)
else:
auto = AutoField(verbose_name='ID', primary_key=True, auto_created=True)
pk_class = self._get_default_pk_class()
auto = pk_class(verbose_name='ID', primary_key=True, auto_created=True)
model.add_to_class('id', auto)
def add_manager(self, manager):

View File

@ -90,6 +90,7 @@ would provide a proper name for the admin::
from django.apps import AppConfig
class RockNRollConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'rock_n_roll'
verbose_name = "Rock n roll"
@ -219,6 +220,16 @@ Configurable attributes
By default, this attribute isn't set.
.. attribute:: AppConfig.default_auto_field
.. versionadded:: 3.2
The implicit primary key type to add to models within this app. You can
use this to keep :class:`~django.db.models.AutoField` as the primary key
type for third party applications.
By default, this is the value of :setting:`DEFAULT_AUTO_FIELD`.
Read-only attributes
--------------------

View File

@ -378,6 +378,8 @@ Models
* **models.W040**: ``<database>`` does not support indexes with non-key
columns.
* **models.E041**: ``constraints`` refers to the joined field ``<field name>``.
* **models.W042**: Auto-created primary key used when not defining a primary
key type, by default ``django.db.models.AutoField``.
Security
--------

View File

@ -415,9 +415,12 @@ cross-site scripting attack.
If ``True``, this field is the primary key for the model.
If you don't specify ``primary_key=True`` for any field in your model, Django
will automatically add an :class:`AutoField` to hold the primary key, so you
don't need to set ``primary_key=True`` on any of your fields unless you want to
override the default primary-key behavior. For more, see
will automatically add a field to hold the primary key, so you don't need to
set ``primary_key=True`` on any of your fields unless you want to override the
default primary-key behavior. The type of auto-created primary key fields can
be specified per app in :attr:`AppConfig.default_auto_field
<django.apps.AppConfig.default_auto_field>` or globally in the
:setting:`DEFAULT_AUTO_FIELD` setting. For more, see
:ref:`automatic-primary-key-fields`.
``primary_key=True`` implies :attr:`null=False <Field.null>` and
@ -428,6 +431,11 @@ The primary key field is read-only. If you change the value of the primary
key on an existing object and then save it, a new object will be created
alongside the old one.
.. versionchanged:: 3.2
In older versions, auto-created primary key fields were always
:class:`AutoField`\s.
``unique``
----------

View File

@ -1245,6 +1245,17 @@ format has higher precedence and will be applied instead.
See also :setting:`NUMBER_GROUPING`, :setting:`THOUSAND_SEPARATOR` and
:setting:`USE_THOUSAND_SEPARATOR`.
.. setting:: DEFAULT_AUTO_FIELD
``DEFAULT_AUTO_FIELD``
----------------------
.. versionadded:: 3.2
Default: ``'``:class:`django.db.models.AutoField`\ ``'``
Default primary key field type to use for models that don't have a field with
:attr:`primary_key=True <django.db.models.Field.primary_key>`.
.. setting:: DEFAULT_CHARSET

View File

@ -53,6 +53,48 @@ needed. As a consequence, it's deprecated.
See :ref:`configuring-applications-ref` for full details.
Customizing type of auto-created primary keys
---------------------------------------------
When defining a model, if no field in a model is defined with
:attr:`primary_key=True <django.db.models.Field.primary_key>` an implicit
primary key is added. The type of this implicit primary key can now be
controlled via the :setting:`DEFAULT_AUTO_FIELD` setting and
:attr:`AppConfig.default_auto_field <django.apps.AppConfig.default_auto_field>`
attribute. No more needing to override primary keys in all models.
Maintaining the historical behavior, the default value for
:setting:`DEFAULT_AUTO_FIELD` is :class:`~django.db.models.AutoField`. Starting
with 3.2 new projects are generated with :setting:`DEFAULT_AUTO_FIELD` set to
:class:`~django.db.models.BigAutoField`. Also, new apps are generated with
:attr:`AppConfig.default_auto_field <django.apps.AppConfig.default_auto_field>`
set to :class:`~django.db.models.BigAutoField`. In a future Django release the
default value of :setting:`DEFAULT_AUTO_FIELD` will be changed to
:class:`~django.db.models.BigAutoField`.
To avoid unwanted migrations in the future, either explicitly set
:setting:`DEFAULT_AUTO_FIELD` to :class:`~django.db.models.AutoField`::
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
or configure it on a per-app basis::
from django.apps import AppConfig
class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'my_app'
or on a per-model basis::
from django.db import models
class MyModel(models.Model):
id = models.AutoField(primary_key=True)
In anticipation of the changing default, a system check will provide a warning
if you do not have an explicit setting for :setting:`DEFAULT_AUTO_FIELD`.
``pymemcache`` support
----------------------

View File

@ -259,11 +259,12 @@ details can be found in the :ref:`common model field option reference
Automatic primary key fields
----------------------------
By default, Django gives each model the following field::
By default, Django gives each model an auto-incrementing primary key with the
type specified per app in :attr:`AppConfig.default_auto_field
<django.apps.AppConfig.default_auto_field>` or globally in the
:setting:`DEFAULT_AUTO_FIELD` setting. For example::
id = models.AutoField(primary_key=True)
This is an auto-incrementing primary key.
id = models.BigAutoField(primary_key=True)
If you'd like to specify a custom primary key, specify
:attr:`primary_key=True <Field.primary_key>` on one of your fields. If Django
@ -273,6 +274,11 @@ sees you've explicitly set :attr:`Field.primary_key`, it won't add the automatic
Each model requires exactly one field to have :attr:`primary_key=True
<Field.primary_key>` (either explicitly declared or automatically added).
.. versionchanged:: 3.2
In older versions, auto-created primary key fields were always
:class:`AutoField`\s.
.. _verbose-field-names:
Verbose field names

View File

@ -61,6 +61,7 @@ class AdminScriptTestCase(SimpleTestCase):
settings_file.write("%s\n" % extra)
exports = [
'DATABASES',
'DEFAULT_AUTO_FIELD',
'ROOT_URLCONF',
'SECRET_KEY',
]
@ -2188,6 +2189,20 @@ class StartApp(AdminScriptTestCase):
"won't replace conflicting files."
)
def test_template(self):
out, err = self.run_django_admin(['startapp', 'new_app'])
self.assertNoOutput(err)
app_path = os.path.join(self.test_dir, 'new_app')
self.assertIs(os.path.exists(app_path), True)
with open(os.path.join(app_path, 'apps.py')) as f:
content = f.read()
self.assertIn('class NewAppConfig(AppConfig)', content)
self.assertIn(
"default_auto_field = 'django.db.models.BigAutoField'",
content,
)
self.assertIn("name = 'new_app'", content)
class DiffSettings(AdminScriptTestCase):
"""Tests for diffsettings management command."""

View File

@ -31,3 +31,8 @@ class PlainAppsConfig(AppConfig):
class RelabeledAppsConfig(AppConfig):
name = 'apps'
label = 'relabeled'
class ModelPKAppsConfig(AppConfig):
name = 'apps'
default_auto_field = 'django.db.models.BigAutoField'

View File

@ -102,8 +102,8 @@ class AppsTests(SimpleTestCase):
def test_no_such_app_config_with_choices(self):
msg = (
"Module 'apps.apps' does not contain a 'NoSuchConfig' class. "
"Choices are: 'BadConfig', 'MyAdmin', 'MyAuth', 'NoSuchApp', "
"'PlainAppsConfig', 'RelabeledAppsConfig'."
"Choices are: 'BadConfig', 'ModelPKAppsConfig', 'MyAdmin', "
"'MyAuth', 'NoSuchApp', 'PlainAppsConfig', 'RelabeledAppsConfig'."
)
with self.assertRaisesMessage(ImportError, msg):
with self.settings(INSTALLED_APPS=['apps.apps.NoSuchConfig']):
@ -436,6 +436,30 @@ class AppConfigTests(SimpleTestCase):
ac = AppConfig('label', Stub(__path__=['a']))
self.assertEqual(repr(ac), '<AppConfig: label>')
@override_settings(
INSTALLED_APPS=['apps.apps.ModelPKAppsConfig'],
DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField',
)
def test_app_default_auto_field(self):
apps_config = apps.get_app_config('apps')
self.assertEqual(
apps_config.default_auto_field,
'django.db.models.BigAutoField',
)
self.assertIs(apps_config._is_default_auto_field_overridden, True)
@override_settings(
INSTALLED_APPS=['apps.apps.PlainAppsConfig'],
DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField',
)
def test_default_auto_field_setting(self):
apps_config = apps.get_app_config('apps')
self.assertEqual(
apps_config.default_auto_field,
'django.db.models.SmallAutoField',
)
self.assertIs(apps_config._is_default_auto_field_overridden, False)
class NamespacePackageAppTests(SimpleTestCase):
# We need nsapp to be top-level so our multiple-paths tests can add another

View File

@ -0,0 +1,10 @@
from django.apps import AppConfig
class CheckDefaultPKConfig(AppConfig):
name = 'check_framework'
class CheckPKConfig(AppConfig):
name = 'check_framework'
default_auto_field = 'django.db.models.BigAutoField'

View File

@ -1,3 +1,5 @@
from unittest import mock
from django.core import checks
from django.core.checks import Error, Warning
from django.db import models
@ -358,3 +360,58 @@ class ConstraintNameTests(TestCase):
constraints = [constraint]
self.assertEqual(checks.run_checks(app_configs=apps.get_app_configs()), [])
def mocked_is_overridden(self, setting):
# Force treating DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' as a not
# overridden setting.
return (
setting != 'DEFAULT_AUTO_FIELD' or
self.DEFAULT_AUTO_FIELD != 'django.db.models.AutoField'
)
@mock.patch('django.conf.UserSettingsHolder.is_overridden', mocked_is_overridden)
@override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField')
@isolate_apps('check_framework.apps.CheckDefaultPKConfig', attr_name='apps')
@override_system_checks([checks.model_checks.check_all_models])
class ModelDefaultAutoFieldTests(SimpleTestCase):
def test_auto_created_pk(self):
class Model(models.Model):
pass
self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [
Warning(
"Auto-created primary key used when not defining a primary "
"key type, by default 'django.db.models.AutoField'.",
hint=(
"Configure the DEFAULT_AUTO_FIELD setting or the "
"CheckDefaultPKConfig.default_auto_field attribute to "
"point to a subclass of AutoField, e.g. "
"'django.db.models.BigAutoField'."
),
obj=Model,
id='models.W042',
),
])
@override_settings(DEFAULT_AUTO_FIELD='django.db.models.BigAutoField')
def test_default_auto_field_setting(self):
class Model(models.Model):
pass
self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [])
def test_explicit_pk(self):
class Model(models.Model):
id = models.BigAutoField(primary_key=True)
self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [])
@isolate_apps('check_framework.apps.CheckPKConfig', kwarg_name='apps')
def test_app_default_auto_field(self, apps):
class ModelWithPkViaAppConfig(models.Model):
class Meta:
app_label = 'check_framework.apps.CheckPKConfig'
self.assertEqual(checks.run_checks(app_configs=apps.get_app_configs()), [])

View File

@ -0,0 +1,25 @@
from django.apps import AppConfig
class ModelDefaultPKConfig(AppConfig):
name = 'model_options'
class ModelPKConfig(AppConfig):
name = 'model_options'
default_auto_field = 'django.db.models.SmallAutoField'
class ModelPKNonAutoConfig(AppConfig):
name = 'model_options'
default_auto_field = 'django.db.models.TextField'
class ModelPKNoneConfig(AppConfig):
name = 'model_options'
default_auto_field = None
class ModelPKNonexistentConfig(AppConfig):
name = 'model_options'
default_auto_field = 'django.db.models.NonexistentAutoField'

View File

@ -0,0 +1,101 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.test import SimpleTestCase, override_settings
from django.test.utils import isolate_apps
@isolate_apps('model_options')
class TestDefaultPK(SimpleTestCase):
@override_settings(DEFAULT_AUTO_FIELD='django.db.models.NonexistentAutoField')
def test_default_auto_field_setting_nonexistent(self):
msg = (
"DEFAULT_AUTO_FIELD refers to the module "
"'django.db.models.NonexistentAutoField' that could not be "
"imported."
)
with self.assertRaisesMessage(ImproperlyConfigured, msg):
class Model(models.Model):
pass
@isolate_apps('model_options.apps.ModelPKNonexistentConfig')
def test_app_default_auto_field_nonexistent(self):
msg = (
"model_options.apps.ModelPKNonexistentConfig.default_auto_field "
"refers to the module 'django.db.models.NonexistentAutoField' "
"that could not be imported."
)
with self.assertRaisesMessage(ImproperlyConfigured, msg):
class Model(models.Model):
pass
@override_settings(DEFAULT_AUTO_FIELD='django.db.models.TextField')
def test_default_auto_field_setting_non_auto(self):
msg = (
"Primary key 'django.db.models.TextField' referred by "
"DEFAULT_AUTO_FIELD must subclass AutoField."
)
with self.assertRaisesMessage(ValueError, msg):
class Model(models.Model):
pass
@isolate_apps('model_options.apps.ModelPKNonAutoConfig')
def test_app_default_auto_field_non_auto(self):
msg = (
"Primary key 'django.db.models.TextField' referred by "
"model_options.apps.ModelPKNonAutoConfig.default_auto_field must "
"subclass AutoField."
)
with self.assertRaisesMessage(ValueError, msg):
class Model(models.Model):
pass
@override_settings(DEFAULT_AUTO_FIELD=None)
def test_default_auto_field_setting_none(self):
msg = 'DEFAULT_AUTO_FIELD must not be empty.'
with self.assertRaisesMessage(ImproperlyConfigured, msg):
class Model(models.Model):
pass
@isolate_apps('model_options.apps.ModelPKNoneConfig')
def test_app_default_auto_field_none(self):
msg = (
'model_options.apps.ModelPKNoneConfig.default_auto_field must not '
'be empty.'
)
with self.assertRaisesMessage(ImproperlyConfigured, msg):
class Model(models.Model):
pass
@isolate_apps('model_options.apps.ModelDefaultPKConfig')
@override_settings(DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField')
def test_default_auto_field_setting(self):
class Model(models.Model):
pass
self.assertIsInstance(Model._meta.pk, models.SmallAutoField)
@isolate_apps('model_options.apps.ModelPKConfig')
@override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField')
def test_app_default_auto_field(self):
class Model(models.Model):
pass
self.assertIsInstance(Model._meta.pk, models.SmallAutoField)
@isolate_apps('model_options.apps.ModelDefaultPKConfig')
@override_settings(DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField')
def test_m2m_default_auto_field_setting(self):
class M2MModel(models.Model):
m2m = models.ManyToManyField('self')
m2m_pk = M2MModel._meta.get_field('m2m').remote_field.through._meta.pk
self.assertIsInstance(m2m_pk, models.SmallAutoField)
@isolate_apps('model_options.apps.ModelPKConfig')
@override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField')
def test_m2m_app_default_auto_field(self):
class M2MModel(models.Model):
m2m = models.ManyToManyField('self')
m2m_pk = M2MModel._meta.get_field('m2m').remote_field.through._meta.pk
self.assertIsInstance(m2m_pk, models.SmallAutoField)

View File

@ -27,3 +27,5 @@ SECRET_KEY = "django_tests_secret_key"
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'