diff --git a/AUTHORS b/AUTHORS index 7ec645c8eb..53486d8f83 100644 --- a/AUTHORS +++ b/AUTHORS @@ -593,6 +593,7 @@ answer newbie questions, and generally made Django that much better: Karderio Karen Tracey Karol Sikora + Kasey Steinhauer Kasun Herath Katherine “Kati” Michel Kathryn Killebrew diff --git a/django/contrib/admin/templatetags/base.py b/django/contrib/admin/templatetags/base.py index 3f8290d3b1..94b2011c3c 100644 --- a/django/contrib/admin/templatetags/base.py +++ b/django/contrib/admin/templatetags/base.py @@ -1,7 +1,7 @@ from inspect import getfullargspec from django.template.library import InclusionNode, parse_bits -from django.utils.inspect import lazy_annotations +from django.utils.inspect import getfullargspec class InclusionAdminNode(InclusionNode): @@ -12,10 +12,9 @@ class InclusionAdminNode(InclusionNode): def __init__(self, parser, token, func, template_name, takes_context=True): self.template_name = template_name - with lazy_annotations(): - params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = ( - getfullargspec(func) - ) + params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec( + func + ) bits = token.split_contents() args, kwargs = parse_bits( parser, diff --git a/django/template/base.py b/django/template/base.py index 9d75111e42..526a5f9ecf 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -60,7 +60,7 @@ from django.template.context import BaseContext 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, signature +from django.utils.inspect import getfullargspec, 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 @@ -826,8 +826,7 @@ class FilterExpression: # Check to see if a decorator is providing the real function. func = inspect.unwrap(func) - with lazy_annotations(): - args, _, _, defaults, _, _, _ = inspect.getfullargspec(func) + args, _, _, defaults, _, _, _ = getfullargspec(func) alen = len(args) dlen = len(defaults or []) # Not enough OR Too many diff --git a/django/template/library.py b/django/template/library.py index 27af7a6969..a645717935 100644 --- a/django/template/library.py +++ b/django/template/library.py @@ -1,10 +1,10 @@ from collections.abc import Iterable from functools import wraps from importlib import import_module -from inspect import getfullargspec, unwrap +from inspect import unwrap from django.utils.html import conditional_escape -from django.utils.inspect import lazy_annotations +from django.utils.inspect import getfullargspec from .base import Node, Template, token_kwargs from .exceptions import TemplateSyntaxError @@ -111,16 +111,15 @@ class Library: """ def dec(func): - with lazy_annotations(): - ( - params, - varargs, - varkw, - defaults, - kwonly, - kwonly_defaults, - _, - ) = getfullargspec(unwrap(func)) + ( + params, + varargs, + varkw, + defaults, + kwonly, + kwonly_defaults, + _, + ) = getfullargspec(unwrap(func)) function_name = name or func.__name__ @wraps(func) @@ -167,16 +166,15 @@ class Library: def dec(func): nonlocal end_name - with lazy_annotations(): - ( - params, - varargs, - varkw, - defaults, - kwonly, - kwonly_defaults, - _, - ) = getfullargspec(unwrap(func)) + ( + params, + varargs, + varkw, + defaults, + kwonly, + kwonly_defaults, + _, + ) = getfullargspec(unwrap(func)) function_name = name or func.__name__ if end_name is None: @@ -251,16 +249,15 @@ class Library: """ def dec(func): - with lazy_annotations(): - ( - params, - varargs, - varkw, - defaults, - kwonly, - kwonly_defaults, - _, - ) = getfullargspec(unwrap(func)) + ( + params, + varargs, + varkw, + defaults, + kwonly, + kwonly_defaults, + _, + ) = getfullargspec(unwrap(func)) function_name = name or func.__name__ @wraps(func) diff --git a/django/utils/inspect.py b/django/utils/inspect.py index a04669fc11..1b4d5733fd 100644 --- a/django/utils/inspect.py +++ b/django/utils/inspect.py @@ -3,16 +3,17 @@ import inspect import threading from contextlib import contextmanager -from django.utils.version import PY314 +from django.utils.version import PY314, PY315 if PY314: import annotationlib - lock = threading.Lock() - safe_signature_from_callable = functools.partial( - inspect._signature_from_callable, - annotation_format=annotationlib.Format.FORWARDREF, - ) + if not PY315: + lock = threading.Lock() + safe_signature_from_callable = functools.partial( + inspect._signature_from_callable, + annotation_format=annotationlib.Format.FORWARDREF, + ) @functools.lru_cache(maxsize=512) @@ -106,12 +107,12 @@ def lazy_annotations(): compatibility with Python 3.14+ deferred evaluation, patch the module-level helper to provide the annotation_format that we are using elsewhere. - This private helper could be removed when there is an upstream solution for - https://github.com/python/cpython/issues/141560. + This private helper should only be used for Python 3.14, as + https://github.com/python/cpython/issues/141560 was fixed in 3.15. This context manager is not reentrant. """ - if not PY314: + if PY315 or not PY314: yield return with lock: @@ -123,6 +124,22 @@ def lazy_annotations(): inspect._signature_from_callable = original_helper +def getfullargspec(*args, annotation_format=None, **kwargs): + """ + A wrapper around inspect.getfullargspec that leaves deferred annotations + unevaluated on Python 3.14+, since they are not used in our case. + """ + if PY315: + return inspect.getfullargspec( + *args, **kwargs, annotation_format=annotationlib.Format.FORWARDREF + ) + if PY314: + with lazy_annotations(): + return inspect.getfullargspec(*args, **kwargs) + else: + return inspect.getfullargspec(*args, **kwargs) + + def signature(obj): """ A wrapper around inspect.signature that leaves deferred annotations diff --git a/django/utils/version.py b/django/utils/version.py index 2bb650ac89..9f694070c5 100644 --- a/django/utils/version.py +++ b/django/utils/version.py @@ -20,6 +20,7 @@ PY311 = sys.version_info >= (3, 11) PY312 = sys.version_info >= (3, 12) PY313 = sys.version_info >= (3, 13) PY314 = sys.version_info >= (3, 14) +PY315 = sys.version_info >= (3, 15) def get_version(version=None): diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 0c27194568..9dc8f16013 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -38,7 +38,7 @@ from django.db.migrations.recorder import MigrationRecorder from django.test import LiveServerTestCase, SimpleTestCase, TestCase, override_settings from django.test.utils import captured_stderr, captured_stdout from django.urls import path -from django.utils.version import PY313, get_docs_version +from django.utils.version import PY313, PY315, get_docs_version from django.views.static import serve from . import urls @@ -2446,10 +2446,17 @@ class Discovery(SimpleTestCase): class CommandDBOptionChoiceTests(SimpleTestCase): def test_invalid_choice_db_option(self): - expected_error = ( - r"Error: argument --database: invalid choice: 'deflaut' " - r"\(choose from '?default'?, '?other'?\)" - ) + if PY315: + expected_error = ( + r"Error: argument --database: invalid choice: 'deflaut', " + r"maybe you meant 'default'\? " + r"\(choose from '?default'?, '?other'?\)" + ) + else: + expected_error = ( + r"Error: argument --database: invalid choice: 'deflaut' " + r"\(choose from '?default'?, '?other'?\)" + ) args = [ "changepassword", "createsuperuser", diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index 7e1a90cc93..91561a8de8 100644 --- a/tests/utils_tests/test_html.py +++ b/tests/utils_tests/test_html.py @@ -131,9 +131,6 @@ class TestUtilsHtml(SimpleTestCase): (3, 10): (3, 10, 19), (3, 9): (3, 9, 24), } - htmlparser_fixed_security = ( - sys.version_info >= min_fixed_security[sys.version_info[:2]] - ) # Similarly, there was a fix for terminating incomplete entities. See: # https://github.com/python/cpython/commit/95296a9d min_fixed_incomplete_entities = {