summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Forbes <tom@tomforb.es>2020-07-12 13:59:57 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-12-15 11:25:46 +0100
commitb5e12d490af3debca8c55ab3c1698189fdedbbdb (patch)
tree5fe3005ac567f3addf78b81ae033191e2fa642f4
parentb960e4ed722a04a9db0d35293f76e253eedf9126 (diff)
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.
-rw-r--r--django/apps/config.py10
-rw-r--r--django/conf/app_template/apps.py-tpl1
-rw-r--r--django/conf/global_settings.py3
-rw-r--r--django/conf/project_template/project_name/settings.py-tpl5
-rw-r--r--django/contrib/admin/apps.py1
-rw-r--r--django/contrib/auth/apps.py1
-rw-r--r--django/contrib/contenttypes/apps.py1
-rw-r--r--django/contrib/flatpages/apps.py1
-rw-r--r--django/contrib/gis/apps.py1
-rw-r--r--django/contrib/redirects/apps.py1
-rw-r--r--django/contrib/sitemaps/apps.py1
-rw-r--r--django/contrib/sites/apps.py1
-rw-r--r--django/db/models/base.py25
-rw-r--r--django/db/models/options.py37
-rw-r--r--docs/ref/applications.txt11
-rw-r--r--docs/ref/checks.txt2
-rw-r--r--docs/ref/models/fields.txt14
-rw-r--r--docs/ref/settings.txt11
-rw-r--r--docs/releases/3.2.txt42
-rw-r--r--docs/topics/db/models.txt14
-rw-r--r--tests/admin_scripts/tests.py15
-rw-r--r--tests/apps/apps.py5
-rw-r--r--tests/apps/tests.py28
-rw-r--r--tests/check_framework/apps.py10
-rw-r--r--tests/check_framework/test_model_checks.py57
-rw-r--r--tests/model_options/apps.py25
-rw-r--r--tests/model_options/test_default_pk.py101
-rw-r--r--tests/test_sqlite.py2
28 files changed, 415 insertions, 11 deletions
diff --git a/django/apps/config.py b/django/apps/config.py
index 928e19ac44..6d794eee3a 100644
--- a/django/apps/config.py
+++ b/django/apps/config.py
@@ -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
diff --git a/django/conf/app_template/apps.py-tpl b/django/conf/app_template/apps.py-tpl
index 9b2ce5289c..b705352181 100644
--- a/django/conf/app_template/apps.py-tpl
+++ b/django/conf/app_template/apps.py-tpl
@@ -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 }}'
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 381ad63ae6..cf9fae496e 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -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'
diff --git a/django/conf/project_template/project_name/settings.py-tpl b/django/conf/project_template/project_name/settings.py-tpl
index 444c899b2b..7830fb2f3c 100644
--- a/django/conf/project_template/project_name/settings.py-tpl
+++ b/django/conf/project_template/project_name/settings.py-tpl
@@ -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'
diff --git a/django/contrib/admin/apps.py b/django/contrib/admin/apps.py
index 85e9ff830a..c4fba8837c 100644
--- a/django/contrib/admin/apps.py
+++ b/django/contrib/admin/apps.py
@@ -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")
diff --git a/django/contrib/auth/apps.py b/django/contrib/auth/apps.py
index b9d271bb1f..4e4ef06d27 100644
--- a/django/contrib/auth/apps.py
+++ b/django/contrib/auth/apps.py
@@ -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")
diff --git a/django/contrib/contenttypes/apps.py b/django/contrib/contenttypes/apps.py
index 1a8e25b98e..390afb3fcf 100644
--- a/django/contrib/contenttypes/apps.py
+++ b/django/contrib/contenttypes/apps.py
@@ -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")
diff --git a/django/contrib/flatpages/apps.py b/django/contrib/flatpages/apps.py
index 330ee05063..4f5ef17004 100644
--- a/django/contrib/flatpages/apps.py
+++ b/django/contrib/flatpages/apps.py
@@ -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")
diff --git a/django/contrib/gis/apps.py b/django/contrib/gis/apps.py
index 662ae43d8d..e582e76760 100644
--- a/django/contrib/gis/apps.py
+++ b/django/contrib/gis/apps.py
@@ -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")
diff --git a/django/contrib/redirects/apps.py b/django/contrib/redirects/apps.py
index cab67424e7..c1d80ee3c1 100644
--- a/django/contrib/redirects/apps.py
+++ b/django/contrib/redirects/apps.py
@@ -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")
diff --git a/django/contrib/sitemaps/apps.py b/django/contrib/sitemaps/apps.py
index 502d6890d0..ec795eab87 100644
--- a/django/contrib/sitemaps/apps.py
+++ b/django/contrib/sitemaps/apps.py
@@ -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")
diff --git a/django/contrib/sites/apps.py b/django/contrib/sites/apps.py
index ed52d34786..7f820dcc79 100644
--- a/django/contrib/sites/apps.py
+++ b/django/contrib/sites/apps.py
@@ -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")
diff --git a/django/db/models/base.py b/django/db/models/base.py
index de044886c3..822aad080d 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -1290,11 +1290,36 @@ 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."""
errors = []
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 0e28b6812a..4028e05b99 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -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):
diff --git a/docs/ref/applications.txt b/docs/ref/applications.txt
index 765e9e8d3c..2cf175e9f0 100644
--- a/docs/ref/applications.txt
+++ b/docs/ref/applications.txt
@@ -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
--------------------
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index 2ac5d4cb1d..a6e66d462f 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -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
--------
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index 93c011fe2c..759e004fcf 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -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``
----------
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index bfa62f1508..8cb14e9768 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -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
diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt
index 93b60690b3..5b1969698c 100644
--- a/docs/releases/3.2.txt
+++ b/docs/releases/3.2.txt
@@ -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
----------------------
diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt
index 81eb5b516b..26a6d7dc58 100644
--- a/docs/topics/db/models.txt
+++ b/docs/topics/db/models.txt
@@ -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
diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py
index a82f5be5ed..7a38306d17 100644
--- a/tests/admin_scripts/tests.py
+++ b/tests/admin_scripts/tests.py
@@ -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."""
diff --git a/tests/apps/apps.py b/tests/apps/apps.py
index d322b28f2b..efd2983779 100644
--- a/tests/apps/apps.py
+++ b/tests/apps/apps.py
@@ -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'
diff --git a/tests/apps/tests.py b/tests/apps/tests.py
index 26d135f244..0e1f918bcc 100644
--- a/tests/apps/tests.py
+++ b/tests/apps/tests.py
@@ -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
diff --git a/tests/check_framework/apps.py b/tests/check_framework/apps.py
new file mode 100644
index 0000000000..c57994f722
--- /dev/null
+++ b/tests/check_framework/apps.py
@@ -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'
diff --git a/tests/check_framework/test_model_checks.py b/tests/check_framework/test_model_checks.py
index ce2d73fa20..d4342ada07 100644
--- a/tests/check_framework/test_model_checks.py
+++ b/tests/check_framework/test_model_checks.py
@@ -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()), [])
diff --git a/tests/model_options/apps.py b/tests/model_options/apps.py
new file mode 100644
index 0000000000..be2250fe7a
--- /dev/null
+++ b/tests/model_options/apps.py
@@ -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'
diff --git a/tests/model_options/test_default_pk.py b/tests/model_options/test_default_pk.py
new file mode 100644
index 0000000000..b678848916
--- /dev/null
+++ b/tests/model_options/test_default_pk.py
@@ -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)
diff --git a/tests/test_sqlite.py b/tests/test_sqlite.py
index f1b65f7d01..099f37e56d 100644
--- a/tests/test_sqlite.py
+++ b/tests/test_sqlite.py
@@ -27,3 +27,5 @@ SECRET_KEY = "django_tests_secret_key"
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
+
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'