Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-astroid for openSUSE:Factory checked in at 2026-01-05 14:50:15 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-astroid (Old) and /work/SRC/openSUSE:Factory/.python-astroid.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-astroid" Mon Jan 5 14:50:15 2026 rev:63 rq:1325245 version:4.0.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-astroid/python-astroid.changes 2025-11-18 15:29:17.048716491 +0100 +++ /work/SRC/openSUSE:Factory/.python-astroid.new.1928/python-astroid.changes 2026-01-05 14:50:28.951587996 +0100 @@ -1,0 +2,10 @@ +Sun Jan 4 08:45:22 UTC 2026 - Dirk Müller <[email protected]> + +- update to 4.0.3: + * Fix inference of ``IfExp`` (ternary expression) nodes to + avoid prematurely narrowing results in the face of inference + ambiguity. + * Fix base class inference for dataclasses using the PEP 695 + typing syntax. + +------------------------------------------------------------------- Old: ---- astroid-4.0.2-gh.tar.gz New: ---- astroid-4.0.3-gh.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-astroid.spec ++++++ --- /var/tmp/diff_new_pack.PcYttd/_old 2026-01-05 14:50:32.999756596 +0100 +++ /var/tmp/diff_new_pack.PcYttd/_new 2026-01-05 14:50:33.031757929 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-astroid # -# Copyright (c) 2025 SUSE LLC and contributors +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-astroid -Version: 4.0.2 +Version: 4.0.3 Release: 0 Summary: Representation of Python source as an AST for pylint License: LGPL-2.1-or-later ++++++ astroid-4.0.2-gh.tar.gz -> astroid-4.0.3-gh.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/.github/workflows/ci.yaml new/astroid-4.0.3/.github/workflows/ci.yaml --- old/astroid-4.0.2/.github/workflows/ci.yaml 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/.github/workflows/ci.yaml 2026-01-03 23:11:10.000000000 +0100 @@ -156,7 +156,7 @@ run: >- echo "key=${{ env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml', 'requirements_dev.txt', - 'requirements_full.txt', 'requirements_minimal.txt') }}" >> $GITHUB_OUTPUT + 'requirements_full.txt', 'requirements_minimal.txt') }}" >> $env:GITHUB_OUTPUT - *cache-python - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/ChangeLog new/astroid-4.0.3/ChangeLog --- old/astroid-4.0.2/ChangeLog 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/ChangeLog 2026-01-03 23:11:10.000000000 +0100 @@ -9,12 +9,26 @@ -What's New in astroid 4.0.3? +What's New in astroid 4.0.4? ============================ Release date: TBA +What's New in astroid 4.0.3? +============================ +Release date: 2026-01-03 + +* Fix inference of ``IfExp`` (ternary expression) nodes to avoid prematurely narrowing + results in the face of inference ambiguity. + + Closes #2899 + +* Fix base class inference for dataclasses using the PEP 695 typing syntax. + + Refs pylint-dev/pylint#10788 + + What's New in astroid 4.0.2? ============================ Release date: 2025-11-09 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/astroid/__pkginfo__.py new/astroid-4.0.3/astroid/__pkginfo__.py --- old/astroid-4.0.2/astroid/__pkginfo__.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/astroid/__pkginfo__.py 2026-01-03 23:11:10.000000000 +0100 @@ -2,5 +2,5 @@ # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "4.0.2" +__version__ = "4.0.3" version = __version__ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/astroid/brain/brain_dataclasses.py new/astroid-4.0.3/astroid/brain/brain_dataclasses.py --- old/astroid-4.0.2/astroid/brain/brain_dataclasses.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/astroid/brain/brain_dataclasses.py 2026-01-03 23:11:10.000000000 +0100 @@ -54,7 +54,7 @@ ) -def dataclass_transform(node: nodes.ClassDef) -> None: +def dataclass_transform(node: nodes.ClassDef) -> nodes.ClassDef | None: """Rewrite a dataclass to be easily understood by pylint.""" node.is_dataclass = True @@ -70,7 +70,7 @@ node.instance_attrs[name] = [rhs_node] if not _check_generate_dataclass_init(node): - return + return None kw_only_decorated = False if node.decorators.nodes: @@ -102,6 +102,7 @@ new_assign = parse(f"{DEFAULT_FACTORY} = object()").body[0] new_assign.parent = root root.locals[DEFAULT_FACTORY] = [new_assign.targets[0]] + return node def _get_dataclass_attributes( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/astroid/nodes/node_classes.py new/astroid-4.0.3/astroid/nodes/node_classes.py --- old/astroid-4.0.2/astroid/nodes/node_classes.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/astroid/nodes/node_classes.py 2026-01-03 23:11:10.000000000 +0100 @@ -1022,7 +1022,7 @@ @decorators.raise_if_nothing_inferred def _infer( - self: nodes.Arguments, context: InferenceContext | None = None, **kwargs: Any + self, context: InferenceContext | None = None, **kwargs: Any ) -> Generator[InferenceResult]: # pylint: disable-next=import-outside-toplevel from astroid.protocols import _arguments_infer_argname @@ -1441,7 +1441,7 @@ @decorators.raise_if_nothing_inferred @decorators.path_wrapper def _infer( - self: nodes.AugAssign, context: InferenceContext | None = None, **kwargs: Any + self, context: InferenceContext | None = None, **kwargs: Any ) -> Generator[InferenceResult]: return self._filter_operation_errors( self._infer_augassign, context, util.BadBinaryOperationMessage @@ -1556,7 +1556,7 @@ @decorators.yes_if_nothing_inferred @decorators.path_wrapper def _infer( - self: nodes.BinOp, context: InferenceContext | None = None, **kwargs: Any + self, context: InferenceContext | None = None, **kwargs: Any ) -> Generator[InferenceResult]: return self._filter_operation_errors( self._infer_binop, context, util.BadBinaryOperationMessage @@ -1633,7 +1633,7 @@ @decorators.raise_if_nothing_inferred @decorators.path_wrapper def _infer( - self: nodes.BoolOp, context: InferenceContext | None = None, **kwargs: Any + self, context: InferenceContext | None = None, **kwargs: Any ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]: """Infer a boolean operation (and / or / not). @@ -3108,31 +3108,37 @@ to inferring both branches. Otherwise, we infer either branch depending on the condition. """ - both_branches = False + # We use two separate contexts for evaluating lhs and rhs because # evaluating lhs may leave some undesired entries in context.path # which may not let us infer right value of rhs. - context = context or InferenceContext() lhs_context = copy_context(context) rhs_context = copy_context(context) + + # Infer bool condition. Stop inferring if in doubt and fallback to + # evaluating both branches. + condition: bool | None = None try: - test = next(self.test.infer(context=context.clone())) - except (InferenceError, StopIteration): - both_branches = True - else: - test_bool_value = test.bool_value() - if not isinstance(test, util.UninferableBase) and not isinstance( - test_bool_value, util.UninferableBase - ): - if test_bool_value: - yield from self.body.infer(context=lhs_context) - else: - yield from self.orelse.infer(context=rhs_context) - else: - both_branches = True - if both_branches: + for test in self.test.infer(context=context.clone()): + if isinstance(test, util.UninferableBase): + condition = None + break + test_bool_value = test.bool_value() + if isinstance(test_bool_value, util.UninferableBase): + condition = None + break + if condition is None: + condition = test_bool_value + elif test_bool_value != condition: + condition = None + break + except InferenceError: + condition = None + + if condition is True or condition is None: yield from self.body.infer(context=lhs_context) + if condition is False or condition is None: yield from self.orelse.infer(context=rhs_context) @@ -4318,7 +4324,7 @@ return super().op_precedence() def _infer_unaryop( - self: nodes.UnaryOp, context: InferenceContext | None = None, **kwargs: Any + self, context: InferenceContext | None = None, **kwargs: Any ) -> Generator[ InferenceResult | util.BadUnaryOperationMessage, None, InferenceErrorInfo ]: @@ -4384,7 +4390,7 @@ @decorators.raise_if_nothing_inferred @decorators.path_wrapper def _infer( - self: nodes.UnaryOp, context: InferenceContext | None = None, **kwargs: Any + self, context: InferenceContext | None = None, **kwargs: Any ) -> Generator[InferenceResult, None, InferenceErrorInfo]: """Infer what an UnaryOp should return when evaluated.""" yield from self._filter_operation_errors( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/astroid/nodes/scoped_nodes/mixin.py new/astroid-4.0.3/astroid/nodes/scoped_nodes/mixin.py --- old/astroid-4.0.2/astroid/nodes/scoped_nodes/mixin.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/astroid/nodes/scoped_nodes/mixin.py 2026-01-03 23:11:10.000000000 +0100 @@ -6,7 +6,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, TypeVar, overload +import sys +from typing import TYPE_CHECKING, overload from astroid.exceptions import ParentMissingError from astroid.filter_statements import _filter_stmts @@ -14,11 +15,13 @@ from astroid.nodes.scoped_nodes.utils import builtin_lookup from astroid.typing import InferenceResult, SuccessfulInferenceResult +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self if TYPE_CHECKING: from astroid import nodes -_T = TypeVar("_T") - class LocalsDictNodeNG(_base_nodes.LookupMixIn): """this class provides locals handling common to Module, FunctionDef @@ -46,7 +49,7 @@ except ParentMissingError: return self.name - def scope(self: _T) -> _T: + def scope(self) -> Self: """The first parent node defining a new scope. :returns: The first parent scope node. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/astroid/nodes/scoped_nodes/scoped_nodes.py new/astroid-4.0.3/astroid/nodes/scoped_nodes/scoped_nodes.py --- old/astroid-4.0.2/astroid/nodes/scoped_nodes/scoped_nodes.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/astroid/nodes/scoped_nodes/scoped_nodes.py 2026-01-03 23:11:10.000000000 +0100 @@ -13,9 +13,10 @@ import io import itertools import os +import sys from collections.abc import Generator, Iterable, Iterator, Sequence from functools import cached_property, lru_cache -from typing import TYPE_CHECKING, Any, ClassVar, Literal, NoReturn, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, Literal, NoReturn from astroid import bases, protocols, util from astroid.context import ( @@ -50,6 +51,11 @@ SuccessfulInferenceResult, ) +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + if TYPE_CHECKING: from astroid import nodes, objects from astroid.nodes import Arguments, Const, NodeNG @@ -62,8 +68,6 @@ {"classmethod", "staticmethod", "builtins.classmethod", "builtins.staticmethod"} ) -_T = TypeVar("_T") - def _c3_merge(sequences, cls, context): """Merges MROs in *sequences* to a single MRO using the C3 algorithm. @@ -587,7 +591,7 @@ def get_children(self): yield from self.body - def frame(self: _T, *, future: Literal[None, True] = None) -> _T: + def frame(self, *, future: Literal[None, True] = None) -> Self: """The node's frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, @@ -1030,7 +1034,7 @@ yield self.args yield self.body - def frame(self: _T, *, future: Literal[None, True] = None) -> _T: + def frame(self, *, future: Literal[None, True] = None) -> Self: """The node's frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, @@ -1677,7 +1681,7 @@ frame = self return frame._scope_lookup(node, name, offset) - def frame(self: _T, *, future: Literal[None, True] = None) -> _T: + def frame(self, *, future: Literal[None, True] = None) -> Self: """The node's frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, @@ -2884,7 +2888,7 @@ ) return list(itertools.chain.from_iterable(children_assign_nodes)) - def frame(self: _T, *, future: Literal[None, True] = None) -> _T: + def frame(self, *, future: Literal[None, True] = None) -> Self: """The node's frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/astroid/objects.py new/astroid-4.0.3/astroid/objects.py --- old/astroid-4.0.2/astroid/objects.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/astroid/objects.py 2026-01-03 23:11:10.000000000 +0100 @@ -13,9 +13,10 @@ from __future__ import annotations +import sys from collections.abc import Generator, Iterator from functools import cached_property -from typing import Any, Literal, NoReturn, TypeVar +from typing import Any, Literal, NoReturn from astroid import bases, util from astroid.context import InferenceContext @@ -30,7 +31,10 @@ from astroid.nodes import node_classes, scoped_nodes from astroid.typing import InferenceResult, SuccessfulInferenceResult -_T = TypeVar("_T") +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self class FrozenSet(node_classes.BaseContainer): @@ -355,6 +359,6 @@ raise InferenceError("Properties are not callable") def _infer( - self: _T, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[_T]: + self, context: InferenceContext | None = None, **kwargs: Any + ) -> Generator[Self]: yield self diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/requirements_minimal.txt new/astroid-4.0.3/requirements_minimal.txt --- old/astroid-4.0.2/requirements_minimal.txt 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/requirements_minimal.txt 2026-01-03 23:11:10.000000000 +0100 @@ -6,4 +6,4 @@ coverage~=7.10 pytest pytest-cov~=7.0 -mypy +mypy; platform_python_implementation!="PyPy" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/tbump.toml new/astroid-4.0.3/tbump.toml --- old/astroid-4.0.2/tbump.toml 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/tbump.toml 2026-01-03 23:11:10.000000000 +0100 @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/astroid" [version] -current = "4.0.2" +current = "4.0.3" regex = ''' ^(?P<major>0|[1-9]\d*) \. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/tests/test_inference.py new/astroid-4.0.3/tests/test_inference.py --- old/astroid-4.0.2/tests/test_inference.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/tests/test_inference.py 2026-01-03 23:11:10.000000000 +0100 @@ -6442,6 +6442,98 @@ assert [third[0].value, third[1].value] == [1, 2] +def test_ifexp_with_default_arguments() -> None: + code = """ + def with_default(foo: str | None = None): + a = 1 if foo else "bar" #@ + + def without_default(foo: str): + a = 1 if foo else "bar" #@ + + def some_ifexps(foo: str | None = None): + a = 1 if foo else 2 + b = 3 if a else 4 #@ + c = 4 if b else 5 #@ + d = 5 if not foo else foo #@ + e = d if not foo else foo #@ + """ + + ast_nodes = extract_node(code) + + first = ast_nodes[0].value.inferred() + second = ast_nodes[1].value.inferred() + third = ast_nodes[2].value.inferred() + fourth = ast_nodes[3].value.inferred() + fifth = ast_nodes[4].value.inferred() + sixth = ast_nodes[5].value.inferred() + + assert len(first) == 2 + assert [first[0].value, first[1].value] == [1, "bar"] + + assert len(second) == 2 + assert [second[0].value, second[1].value] == [1, "bar"] + + assert len(third) == 1 + assert third[0].value == 3 + + assert len(fourth) == 1 + assert fourth[0].value == 4 + + assert len(fifth) == 2 + assert [fifth[0].value, fifth[1].value] == [5, Uninferable] + + assert len(sixth) == 3 + assert [sixth[0].value, sixth[1].value, sixth[2].value] == [ + 5, + Uninferable, + Uninferable, + ] + + +def test_ifexp_with_uninferables() -> None: + code = """ + def truthy_and_falsy(): + return False if unknown() else True + + def truthy_and_uninferable(): + return False if unknown() else unknown() + + def calls_truthy_and_falsy(): + return 1 if truthy_and_falsy() else 2 + + def calls_truthy_and_uninferable(): + return 1 if range(10) else truthy_and_uninferable() + + truthy_and_falsy() #@ + truthy_and_uninferable() #@ + calls_truthy_and_falsy() #@ + calls_truthy_and_uninferable() #@ + """ + + ast_nodes = extract_node(code) + + first = ast_nodes[0].inferred() + second = ast_nodes[1].inferred() + third = ast_nodes[2].inferred() + fourth = ast_nodes[3].inferred() + + assert len(first) == 2 + assert [first[0].value, first[1].value] == [False, True] + + assert len(second) == 2 + assert [second[0].value, second[1].value] == [False, Uninferable] + + assert len(third) == 2 + assert [third[0].value, third[1].value] == [1, 2] + + assert len(fourth) == 3 + assert [fourth[0].value, fourth[1].value, fourth[2].value] == [ + 1, + False, + Uninferable, + ] + + def test_assert_last_function_returns_none_on_inference() -> None: code = """ def check_equal(a, b): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/tests/test_raw_building.py new/astroid-4.0.3/tests/test_raw_building.py --- old/astroid-4.0.2/tests/test_raw_building.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/tests/test_raw_building.py 2026-01-03 23:11:10.000000000 +0100 @@ -19,7 +19,6 @@ from typing import Any from unittest import mock -import mypy.build import pytest import tests.testdata.python3.data.fake_module_with_broken_getattr as fm_getattr @@ -37,6 +36,13 @@ object_build_class, ) +try: + import mypy.build + + HAS_MYPY = True +except ImportError: + HAS_MYPY = False + DUMMY_MOD = build_module("DUMMY") @@ -173,6 +179,7 @@ assert not err [email protected](not HAS_MYPY, reason="This test requires mypy") def test_missing__dict__(): # This shouldn't raise an exception. object_build_class(DUMMY_MOD, mypy.build.ModuleNotFound) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/astroid-4.0.2/tests/test_scoped_nodes.py new/astroid-4.0.3/tests/test_scoped_nodes.py --- old/astroid-4.0.2/tests/test_scoped_nodes.py 2025-11-09 22:19:11.000000000 +0100 +++ new/astroid-4.0.3/tests/test_scoped_nodes.py 2026-01-03 23:11:10.000000000 +0100 @@ -29,7 +29,7 @@ util, ) from astroid.bases import BoundMethod, Generator, Instance, UnboundMethod -from astroid.const import WIN32 +from astroid.const import PY312_PLUS, WIN32 from astroid.exceptions import ( AstroidBuildingError, AttributeInferenceError, @@ -1967,6 +1967,34 @@ cls, [".E", ".C", ".A", ".B", "typing.Generic", ".D", "builtins.object"] ) + @pytest.mark.skipif(not PY312_PLUS, reason="PEP 695 syntax requires Python 3.12") + def test_mro_generic_8(self): + cls = builder.extract_node( + """ + class A: ... + class B[T]: ... + class C[T](A, B[T]): ... + """ + ) + assert isinstance(cls, nodes.ClassDef) + self.assertEqualMroQName(cls, [".C", ".A", ".B", "builtins.object"]) + + @pytest.mark.skipif(not PY312_PLUS, reason="PEP 695 syntax requires Python 3.12") + def test_mro_generic_9(self): + cls = builder.extract_node( + """ + from dataclasses import dataclass + @dataclass + class A: ... + @dataclass + class B[T]: ... + @dataclass + class C[T](A, B[T]): ... + """ + ) + assert isinstance(cls, nodes.ClassDef) + self.assertEqualMroQName(cls, [".C", ".A", ".B", "builtins.object"]) + def test_mro_generic_error_1(self): cls = builder.extract_node( """
