Package: src:python-django
Version: 3:5.2.17-1
User: [email protected]
Usertags: python3.15
Tags: patch, ftbfs, forky, sid

Hi!

While rebuilding the python related packages against the Python 3.15rc1
version we found that python-django fails to build from source [1].

There are three separate issues related to python 3.15.

First, tests failing with `NameError: name 'SomeType' is not defined.
Did you mean: 'TokenType'?`. This was fixed upstream with Commit e7f539f
[2].

Second, the error message for `test_invalid_choice_db_option` has
changed. To fix this, it required applying a few upstream patches [3]
[4].

Third, test_strip_tags failed with `sys.version_info >=
min_fixed_security[sys.version_info[:2]] KeyError: (3, 15)`, this is
caused by line 136 of tests/utils_tests/test_html.py, which defines
htmlparser_fixed_security but that's never used and it's later recrated
in line 151 in a way that avoids the error, the related upstream fix
about this is in [5], but backporting the fix is actually only deleting
the line 136.

I applied the upstream fixes in the sandbox [5] to be able to build the
packages that depend on python-django, please consider applying the
patch to support the upcoming 3.15 version.

Happy hacking,

[1]: 
https://debusine.debian.net/debian/r-python-python3.15/artifact/4415709/raw/python-django_5.2.17-1+py315.1_amd64-2026-08-22T01:25:30Z.build
[2]: 
https://github.com/django/django/commit/e7f539f813bd56a71ca3c1fbf379f47691002086
[3]: https://github.com/django/django/pull/20021
[4]: 
https://github.com/django/django/commit/ed13a58bf63df94508c8a0fe779da0b6a2bc26bb
[5]: https://github.com/django/django/commit/185b049
[6]: https://debusine.debian.net/debian/r-python-python3.15/

