python-django: FTBFS against python 3.15rc1

Maximiliano Curia <[email protected]>
Newsgroups gmane.linux.debian.devel.python
Message-ID <[email protected]>
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 /\/\ /\ >< `/
0007-Used-annotation_format-parameter-of-getfullargspec.patch (text/x-diff, 7.8 KB)
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
0008-Defaulted-suggest_on_error-True-in-management.patch (text/x-diff, 3.5 KB)
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",
0009-Fixed-test_invalid_choice_db_option-on-py314.patch (text/x-diff, 1008 B)
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 = (
(unnamed) (text/x-diff, 1.4 KB)
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 = {
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.