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