--
"Can you imagine what I would do if I could do all I can?" -- Sun Tzu
Saludos /\/\ /\ >< `/
From: Jacob Walls <[email protected]>
Date: Wed, 6 May 2026 15:39:41 -0400
Subject: [PATCH] Refs #36712, #36664 -- Used annotation_format parameter of
 getfullargspec() on Python 3.15.

https://github.com/python/cpython/pull/149457

Origin: upstream, https://github.com/django/django/commit/e7f539f813bd56a71ca3c1fbf379f47691002086
---
 django/contrib/admin/templatetags/base.py |  9 +++------
 django/template/base.py                   |  5 ++---
 django/template/library.py                | 23 ++++++++++-------------
 django/utils/inspect.py                   | 27 ++++++++++++++++++++++-----
 4 files changed, 37 insertions(+), 27 deletions(-)

Index: python-django/django/contrib/admin/templatetags/base.py
===================================================================
--- python-django.orig/django/contrib/admin/templatetags/base.py
+++ python-django/django/contrib/admin/templatetags/base.py
@@ -1,7 +1,5 @@
-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 +10,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,
Index: python-django/django/template/base.py
===================================================================
--- python-django.orig/django/template/base.py
+++ python-django/django/template/base.py
@@ -58,7 +58,7 @@ from enum import Enum
 from django.template.context import BaseContext
 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
@@ -761,8 +761,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
Index: python-django/django/template/library.py
===================================================================
--- python-django.orig/django/template/library.py
+++ python-django/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
@@ -110,16 +110,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)
@@ -166,16 +165,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:
@@ -250,16 +248,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)
Index: python-django/django/utils/inspect.py
===================================================================
--- python-django.orig/django/utils/inspect.py
+++ python-django/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)
@@ -96,12 +97,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:
@@ -113,6 +114,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
From: kihuni <[email protected]>
Date: Sun, 2 Nov 2025 08:50:45 +0300
Subject: [PATCH] Fixed #36321 -- Defaulted suggest_on_error=True in management
 commands.

Python 3.15 defaults suggest_on_error=True, but the feature is available
from 3.14, so this change opts in earlier. This change can be reverted
when Python 3.15 is the minimum supported version.

Origin: upstream, https://github.com/django/django/commit/b1a65eac7c09250d36e12464fc8fff2a401246b6
---
 django/core/management/base.py |  3 +++
 django/utils/version.py        |  1 +
 tests/admin_scripts/tests.py   |  8 +++++++-
 tests/user_commands/tests.py   | 16 ++++++++++++++++
 4 files changed, 27 insertions(+), 1 deletion(-)

Index: python-django/django/core/management/base.py
===================================================================
--- python-django.orig/django/core/management/base.py
+++ python-django/django/core/management/base.py
@@ -15,7 +15,7 @@ from django.core import checks
 from django.core.exceptions import ImproperlyConfigured
 from django.core.management.color import color_style, no_style
 from django.db import DEFAULT_DB_ALIAS, connections
-from django.utils.version import PY314
+from django.utils.version import PY314, PY315
 
 ALL_CHECKS = "__all__"
 
@@ -58,6 +58,8 @@ class CommandParser(ArgumentParser):
     ):
         self.missing_args_message = missing_args_message
         self.called_from_command_line = called_from_command_line
+        if PY314 and not PY315:
+            kwargs.setdefault("suggest_on_error", True)
         if PY314:
             if os.environ.get("DJANGO_COLORS") == "nocolor" or "--no-color" in sys.argv:
                 kwargs.setdefault("color", False)
Index: python-django/django/utils/version.py
===================================================================
--- python-django.orig/django/utils/version.py
+++ python-django/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):
Index: python-django/tests/admin_scripts/tests.py
===================================================================
--- python-django.orig/tests/admin_scripts/tests.py
+++ python-django/tests/admin_scripts/tests.py
@@ -38,7 +38,7 @@ from django.db.migrations.recorder impor
 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, PY314, get_docs_version
 from django.views.static import serve
 
 from . import urls
@@ -2405,10 +2405,16 @@ 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 PY314:
+            expected_error = (
+                r"Error: argument --database: invalid choice: 'deflaut', "
+                r"maybe you meant 'default'\? \(choose from default, other\)"
+            )
+        else:
+            expected_error = (
+                r"Error: argument --database: invalid choice: 'deflaut' "
+                r"\(choose from '?default'?, '?other'?\)"
+            )
         args = [
             "changepassword",
             "createsuperuser",
From: kasey <[email protected]>
Date: Tue, 12 May 2026 00:15:15 -0500
Subject: [PATCH] Fixed #37096 -- Fixed test_invalid_choice_db_option on Python
 3.14.5+.

Origin: upstream, https://github.com/django/django/commit/ed13a58bf63df94508c8a0fe779da0b6a2bc26bb
---
 tests/admin_scripts/tests.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

Index: python-django/tests/admin_scripts/tests.py
===================================================================
--- python-django.orig/tests/admin_scripts/tests.py
+++ python-django/tests/admin_scripts/tests.py
@@ -2408,7 +2408,8 @@ class CommandDBOptionChoiceTests(SimpleT
         if PY314:
             expected_error = (
                 r"Error: argument --database: invalid choice: 'deflaut', "
-                r"maybe you meant 'default'\? \(choose from default, other\)"
+                r"maybe you meant 'default'\? "
+                r"\(choose from '?default'?, '?other'?\)"
             )
         else:
             expected_error = (
From: Mariusz Felisiak <[email protected]>
Date: Wed, 22 Oct 2025 10:04:38 +0200
Subject: [PATCH] Refs #36499 -- Made TestUtilsHtml.test_strip_tags() assume
 behavior change in X.Y.0 version for Python 3.14+.

This also removes unsupported versions of Python from the test dict.

Origin: upstream, https://github.com/django/django/commit/185b049e9e72a5ff4b07e33605e10eb4f52ca74c
---
 tests/utils_tests/test_html.py | 11 -----------
 1 file changed, 11 deletions(-)

Index: python-django/tests/utils_tests/test_html.py
===================================================================
--- python-django.orig/tests/utils_tests/test_html.py
+++ python-django/tests/utils_tests/test_html.py
@@ -126,16 +126,9 @@ class TestUtilsHtml(SimpleTestCase):
         # Python versions and CI workers include the fix. See:
         # https://github.com/python/cpython/commit/6eb6c5db
         min_fixed_security = {
-            (3, 14): (3, 14),
             (3, 13): (3, 13, 6),
             (3, 12): (3, 12, 12),
-            (3, 11): (3, 11, 14),
-            (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 = {

Reply via email to