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 /\/\ /\ >< `/
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
 
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(""), ())

Reply via email to