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