diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index 8a0bcb7420c9..2702c38aa4d3 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -1,4 +1,3 @@
-import inspect
import re
from django.apps import apps as django_apps
@@ -6,6 +5,7 @@
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
+from django.utils.inspect import signature
from django.utils.module_loading import import_string
from django.views.decorators.debug import sensitive_variables
@@ -40,7 +40,7 @@ def get_backends():
def _get_compatible_backends(request, **credentials):
for backend, backend_path in _get_backends(return_tuples=True):
- backend_signature = inspect.signature(backend.authenticate)
+ backend_signature = signature(backend.authenticate)
try:
backend_signature.bind(request, **credentials)
except TypeError:
diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py
index feb225cf8c13..3222edb0d848 100644
--- a/django/contrib/gis/geos/libgeos.py
+++ b/django/contrib/gis/geos/libgeos.py
@@ -60,12 +60,15 @@ def load_geos():
# See the GEOS C API source code for more details on the library function
# calls: https://libgeos.org/doxygen/geos__c_8h_source.html
_lgeos = CDLL(lib_path)
- # Here we set up the prototypes for the initGEOS_r and finishGEOS_r
- # routines. These functions aren't actually called until they are
+ # Here we set up the prototypes for the GEOS_init_r and GEOS_finish_r
+ # routines, as well as the context handler setters.
+ # These functions aren't actually called until they are
# attached to a GEOS context handle -- this actually occurs in
# geos/prototypes/threadsafe.py.
- _lgeos.initGEOS_r.restype = CONTEXT_PTR
- _lgeos.finishGEOS_r.argtypes = [CONTEXT_PTR]
+ _lgeos.GEOS_init_r.restype = CONTEXT_PTR
+ _lgeos.GEOS_finish_r.argtypes = [CONTEXT_PTR]
+ _lgeos.GEOSContext_setErrorHandler_r.argtypes = [CONTEXT_PTR, ERRORFUNC]
+ _lgeos.GEOSContext_setNoticeHandler_r.argtypes = [CONTEXT_PTR, NOTICEFUNC]
# Set restype for compatibility across 32 and 64-bit platforms.
_lgeos.GEOSversion.restype = c_char_p
return _lgeos
diff --git a/django/contrib/gis/geos/prototypes/threadsafe.py b/django/contrib/gis/geos/prototypes/threadsafe.py
index d4f7ffb8ac94..48e13ebfb93d 100644
--- a/django/contrib/gis/geos/prototypes/threadsafe.py
+++ b/django/contrib/gis/geos/prototypes/threadsafe.py
@@ -8,12 +8,12 @@ class GEOSContextHandle(GEOSBase):
"""Represent a GEOS context handle."""
ptr_type = CONTEXT_PTR
- destructor = lgeos.finishGEOS_r
+ destructor = lgeos.GEOS_finish_r
def __init__(self):
- # Initializing the context handler for this thread with
- # the notice and error handler.
- self.ptr = lgeos.initGEOS_r(notice_h, error_h)
+ self.ptr = lgeos.GEOS_init_r()
+ lgeos.GEOSContext_setNoticeHandler_r(self.ptr, notice_h)
+ lgeos.GEOSContext_setErrorHandler_r(self.ptr, error_h)
# Defining a thread-local object and creating an instance
diff --git a/django/core/checks/security/csrf.py b/django/core/checks/security/csrf.py
index dee29384eeea..a4d03b5abb3d 100644
--- a/django/core/checks/security/csrf.py
+++ b/django/core/checks/security/csrf.py
@@ -1,7 +1,6 @@
-import inspect
-
from django.conf import settings
from django.core.checks import Error, Tags, Warning, register
+from django.utils.inspect import signature
W003 = Warning(
"You don't appear to be using Django's built-in "
@@ -56,7 +55,7 @@ def check_csrf_failure_view(app_configs, **kwargs):
errors.append(Error(msg, id="security.E102"))
else:
try:
- inspect.signature(view).bind(None, reason=None)
+ signature(view).bind(None, reason=None)
except TypeError:
msg = (
"The CSRF failure view '%s' does not take the correct number of "
diff --git a/django/core/checks/urls.py b/django/core/checks/urls.py
index 94bf8f990dd5..57141bd025ed 100644
--- a/django/core/checks/urls.py
+++ b/django/core/checks/urls.py
@@ -1,8 +1,8 @@
-import inspect
from collections import Counter
from django.conf import settings
from django.core.exceptions import ViewDoesNotExist
+from django.utils.inspect import signature
from . import Error, Tags, Warning, register
@@ -142,10 +142,9 @@ def check_custom_error_handlers(app_configs, **kwargs):
).format(status_code=status_code, path=path)
errors.append(Error(msg, hint=str(e), id="urls.E008"))
continue
- signature = inspect.signature(handler)
args = [None] * num_parameters
try:
- signature.bind(*args)
+ signature(handler).bind(*args)
except TypeError:
msg = (
"The custom handler{status_code} view '{path}' does not "
diff --git a/django/db/models/aggregates.py b/django/db/models/aggregates.py
index 1cf82416cb73..d1139e8bcc3c 100644
--- a/django/db/models/aggregates.py
+++ b/django/db/models/aggregates.py
@@ -369,6 +369,24 @@ def as_mysql(self, compiler, connection, **extra_context):
return sql, (*params, *delimiter_params)
def as_sqlite(self, compiler, connection, **extra_context):
+ if (
+ self.distinct
+ and isinstance(self.delimiter.value, Value)
+ and self.delimiter.value.value == ","
+ ):
+ clone = self.copy()
+ source_expressions = clone.get_source_expressions()
+ clone.set_source_expressions(
+ source_expressions[:1] + source_expressions[2:]
+ )
+
+ return clone.as_sql(
+ compiler,
+ connection,
+ function="GROUP_CONCAT",
+ **extra_context,
+ )
+
if connection.get_database_version() < (3, 44):
return self.as_sql(
compiler,
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index baa91cc2c173..63d0c1802b49 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -1,7 +1,6 @@
import copy
import datetime
import functools
-import inspect
from collections import defaultdict
from decimal import Decimal
from enum import Enum
@@ -17,6 +16,7 @@
from django.utils.deconstruct import deconstructible
from django.utils.functional import cached_property, classproperty
from django.utils.hashable import make_hashable
+from django.utils.inspect import signature
class SQLiteNumericMixin:
@@ -523,7 +523,7 @@ class Expression(BaseExpression, Combinable):
@classproperty
@functools.lru_cache(maxsize=128)
def _constructor_signature(cls):
- return inspect.signature(cls.__init__)
+ return signature(cls.__init__)
@classmethod
def _identity(cls, value):
diff --git a/django/http/request.py b/django/http/request.py
index c8adde768d23..573ae2b229d6 100644
--- a/django/http/request.py
+++ b/django/http/request.py
@@ -56,6 +56,7 @@ class HttpRequest:
# The encoding used in GET/POST dicts. None means use default setting.
_encoding = None
_upload_handlers = []
+ _multipart_parser_class = MultiPartParser
def __init__(self):
# WARNING: The `WSGIRequest` subclass doesn't call `super`.
@@ -364,6 +365,19 @@ def upload_handlers(self, upload_handlers):
)
self._upload_handlers = upload_handlers
+ @property
+ def multipart_parser_class(self):
+ return self._multipart_parser_class
+
+ @multipart_parser_class.setter
+ def multipart_parser_class(self, multipart_parser_class):
+ if hasattr(self, "_files"):
+ raise RuntimeError(
+ "You cannot set the multipart parser class after the upload has been "
+ "processed."
+ )
+ self._multipart_parser_class = multipart_parser_class
+
def parse_file_upload(self, META, post_data):
"""Return a tuple of (POST QueryDict, FILES MultiValueDict)."""
self.upload_handlers = ImmutableList(
@@ -373,7 +387,9 @@ def parse_file_upload(self, META, post_data):
"processed."
),
)
- parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
+ parser = self.multipart_parser_class(
+ META, post_data, self.upload_handlers, self.encoding
+ )
return parser.parse()
@property
diff --git a/django/template/base.py b/django/template/base.py
index d6595e38e701..9d75111e4240 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -60,7 +60,7 @@
from django.utils.deprecation import django_file_prefixes
from django.utils.formats import localize
from django.utils.html import conditional_escape
-from django.utils.inspect import lazy_annotations
+from django.utils.inspect import lazy_annotations, signature
from django.utils.regex_helper import _lazy_re_compile
from django.utils.safestring import SafeData, SafeString, mark_safe
from django.utils.text import get_text_list, smart_split, unescape_string_literal
@@ -1000,12 +1000,12 @@ def _resolve_lookup(self, context):
current = current()
except TypeError:
try:
- signature = inspect.signature(current)
+ current_signature = signature(current)
except ValueError: # No signature found.
current = context.template.engine.string_if_invalid
else:
try:
- signature.bind()
+ current_signature.bind()
except TypeError: # Arguments *were* required.
# Invalid method call.
current = context.template.engine.string_if_invalid
diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py
index 6b017cd91731..daa485eb35bf 100644
--- a/django/utils/deprecation.py
+++ b/django/utils/deprecation.py
@@ -8,6 +8,7 @@
from asgiref.sync import sync_to_async
import django
+from django.utils.inspect import signature
@functools.cache
@@ -163,7 +164,7 @@ def decorator(func):
if isinstance(func, staticmethod):
raise TypeError("Apply @staticmethod before @deprecate_posargs.")
- params = inspect.signature(func).parameters
+ params = signature(func).parameters
num_by_kind = Counter(param.kind for param in params.values())
if num_by_kind[inspect.Parameter.VAR_POSITIONAL] > 0:
diff --git a/django/utils/inspect.py b/django/utils/inspect.py
index f0f43ae17e35..a04669fc116a 100644
--- a/django/utils/inspect.py
+++ b/django/utils/inspect.py
@@ -17,16 +17,7 @@
@functools.lru_cache(maxsize=512)
def _get_func_parameters(func, remove_first):
- # As the annotations are not used in any case, inspect the signature with
- # FORWARDREF to leave any deferred annotations unevaluated.
- if PY314:
- signature = inspect.signature(
- func, annotation_format=annotationlib.Format.FORWARDREF
- )
- else:
- signature = inspect.signature(func)
-
- parameters = tuple(signature.parameters.values())
+ parameters = tuple(signature(func).parameters.values())
if remove_first:
parameters = parameters[1:]
return parameters
@@ -130,3 +121,14 @@ def lazy_annotations():
yield
finally:
inspect._signature_from_callable = original_helper
+
+
+def signature(obj):
+ """
+ A wrapper around inspect.signature that leaves deferred annotations
+ unevaluated on Python 3.14+, since they are not used in our case.
+ """
+ if PY314:
+ return inspect.signature(obj, annotation_format=annotationlib.Format.FORWARDREF)
+ else:
+ return inspect.signature(obj)
diff --git a/docs/internals/contributing/new-contributors.txt b/docs/internals/contributing/new-contributors.txt
index 63835289ebb5..bf80dee36589 100644
--- a/docs/internals/contributing/new-contributors.txt
+++ b/docs/internals/contributing/new-contributors.txt
@@ -69,8 +69,8 @@ Sign the Contributor License Agreement
--------------------------------------
The code that you write belongs to you or your employer. If your contribution
-is more than one or two lines of code, you need to sign the `CLA`_. See the
-`Contributor License Agreement FAQ`_ for a more thorough explanation.
+is more than one or two lines of code, you have the option to sign the `CLA`_.
+See the `Contributor License Agreement FAQ`_ for a more thorough explanation.
.. _CLA: https://www.djangoproject.com/foundation/cla/
.. _Contributor License Agreement FAQ: https://www.djangoproject.com/foundation/cla/faq/
diff --git a/docs/internals/contributing/writing-code/submitting-patches.txt b/docs/internals/contributing/writing-code/submitting-patches.txt
index 9c1d417031a2..6900045cc05d 100644
--- a/docs/internals/contributing/writing-code/submitting-patches.txt
+++ b/docs/internals/contributing/writing-code/submitting-patches.txt
@@ -59,11 +59,10 @@ and time availability), claim it by following these steps:
* Finally click the "Submit changes" button at the bottom to save.
.. note::
- The Django software foundation requests that anyone contributing more than
- a :ref:`trivial change %d ' % story1.id, 1)
- self.assertContains(response, '%d ' % story2.id, 1)
+ self.assertContains(response, '%s ' % story1.id, 1)
+ self.assertContains(response, '%s ' % story2.id, 1)
def test_pk_hidden_fields_with_list_display_links(self):
"""Similarly as test_pk_hidden_fields, but when the hidden pk fields
@@ -4798,19 +4798,19 @@ def test_pk_hidden_fields_with_list_display_links(self):
self.assertContains(
response,
'%d ' % (link1, story1.id),
+ '%s ' % (link1, story1.id),
1,
)
self.assertContains(
response,
- '%d ' % (link2, story2.id),
+ '%s ' % (link2, story2.id),
1,
)
@@ -7321,7 +7321,7 @@ def test_readonly_get(self):
response = self.client.get(
reverse("admin:admin_views_post_change", args=(p.pk,))
)
- self.assertContains(response, "%d amount of cool" % p.pk)
+ self.assertContains(response, "%s amount of cool" % p.pk)
def test_readonly_text_field(self):
p = Post.objects.create(
diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py
index bf6bf2703112..0a975dcb529e 100644
--- a/tests/aggregation/tests.py
+++ b/tests/aggregation/tests.py
@@ -3,6 +3,7 @@
import re
from decimal import Decimal
from itertools import chain
+from unittest import skipUnless
from django.core.exceptions import FieldError
from django.db import NotSupportedError, connection
@@ -579,6 +580,16 @@ def test_distinct_on_stringagg(self):
)
self.assertCountEqual(books["ratings"].split(","), ["3", "4", "4.5", "5"])
+ @skipUnless(connection.vendor == "sqlite", "Special default case for SQLite.")
+ def test_distinct_on_stringagg_sqlite_special_case(self):
+ """
+ Value(",") is the only delimiter usable on SQLite with distinct=True.
+ """
+ books = Book.objects.aggregate(
+ ratings=StringAgg(Cast(F("rating"), CharField()), Value(","), distinct=True)
+ )
+ self.assertCountEqual(books["ratings"].split(","), ["3.0", "4.0", "4.5", "5.0"])
+
@skipIfDBFeature("supports_aggregate_distinct_multiple_argument")
def test_raises_error_on_multiple_argument_distinct(self):
message = (
@@ -589,7 +600,7 @@ def test_raises_error_on_multiple_argument_distinct(self):
Book.objects.aggregate(
ratings=StringAgg(
Cast(F("rating"), CharField()),
- Value(","),
+ Value(";"),
distinct=True,
)
)
diff --git a/tests/auth_tests/test_auth_backends.py b/tests/auth_tests/test_auth_backends.py
index 0ba169249b22..3ea6ff6a6955 100644
--- a/tests/auth_tests/test_auth_backends.py
+++ b/tests/auth_tests/test_auth_backends.py
@@ -1,5 +1,7 @@
import sys
+import unittest
from datetime import date
+from typing import TYPE_CHECKING
from unittest import mock
from unittest.mock import patch
@@ -30,6 +32,7 @@
override_settings,
)
from django.urls import reverse
+from django.utils.version import PY314
from django.views.debug import ExceptionReporter, technical_500_response
from django.views.decorators.debug import sensitive_variables
@@ -41,6 +44,10 @@
UUIDUser,
)
+if TYPE_CHECKING:
+ type AnnotatedUsername = str
+ type AnnotatedPassword = str
+
class FilteredExceptionReporter(ExceptionReporter):
def get_traceback_frames(self):
@@ -1285,6 +1292,29 @@ def test_skips_backends_without_arguments(self):
def test_skips_backends_with_decorated_method(self):
self.assertEqual(authenticate(username="test", password="test"), self.user1)
+ @unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
+ @override_settings(
+ AUTHENTICATION_BACKENDS=[
+ "auth_tests.test_auth_backends.AnnotatedBackend",
+ ],
+ )
+ def test_backend_uses_deferred_annotations(self):
+ class AnnotatedBackend:
+ invariant_user = self.user1
+
+ def authenticate(
+ self,
+ request: HttpRequest,
+ username: AnnotatedUsername,
+ password: AnnotatedPassword,
+ ) -> User | None:
+ return self.invariant_user
+
+ with unittest.mock.patch(
+ "django.contrib.auth.import_string", return_value=AnnotatedBackend
+ ):
+ self.assertEqual(authenticate(username="test", password="test"), self.user1)
+
class ImproperlyConfiguredUserModelTest(TestCase):
"""
diff --git a/tests/auth_tests/test_context_processors.py b/tests/auth_tests/test_context_processors.py
index cebc1108dc8b..85b9e6367b6a 100644
--- a/tests/auth_tests/test_context_processors.py
+++ b/tests/auth_tests/test_context_processors.py
@@ -140,7 +140,7 @@ def test_user_attrs(self):
user = authenticate(username="super", password="secret")
response = self.client.get("/auth_processor_user/")
self.assertContains(response, "unicode: super")
- self.assertContains(response, "id: %d" % self.superuser.pk)
+ self.assertContains(response, "id: %s" % self.superuser.pk)
self.assertContains(response, "username: super")
# bug #12037 is tested by the {% url %} in the template:
self.assertContains(response, "url: /userpage/super/")
diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py
index 0701ac2d68ed..f5027da4e5eb 100644
--- a/tests/auth_tests/test_management.py
+++ b/tests/auth_tests/test_management.py
@@ -607,7 +607,7 @@ def test_validate_fk(self):
email = Email.objects.create(email="mymail@gmail.com")
Group.objects.all().delete()
nonexistent_group_id = 1
- msg = f"group instance with id {nonexistent_group_id} is not a valid choice."
+ msg = f"group instance with id {nonexistent_group_id!r} is not a valid choice."
with self.assertRaisesMessage(CommandError, msg):
call_command(
@@ -624,7 +624,7 @@ def test_validate_fk_environment_variable(self):
email = Email.objects.create(email="mymail@gmail.com")
Group.objects.all().delete()
nonexistent_group_id = 1
- msg = f"group instance with id {nonexistent_group_id} is not a valid choice."
+ msg = f"group instance with id {nonexistent_group_id!r} is not a valid choice."
with mock.patch.dict(
os.environ,
@@ -644,7 +644,7 @@ def test_validate_fk_via_option_interactive(self):
email = Email.objects.create(email="mymail@gmail.com")
Group.objects.all().delete()
nonexistent_group_id = 1
- msg = f"group instance with id {nonexistent_group_id} is not a valid choice."
+ msg = f"group instance with id {nonexistent_group_id!r} is not a valid choice."
@mock_inputs(
{
diff --git a/tests/backends/base/test_base.py b/tests/backends/base/test_base.py
index 120584e7fcf5..2d19840851fb 100644
--- a/tests/backends/base/test_base.py
+++ b/tests/backends/base/test_base.py
@@ -93,6 +93,7 @@ def test_release_memory_without_garbage_collection(self):
self.assertEqual(gc.garbage, [])
+@skipUnlessDBFeature("supports_transactions")
class DatabaseWrapperLoggingTests(TransactionTestCase):
available_apps = ["backends"]
diff --git a/tests/check_framework/test_security.py b/tests/check_framework/test_security.py
index d5cc8a9ad69f..0f03845c15b1 100644
--- a/tests/check_framework/test_security.py
+++ b/tests/check_framework/test_security.py
@@ -1,4 +1,6 @@
import itertools
+import unittest
+from typing import TYPE_CHECKING
from django.conf import settings
from django.core.checks.messages import Error, Warning
@@ -6,8 +8,12 @@
from django.core.management.utils import get_random_secret_key
from django.test import SimpleTestCase
from django.test.utils import override_settings
+from django.utils.version import PY314
from django.views.generic import View
+if TYPE_CHECKING:
+ from django.http.request import HttpRequest
+
class CheckSessionCookieSecureTest(SimpleTestCase):
@override_settings(
@@ -613,6 +619,12 @@ def failure_view_with_invalid_signature():
pass
+if PY314:
+
+ def failure_view_with_deferred_annotations(request: HttpRequest, reason: str):
+ pass
+
+
good_class_based_csrf_failure_view = View.as_view()
@@ -653,6 +665,15 @@ def test_failure_view_invalid_signature(self):
def test_failure_view_valid_class_based(self):
self.assertEqual(csrf.check_csrf_failure_view(None), [])
+ @unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
+ @override_settings(
+ CSRF_FAILURE_VIEW=(
+ "check_framework.test_security.failure_view_with_deferred_annotations"
+ ),
+ )
+ def test_failure_view_valid_deferred_annotations(self):
+ self.assertEqual(csrf.check_csrf_failure_view(None), [])
+
class CheckCrossOriginOpenerPolicyTest(SimpleTestCase):
@override_settings(
diff --git a/tests/check_framework/test_urls.py b/tests/check_framework/test_urls.py
index a3b219f3a489..5164fe7671e8 100644
--- a/tests/check_framework/test_urls.py
+++ b/tests/check_framework/test_urls.py
@@ -1,3 +1,5 @@
+import unittest
+
from django.conf import settings
from django.core.checks.messages import Error, Warning
from django.core.checks.urls import (
@@ -10,6 +12,7 @@
)
from django.test import SimpleTestCase
from django.test.utils import override_settings
+from django.utils.version import PY314
class CheckUrlConfigTests(SimpleTestCase):
@@ -323,6 +326,14 @@ def test_good_function_based_handlers(self):
result = check_custom_error_handlers(None)
self.assertEqual(result, [])
+ @unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
+ @override_settings(
+ ROOT_URLCONF="check_framework.urls.good_error_handler_deferred_annotations",
+ )
+ def test_good_function_based_handlers_deferred_annotations(self):
+ result = check_custom_error_handlers(None)
+ self.assertEqual(result, [])
+
@override_settings(
ROOT_URLCONF="check_framework.urls.good_class_based_error_handlers",
)
diff --git a/tests/check_framework/urls/good_error_handler_deferred_annotations.py b/tests/check_framework/urls/good_error_handler_deferred_annotations.py
new file mode 100644
index 000000000000..8b4ead556643
--- /dev/null
+++ b/tests/check_framework/urls/good_error_handler_deferred_annotations.py
@@ -0,0 +1,15 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from django.http.request import HttpRequest
+
+urlpatterns = []
+
+handler400 = __name__ + ".good_handler_deferred_annotations"
+handler403 = __name__ + ".good_handler_deferred_annotations"
+handler404 = __name__ + ".good_handler_deferred_annotations"
+handler500 = __name__ + ".good_handler_deferred_annotations"
+
+
+def good_handler_deferred_annotations(request: HttpRequest, exception=None):
+ pass
diff --git a/tests/deprecation/test_deprecate_posargs.py b/tests/deprecation/test_deprecate_posargs.py
index 166fe5dd69ed..8c70d5d20587 100644
--- a/tests/deprecation/test_deprecate_posargs.py
+++ b/tests/deprecation/test_deprecate_posargs.py
@@ -1,7 +1,13 @@
import inspect
+import unittest
+from typing import TYPE_CHECKING
from django.test import SimpleTestCase
from django.utils.deprecation import RemovedAfterNextVersionWarning, deprecate_posargs
+from django.utils.version import PY314
+
+if TYPE_CHECKING:
+ type AnnotatedKwarg = int
class DeprecatePosargsTests(SimpleTestCase):
@@ -355,6 +361,17 @@ def test_decorator_rejects_var_positional_param(self):
def func(*args, b=1):
return args, b
+ @unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
+ def test_decorator_rejects_var_positional_param_with_deferred_annotation(self):
+ with self.assertRaisesMessage(
+ TypeError,
+ "@deprecate_posargs() cannot be used with variable positional `*args`.",
+ ):
+
+ @deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
+ def func(*args, b: AnnotatedKwarg = 1):
+ return args, b
+
def test_decorator_does_not_apply_to_class(self):
with self.assertRaisesMessage(
TypeError,
diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py
index 02126fa89612..e495012e079f 100644
--- a/tests/expressions/tests.py
+++ b/tests/expressions/tests.py
@@ -5,6 +5,7 @@
from collections import namedtuple
from copy import deepcopy
from decimal import Decimal
+from typing import TYPE_CHECKING
from unittest import mock
from django.core.exceptions import FieldError
@@ -77,6 +78,7 @@
register_lookup,
)
from django.utils.functional import SimpleLazyObject
+from django.utils.version import PY314
from .models import (
UUID,
@@ -94,6 +96,9 @@
Time,
)
+if TYPE_CHECKING:
+ type AnnotatedKwarg = str
+
class BasicExpressionsTests(TestCase):
@classmethod
@@ -1599,6 +1604,14 @@ def set_source_expressions(self, exprs):
replaced = expression.replace_expressions({"replacement": Expression()})
self.assertEqual(replaced.get_source_expressions(), [falsey])
+ @unittest.skipUnless(PY314, "Deferred annotations are Python 3.14+ only")
+ def test_expression_signature_uses_deferred_annotations(self):
+ class AnnotatedExpression(Expression):
+ def __init__(self, *args, my_kw: AnnotatedKwarg, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.assertEqual(AnnotatedExpression(my_kw=""), AnnotatedExpression(my_kw=""))
+
class ExpressionsNumericTests(TestCase):
@classmethod
diff --git a/tests/file_uploads/views.py b/tests/file_uploads/views.py
index f4f3b5c51458..49916845d0fb 100644
--- a/tests/file_uploads/views.py
+++ b/tests/file_uploads/views.py
@@ -157,7 +157,7 @@ def file_upload_filename_case_view(request):
file = request.FILES["file_field"]
obj = FileModel()
obj.testfile.save(file.name, file)
- return HttpResponse("%d" % obj.pk)
+ return HttpResponse("%s" % obj.pk)
def file_upload_content_type_extra(request):
diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py
index 999555effe43..156c403c2343 100644
--- a/tests/fixtures_regress/tests.py
+++ b/tests/fixtures_regress/tests.py
@@ -499,6 +499,7 @@ def test_loaddata_works_when_fixture_has_forward_refs(self):
self.assertEqual(Book.objects.all()[0].id, 1)
self.assertEqual(Person.objects.all()[0].id, 4)
+ @skipUnlessDBFeature("supports_foreign_keys")
def test_loaddata_raises_error_when_fixture_has_invalid_foreign_key(self):
"""
Data with nonexistent child key references raises error.
diff --git a/tests/forms_tests/models.py b/tests/forms_tests/models.py
index d6d0725b32b3..b1319abe17f1 100644
--- a/tests/forms_tests/models.py
+++ b/tests/forms_tests/models.py
@@ -68,7 +68,7 @@ class Meta:
ordering = ("name",)
def __str__(self):
- return "ChoiceOption %d" % self.pk
+ return "ChoiceOption %s" % self.pk
def choice_default():
diff --git a/tests/generic_views/test_dates.py b/tests/generic_views/test_dates.py
index 140083d31538..d12b1f6baa61 100644
--- a/tests/generic_views/test_dates.py
+++ b/tests/generic_views/test_dates.py
@@ -895,7 +895,7 @@ def test_get_object_custom_queryset_numqueries(self):
def test_datetime_date_detail(self):
bs = BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0))
- res = self.client.get("/dates/booksignings/2008/apr/2/%d/" % bs.pk)
+ res = self.client.get("/dates/booksignings/2008/apr/2/%s/" % bs.pk)
self.assertEqual(res.status_code, 200)
@requires_tz_support
@@ -904,17 +904,17 @@ def test_aware_datetime_date_detail(self):
bs = BookSigning.objects.create(
event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=datetime.UTC)
)
- res = self.client.get("/dates/booksignings/2008/apr/2/%d/" % bs.pk)
+ res = self.client.get("/dates/booksignings/2008/apr/2/%s/" % bs.pk)
self.assertEqual(res.status_code, 200)
# 2008-04-02T00:00:00+03:00 (beginning of day) >
# 2008-04-01T22:00:00+00:00 (book signing event date).
bs.event_date = datetime.datetime(2008, 4, 1, 22, 0, tzinfo=datetime.UTC)
bs.save()
- res = self.client.get("/dates/booksignings/2008/apr/2/%d/" % bs.pk)
+ res = self.client.get("/dates/booksignings/2008/apr/2/%s/" % bs.pk)
self.assertEqual(res.status_code, 200)
# 2008-04-03T00:00:00+03:00 (end of day) > 2008-04-02T22:00:00+00:00
# (book signing event date).
bs.event_date = datetime.datetime(2008, 4, 2, 22, 0, tzinfo=datetime.UTC)
bs.save()
- res = self.client.get("/dates/booksignings/2008/apr/2/%d/" % bs.pk)
+ res = self.client.get("/dates/booksignings/2008/apr/2/%s/" % bs.pk)
self.assertEqual(res.status_code, 404)
diff --git a/tests/generic_views/test_edit.py b/tests/generic_views/test_edit.py
index d66b316090d0..b483ea1028ff 100644
--- a/tests/generic_views/test_edit.py
+++ b/tests/generic_views/test_edit.py
@@ -124,7 +124,7 @@ def test_create_with_object_url(self):
res = self.client.post("/edit/artists/create/", {"name": "Rene Magritte"})
self.assertEqual(res.status_code, 302)
artist = Artist.objects.get(name="Rene Magritte")
- self.assertRedirects(res, "/detail/artist/%d/" % artist.pk)
+ self.assertRedirects(res, "/detail/artist/%s/" % artist.pk)
self.assertQuerySetEqual(Artist.objects.all(), [artist])
def test_create_with_redirect(self):
@@ -148,7 +148,7 @@ def test_create_with_interpolated_redirect(self):
)
self.assertEqual(res.status_code, 302)
pk = Author.objects.first().pk
- self.assertRedirects(res, "/edit/author/%d/update/" % pk)
+ self.assertRedirects(res, "/edit/author/%s/update/" % pk)
# Also test with escaped chars in URL
res = self.client.post(
"/edit/authors/create/interpolate_redirect_nonascii/",
@@ -245,7 +245,7 @@ def setUpTestData(cls):
)
def test_update_post(self):
- res = self.client.get("/edit/author/%d/update/" % self.author.pk)
+ res = self.client.get("/edit/author/%s/update/" % self.author.pk)
self.assertEqual(res.status_code, 200)
self.assertIsInstance(res.context["form"], forms.ModelForm)
self.assertEqual(res.context["object"], self.author)
@@ -255,7 +255,7 @@ def test_update_post(self):
# Modification with both POST and PUT (browser compatible)
res = self.client.post(
- "/edit/author/%d/update/" % self.author.pk,
+ "/edit/author/%s/update/" % self.author.pk,
{"name": "Randall Munroe (xkcd)", "slug": "randall-munroe"},
)
self.assertEqual(res.status_code, 302)
@@ -266,7 +266,7 @@ def test_update_post(self):
def test_update_invalid(self):
res = self.client.post(
- "/edit/author/%d/update/" % self.author.pk,
+ "/edit/author/%s/update/" % self.author.pk,
{"name": "A" * 101, "slug": "randall-munroe"},
)
self.assertEqual(res.status_code, 200)
@@ -278,15 +278,15 @@ def test_update_invalid(self):
def test_update_with_object_url(self):
a = Artist.objects.create(name="Rene Magritte")
res = self.client.post(
- "/edit/artists/%d/update/" % a.pk, {"name": "Rene Magritte"}
+ "/edit/artists/%s/update/" % a.pk, {"name": "Rene Magritte"}
)
self.assertEqual(res.status_code, 302)
- self.assertRedirects(res, "/detail/artist/%d/" % a.pk)
+ self.assertRedirects(res, "/detail/artist/%s/" % a.pk)
self.assertQuerySetEqual(Artist.objects.all(), [a])
def test_update_with_redirect(self):
res = self.client.post(
- "/edit/author/%d/update/redirect/" % self.author.pk,
+ "/edit/author/%s/update/redirect/" % self.author.pk,
{"name": "Randall Munroe (author of xkcd)", "slug": "randall-munroe"},
)
self.assertEqual(res.status_code, 302)
@@ -298,7 +298,7 @@ def test_update_with_redirect(self):
def test_update_with_interpolated_redirect(self):
res = self.client.post(
- "/edit/author/%d/update/interpolate_redirect/" % self.author.pk,
+ "/edit/author/%s/update/interpolate_redirect/" % self.author.pk,
{"name": "Randall Munroe (author of xkcd)", "slug": "randall-munroe"},
)
self.assertQuerySetEqual(
@@ -307,10 +307,10 @@ def test_update_with_interpolated_redirect(self):
)
self.assertEqual(res.status_code, 302)
pk = Author.objects.first().pk
- self.assertRedirects(res, "/edit/author/%d/update/" % pk)
+ self.assertRedirects(res, "/edit/author/%s/update/" % pk)
# Also test with escaped chars in URL
res = self.client.post(
- "/edit/author/%d/update/interpolate_redirect_nonascii/" % self.author.pk,
+ "/edit/author/%s/update/interpolate_redirect_nonascii/" % self.author.pk,
{"name": "John Doe", "slug": "john-doe"},
)
self.assertEqual(res.status_code, 302)
@@ -318,7 +318,7 @@ def test_update_with_interpolated_redirect(self):
self.assertRedirects(res, "/%C3%A9dit/author/{}/update/".format(pk))
def test_update_with_special_properties(self):
- res = self.client.get("/edit/author/%d/update/special/" % self.author.pk)
+ res = self.client.get("/edit/author/%s/update/special/" % self.author.pk)
self.assertEqual(res.status_code, 200)
self.assertIsInstance(res.context["form"], views.AuthorForm)
self.assertEqual(res.context["object"], self.author)
@@ -327,11 +327,11 @@ def test_update_with_special_properties(self):
self.assertTemplateUsed(res, "generic_views/form.html")
res = self.client.post(
- "/edit/author/%d/update/special/" % self.author.pk,
+ "/edit/author/%s/update/special/" % self.author.pk,
{"name": "Randall Munroe (author of xkcd)", "slug": "randall-munroe"},
)
self.assertEqual(res.status_code, 302)
- self.assertRedirects(res, "/detail/author/%d/" % self.author.pk)
+ self.assertRedirects(res, "/detail/author/%s/" % self.author.pk)
self.assertQuerySetEqual(
Author.objects.values_list("name", flat=True),
["Randall Munroe (author of xkcd)"],
@@ -344,7 +344,7 @@ def test_update_without_redirect(self):
)
with self.assertRaisesMessage(ImproperlyConfigured, msg):
self.client.post(
- "/edit/author/%d/update/naive/" % self.author.pk,
+ "/edit/author/%s/update/naive/" % self.author.pk,
{"name": "Randall Munroe (author of xkcd)", "slug": "randall-munroe"},
)
@@ -379,37 +379,37 @@ def setUpTestData(cls):
)
def test_delete_by_post(self):
- res = self.client.get("/edit/author/%d/delete/" % self.author.pk)
+ res = self.client.get("/edit/author/%s/delete/" % self.author.pk)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.context["object"], self.author)
self.assertEqual(res.context["author"], self.author)
self.assertTemplateUsed(res, "generic_views/author_confirm_delete.html")
# Deletion with POST
- res = self.client.post("/edit/author/%d/delete/" % self.author.pk)
+ res = self.client.post("/edit/author/%s/delete/" % self.author.pk)
self.assertEqual(res.status_code, 302)
self.assertRedirects(res, "/list/authors/")
self.assertQuerySetEqual(Author.objects.all(), [])
def test_delete_by_delete(self):
# Deletion with browser compatible DELETE method
- res = self.client.delete("/edit/author/%d/delete/" % self.author.pk)
+ res = self.client.delete("/edit/author/%s/delete/" % self.author.pk)
self.assertEqual(res.status_code, 302)
self.assertRedirects(res, "/list/authors/")
self.assertQuerySetEqual(Author.objects.all(), [])
def test_delete_with_redirect(self):
- res = self.client.post("/edit/author/%d/delete/redirect/" % self.author.pk)
+ res = self.client.post("/edit/author/%s/delete/redirect/" % self.author.pk)
self.assertEqual(res.status_code, 302)
self.assertRedirects(res, "/edit/authors/create/")
self.assertQuerySetEqual(Author.objects.all(), [])
def test_delete_with_interpolated_redirect(self):
res = self.client.post(
- "/edit/author/%d/delete/interpolate_redirect/" % self.author.pk
+ "/edit/author/%s/delete/interpolate_redirect/" % self.author.pk
)
self.assertEqual(res.status_code, 302)
- self.assertRedirects(res, "/edit/authors/create/?deleted=%d" % self.author.pk)
+ self.assertRedirects(res, "/edit/authors/create/?deleted=%s" % self.author.pk)
self.assertQuerySetEqual(Author.objects.all(), [])
# Also test with escaped chars in URL
a = Author.objects.create(
@@ -422,14 +422,14 @@ def test_delete_with_interpolated_redirect(self):
self.assertRedirects(res, "/%C3%A9dit/authors/create/?deleted={}".format(a.pk))
def test_delete_with_special_properties(self):
- res = self.client.get("/edit/author/%d/delete/special/" % self.author.pk)
+ res = self.client.get("/edit/author/%s/delete/special/" % self.author.pk)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.context["object"], self.author)
self.assertEqual(res.context["thingy"], self.author)
self.assertNotIn("author", res.context)
self.assertTemplateUsed(res, "generic_views/confirm_delete.html")
- res = self.client.post("/edit/author/%d/delete/special/" % self.author.pk)
+ res = self.client.post("/edit/author/%s/delete/special/" % self.author.pk)
self.assertEqual(res.status_code, 302)
self.assertRedirects(res, "/list/authors/")
self.assertQuerySetEqual(Author.objects.all(), [])
@@ -437,29 +437,29 @@ def test_delete_with_special_properties(self):
def test_delete_without_redirect(self):
msg = "No URL to redirect to. Provide a success_url."
with self.assertRaisesMessage(ImproperlyConfigured, msg):
- self.client.post("/edit/author/%d/delete/naive/" % self.author.pk)
+ self.client.post("/edit/author/%s/delete/naive/" % self.author.pk)
def test_delete_with_form_as_post(self):
- res = self.client.get("/edit/author/%d/delete/form/" % self.author.pk)
+ res = self.client.get("/edit/author/%s/delete/form/" % self.author.pk)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.context["object"], self.author)
self.assertEqual(res.context["author"], self.author)
self.assertTemplateUsed(res, "generic_views/author_confirm_delete.html")
res = self.client.post(
- "/edit/author/%d/delete/form/" % self.author.pk, data={"confirm": True}
+ "/edit/author/%s/delete/form/" % self.author.pk, data={"confirm": True}
)
self.assertEqual(res.status_code, 302)
self.assertRedirects(res, "/list/authors/")
self.assertSequenceEqual(Author.objects.all(), [])
def test_delete_with_form_as_post_with_validation_error(self):
- res = self.client.get("/edit/author/%d/delete/form/" % self.author.pk)
+ res = self.client.get("/edit/author/%s/delete/form/" % self.author.pk)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.context["object"], self.author)
self.assertEqual(res.context["author"], self.author)
self.assertTemplateUsed(res, "generic_views/author_confirm_delete.html")
- res = self.client.post("/edit/author/%d/delete/form/" % self.author.pk)
+ res = self.client.post("/edit/author/%s/delete/form/" % self.author.pk)
self.assertEqual(res.status_code, 200)
self.assertEqual(len(res.context_data["form"].errors), 2)
self.assertEqual(
diff --git a/tests/gis_tests/geoapp/test_expressions.py b/tests/gis_tests/geoapp/test_expressions.py
index b56832bb6f66..a93bffdbbcc9 100644
--- a/tests/gis_tests/geoapp/test_expressions.py
+++ b/tests/gis_tests/geoapp/test_expressions.py
@@ -54,6 +54,7 @@ def test_update_from_other_field(self):
obj.point3.equals_exact(p1.transform(3857, clone=True), 0.1)
)
+ @skipUnlessDBFeature("has_Distance_function")
def test_multiple_annotation(self):
multi_field = MultiFields.objects.create(
point=Point(1, 1),
diff --git a/tests/many_to_one/tests.py b/tests/many_to_one/tests.py
index 4d2343e304fd..193e6e7086b3 100644
--- a/tests/many_to_one/tests.py
+++ b/tests/many_to_one/tests.py
@@ -889,7 +889,7 @@ def test_reverse_foreign_key_instance_to_field_caching(self):
def test_add_remove_set_by_pk_raises(self):
usa = Country.objects.create(name="United States")
chicago = City.objects.create(name="Chicago")
- msg = "'City' instance expected, got %s" % chicago.pk
+ msg = "'City' instance expected, got %r" % chicago.pk
with self.assertRaisesMessage(TypeError, msg):
usa.cities.add(chicago.pk)
with self.assertRaisesMessage(TypeError, msg):
diff --git a/tests/migrations/test_executor.py b/tests/migrations/test_executor.py
index dd6793b533b0..060a35e379c9 100644
--- a/tests/migrations/test_executor.py
+++ b/tests/migrations/test_executor.py
@@ -162,6 +162,7 @@ def test_non_atomic_migration(self):
self.assertTrue(Publisher.objects.exists())
self.assertTableNotExists("migrations_book")
+ @skipUnlessDBFeature("supports_transactions")
@override_settings(
MIGRATION_MODULES={"migrations": "migrations.test_migrations_atomic_operation"}
)
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 90c22bc73bc7..85295d8e5099 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -5809,6 +5809,7 @@ def test_run_python_invalid_reverse_code(self):
with self.assertRaisesMessage(ValueError, msg):
migrations.RunPython(code=migrations.RunPython.noop, reverse_code="invalid")
+ @skipUnlessDBFeature("supports_transactions")
def test_run_python_atomic(self):
"""
Tests the RunPython operation correctly handles the "atomic" keyword
diff --git a/tests/model_forms/test_modelchoicefield.py b/tests/model_forms/test_modelchoicefield.py
index 7b086fb182d0..7f66b5b07867 100644
--- a/tests/model_forms/test_modelchoicefield.py
+++ b/tests/model_forms/test_modelchoicefield.py
@@ -347,11 +347,11 @@ class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
field.widget.render("name", []),
(
"