astroid: FTBFS against python 3.15rc1

Maximiliano Curia <[email protected]>
Newsgroups gmane.linux.debian.devel.python
Message-ID <[email protected]>
Package: src:astroid
Version: 4.1.1-2
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 astroid fails to build from source [1]. 

The problems were already fixed upstream through two separate commits
[2] and [3].

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

Happy hacking,

[1]: https://debusine.debian.net/debian/r-python-python3.15/work-request/1090897/
[2]: https://github.com/pylint-dev/astroid/commit/341ef0a19c8f68e4b89c9ccafc535a66a59da0a4
[3]: https://github.com/pylint-dev/astroid/commit/3c9d1d9e78bb0096c8c55df96e6abcfb19e6aa32
[4]: 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 /\/\ /\ >< `/
python-3.15-compatibility.patch (text/x-diff, 6.1 KB)
commit 341ef0a19c8f68e4b89c9ccafc535a66a59da0a4
Author: Copilot <[email protected]>
Date:   Thu Apr 23 08:14:41 2026 +0200

    Add Python 3.15 compatibility: CI, ByteString stub, and test fixes (#3033)
    
    * Add initial support for Python 3.15
    
    * Fix for `test_typing_object_notsubscriptable_3`
    
    ---------
    
    Co-authored-by: Daniël van Noord <[email protected]>

Index: astroid/astroid/brain/brain_typing.py
===================================================================
--- astroid.orig/astroid/brain/brain_typing.py
+++ astroid/astroid/brain/brain_typing.py
@@ -15,7 +15,7 @@ from typing import Final
 from astroid import context, nodes
 from astroid.brain.helpers import register_module_extender
 from astroid.builder import AstroidBuilder, _extract_single_node, extract_node
-from astroid.const import PY312_PLUS, PY313_PLUS, PY314_PLUS
+from astroid.const import PY312_PLUS, PY313_PLUS, PY314_PLUS, PY315_PLUS
 from astroid.exceptions import (
     AstroidSyntaxError,
     AttributeInferenceError,
@@ -464,6 +464,13 @@ def _typing_transform():
         @classmethod
         def __class_getitem__(cls, item): return cls
     """)
+    if PY315_PLUS:
+        # typing.ByteString was removed from the typing module in Python 3.15
+        # (it was deprecated since 3.12 and present at module level until 3.14).
+        # Inject a stub so code using `typing.ByteString` can still be inferred.
+        code += textwrap.dedent("""
+    class ByteString: ...
+    """)
     return AstroidBuilder(AstroidManager()).string_build(code)
 
 
Index: astroid/astroid/const.py
===================================================================
--- astroid.orig/astroid/const.py
+++ astroid/astroid/const.py
@@ -10,6 +10,7 @@ PY312_PLUS = sys.version_info >= (3, 12)
 PY313 = sys.version_info[:2] == (3, 13)
 PY313_PLUS = sys.version_info >= (3, 13)
 PY314_PLUS = sys.version_info >= (3, 14)
+PY315_PLUS = sys.version_info >= (3, 15)
 
 WIN32 = sys.platform == "win32"
 
Index: astroid/astroid/protocols.py
===================================================================
--- astroid.orig/astroid/protocols.py
+++ astroid/astroid/protocols.py
@@ -545,7 +545,7 @@ ExceptionGroup
 """)))
         assigned = objects.ExceptionInstance(eg)
         assigned.instance_attrs["exceptions"] = [
-            nodes.List.from_elements(_generate_assigned())
+            nodes.Tuple.from_elements(_generate_assigned())
         ]
         yield assigned
     else:
Index: astroid/tests/brain/test_brain.py
===================================================================
--- astroid.orig/tests/brain/test_brain.py
+++ astroid/tests/brain/test_brain.py
@@ -15,7 +15,7 @@ import astroid
 from astroid import MANAGER, builder, nodes, objects, test_utils, util
 from astroid.bases import Instance
 from astroid.brain.brain_namedtuple_enum import _get_namedtuple_fields
-from astroid.const import PY312_PLUS, PY313_PLUS
+from astroid.const import PY312_PLUS, PY313_PLUS, PY315_PLUS
 from astroid.exceptions import (
     AttributeInferenceError,
     InferenceError,
@@ -787,7 +787,10 @@ class TypingBrain(unittest.TestCase):
         typing.ByteString
         """)
         inferred = next(right_node.infer())
-        check_metaclass_is_abc(inferred)
+        # From Python 3.15 we add a stub definition of `ByteString`. It doesn't need all properties
+        # of the original implementation.
+        if not PY315_PLUS:
+            check_metaclass_is_abc(inferred)
         with self.assertRaises(AttributeInferenceError):
             self.assertIsInstance(
                 inferred.getattr("__class_getitem__")[0], nodes.FunctionDef
Index: astroid/tests/test_group_exceptions.py
===================================================================
--- astroid.orig/tests/test_group_exceptions.py
+++ astroid/tests/test_group_exceptions.py
@@ -126,12 +126,12 @@ def test_star_exceptions_infer_exception
     assert isinstance(node, nodes.TryStar)
     inferred_ve = next(node.handlers[0].statement().name.infer())
     assert inferred_ve.name == "ExceptionGroup"
-    assert isinstance(inferred_ve.getattr("exceptions")[0], nodes.List)
+    assert isinstance(inferred_ve.getattr("exceptions")[0], nodes.Tuple)
     assert (
         inferred_ve.getattr("exceptions")[0].elts[0].pytype() == "builtins.ValueError"
     )
 
     inferred_te = next(node.handlers[1].statement().name.infer())
     assert inferred_te.name == "ExceptionGroup"
-    assert isinstance(inferred_te.getattr("exceptions")[0], nodes.List)
+    assert isinstance(inferred_te.getattr("exceptions")[0], nodes.Tuple)
     assert inferred_te.getattr("exceptions")[0].elts[0].pytype() == "builtins.TypeError"
Index: astroid/tests/test_regrtest.py
===================================================================
--- astroid.orig/tests/test_regrtest.py
+++ astroid/tests/test_regrtest.py
@@ -12,7 +12,7 @@ import pytest
 
 from astroid import MANAGER, Instance, bases, manager, nodes, parse, test_utils
 from astroid.builder import AstroidBuilder, _extract_single_node, extract_node
-from astroid.const import PY312_PLUS
+from astroid.const import PY312_PLUS, PY315_PLUS
 from astroid.context import InferenceContext
 from astroid.exceptions import AstroidSyntaxError, InferenceError
 from astroid.manager import AstroidManager
@@ -530,6 +530,9 @@ def test_regression_infer_namedtuple_inv
     assert inferred.value == Uninferable
 
 
+# On Python 3.15+ the parser emits a regular SyntaxError instead of a MemoryError for deeply nested
+# parentheses, so the special-case test here is no longer needed.
[email protected](PY315_PLUS, reason="No longer a MemoryError on Python 3.15+")
 def test_regression_parse_deeply_nested_parentheses() -> None:
     """Regression test for issue #2643."""
     with pytest.raises(AstroidSyntaxError, match="Parsing Python code failed:") as ctx:
Index: astroid/tox.ini
===================================================================
--- astroid.orig/tox.ini
+++ astroid/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py{39,310,311,312,313,314}
+envlist = py{39,310,311,312,313,314,315}
 skip_missing_interpreters = true
 isolated_build = true
fix_handle_py315_kw_only_and_namespace_pth.patch (text/x-diff, 9 KB)
commit 3c9d1d9e78bb0096c8c55df96e6abcfb19e6aa32
Author: Sai Asish Y <[email protected]>
Date:   Sat May 16 05:29:53 2026 -0700

    fix: handle Python 3.15 KW_ONLY and namespace .pth test behavior (#3047)
    
    
    Signed-off-by: SAY-5 <[email protected]>
    Co-authored-by: SAY-5 <[email protected]>
    Co-authored-by: Sai Asish Y <[email protected]>
    Co-authored-by: Pierre Sassoulas <[email protected]>

Index: astroid/astroid/brain/brain_dataclasses.py
===================================================================
--- astroid.orig/astroid/brain/brain_dataclasses.py
+++ astroid/astroid/brain/brain_dataclasses.py
@@ -556,10 +556,27 @@ def _get_field_default(field_call: nodes
 def _is_keyword_only_sentinel(node: nodes.NodeNG) -> bool:
     """Return True if node is the KW_ONLY sentinel."""
     inferred = safe_infer(node)
-    return (
-        isinstance(inferred, bases.Instance)
-        and inferred.qname() == "dataclasses._KW_ONLY_TYPE"
-    )
+    if not isinstance(inferred, bases.Instance):
+        return False
+    if inferred.qname() == "dataclasses._KW_ONLY_TYPE":
+        return True
+    if inferred.qname() != "builtins.sentinel":
+        return False
+    if isinstance(node, nodes.Name):
+        _, assignments = node.lookup(node.name)
+        return any(
+            isinstance(assignment, nodes.ImportFrom)
+            and assignment.modname == "dataclasses"
+            and any(imported == "KW_ONLY" for imported, _ in assignment.names)
+            for assignment in assignments
+        )
+    if isinstance(node, nodes.Attribute) and node.attrname == "KW_ONLY":
+        inferred_expr = safe_infer(node.expr)
+        return (
+            isinstance(inferred_expr, nodes.Module)
+            and inferred_expr.qname() == "dataclasses"
+        )
+    return False
 
 
 def _is_init_var(node: nodes.NodeNG) -> bool:
Index: astroid/tests/brain/test_dataclasses.py
===================================================================
--- astroid.orig/tests/brain/test_dataclasses.py
+++ astroid/tests/brain/test_dataclasses.py
@@ -728,7 +728,8 @@ def test_non_dataclass_is_not_dataclass(
 
 def test_kw_only_sentinel() -> None:
     """Test that the KW_ONLY sentinel doesn't get added to the fields."""
-    node_one, node_two = astroid.extract_node("""
+    node_one, node_two, node_three = astroid.extract_node("""
+    import dataclasses
     from dataclasses import dataclass, KW_ONLY
     from dataclasses import KW_ONLY as keyword_only
 
@@ -745,13 +746,37 @@ def test_kw_only_sentinel() -> None:
         y: str
 
     B.__init__  #@
+
+    @dataclass
+    class C:
+        _: dataclasses.KW_ONLY
+        y: str
+
+    C.__init__  #@
     """)
     expected = ["self", "y"]
-    init = next(node_one.infer())
-    assert [a.name for a in init.args.args] == expected
+    for node in (node_one, node_two, node_three):
+        init = next(node.infer())
+        assert [a.name for a in init.args.args] == expected
+
 
-    init = next(node_two.infer())
-    assert [a.name for a in init.args.args] == expected
+def test_kw_only_sentinel_other_dataclasses_attr() -> None:
+    """Annotating with another ``dataclasses`` attribute (e.g. ``MISSING``)
+    that also infers to ``builtins.sentinel`` on Python 3.15+ must not be
+    treated as ``KW_ONLY``."""
+    node = astroid.extract_node("""
+    import dataclasses
+    from dataclasses import dataclass
+
+    @dataclass
+    class C:
+        _: dataclasses.MISSING
+        y: str
+
+    C.__init__  #@
+    """)
+    init = next(node.infer())
+    assert [a.name for a in init.args.args] == ["self", "_", "y"]
 
 
 def test_kw_only_decorator() -> None:
Index: astroid/tests/test_manager.py
===================================================================
--- astroid.orig/tests/test_manager.py
+++ astroid/tests/test_manager.py
@@ -3,9 +3,10 @@
 # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
 
 import os
-import site
+import re
 import sys
 import time
+import types
 import unittest
 import warnings
 from collections.abc import Iterator
@@ -36,6 +37,51 @@ def _get_file_from_object(obj) -> str:
     return obj.__file__
 
 
+_NSPKG_PTH_PARTS_RE = re.compile(r"\*\(([^)]+)\)")
+
+
+def _parse_pth_package_parts(line: str) -> tuple[str, ...]:
+    """Extract the namespace package tuple from a setuptools nspkg .pth line."""
+    match = _NSPKG_PTH_PARTS_RE.search(line)
+    if not match:
+        return ()
+    parts = []
+    for token in match.group(1).split(","):
+        token = token.strip().strip("'\"")
+        if token:
+            parts.append(token)
+    return tuple(parts)
+
+
+def _load_namespace_package_pth(pth: str) -> None:
+    """Apply a setuptools-style namespace package .pth fixture without exec().
+
+    Each non-comment line in the fixture wires up one namespace package by
+    appending a directory under `resources.RESOURCE_PATH` to that package's
+    ``__path__``. We parse the package tuple out of the line and replay the
+    same effect here.
+    """
+    sitedir = str(resources.RESOURCE_PATH)
+    with (resources.RESOURCE_PATH / pth).open(encoding="utf-8") as pth_file:
+        for raw_line in pth_file:
+            line = raw_line.strip()
+            if not line or line.startswith("#"):
+                continue
+            parts = _parse_pth_package_parts(line)
+            if not parts:
+                continue
+            package_name = ".".join(parts)
+            package_path = os.path.join(sitedir, *parts)
+            if os.path.exists(os.path.join(package_path, "__init__.py")):
+                continue
+            module = sys.modules.setdefault(
+                package_name, types.ModuleType(package_name)
+            )
+            mod_path = module.__dict__.setdefault("__path__", [])
+            if package_path not in mod_path:
+                mod_path.append(package_path)
+
+
 class AstroidManagerTest(resources.SysPathSetup, unittest.TestCase):
     def setUp(self) -> None:
         super().setUp()
@@ -196,7 +242,7 @@ class AstroidManagerTest(resources.SysPa
     )
     def test_namespace_package_pth_support(self) -> None:
         pth = "foogle_fax-0.12.5-py2.7-nspkg.pth"
-        site.addpackage(resources.RESOURCE_PATH, pth, [])
+        _load_namespace_package_pth(pth)
 
         try:
             module = self.manager.ast_from_module_name("foogle.fax")
@@ -206,7 +252,8 @@ class AstroidManagerTest(resources.SysPa
             with self.assertRaises(AstroidImportError):
                 self.manager.ast_from_module_name("foogle.moogle")
         finally:
-            sys.modules.pop("foogle")
+            sys.modules.pop("foogle", None)
+            sys.modules.pop("foogle.crank", None)
 
     @pytest.mark.skipif(
         IS_PYPY,
@@ -214,23 +261,25 @@ class AstroidManagerTest(resources.SysPa
     )
     def test_nested_namespace_import(self) -> None:
         pth = "foogle_fax-0.12.5-py2.7-nspkg.pth"
-        site.addpackage(resources.RESOURCE_PATH, pth, [])
+        _load_namespace_package_pth(pth)
         try:
             self.manager.ast_from_module_name("foogle.crank")
         finally:
-            sys.modules.pop("foogle")
+            sys.modules.pop("foogle", None)
+            sys.modules.pop("foogle.crank", None)
 
     def test_namespace_and_file_mismatch(self) -> None:
         filepath = unittest.__file__
         ast = self.manager.ast_from_file(filepath)
         self.assertEqual(ast.name, "unittest")
         pth = "foogle_fax-0.12.5-py2.7-nspkg.pth"
-        site.addpackage(resources.RESOURCE_PATH, pth, [])
+        _load_namespace_package_pth(pth)
         try:
             with self.assertRaises(AstroidImportError):
                 self.manager.ast_from_module_name("unittest.foogle.fax")
         finally:
-            sys.modules.pop("foogle")
+            sys.modules.pop("foogle", None)
+            sys.modules.pop("foogle.crank", None)
 
     def _test_ast_from_zip(self, archive: str) -> None:
         sys.modules.pop("mypypa", None)
@@ -553,3 +602,23 @@ class ClearCacheTest(unittest.TestCase):
         isinstance_call = astroid.extract_node("isinstance(1, int)")
         inferred = next(isinstance_call.infer())
         self.assertIs(inferred.value, True)
+
+
+class NamespacePthParserTest(unittest.TestCase):
+    """Direct coverage for the .pth parsing helpers used by namespace tests."""
+
+    def test_parse_extracts_quoted_tuple(self) -> None:
+        line = "import sys; p = os.path.join(s, *('foogle', 'crank'))"
+        self.assertEqual(_parse_pth_package_parts(line), ("foogle", "crank"))
+
+    def test_parse_handles_double_quotes_and_whitespace(self) -> None:
+        line = '*(  "foo" ,  "bar" )'
+        self.assertEqual(_parse_pth_package_parts(line), ("foo", "bar"))
+
+    def test_parse_skips_empty_tokens(self) -> None:
+        line = "*('foo', '', 'bar',)"
+        self.assertEqual(_parse_pth_package_parts(line), ("foo", "bar"))
+
+    def test_parse_returns_empty_when_no_match(self) -> None:
+        self.assertEqual(_parse_pth_package_parts("# comment only"), ())
+        self.assertEqual(_parse_pth_package_parts(""), ())
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.