diff --git a/ChangeLog b/ChangeLog index d057957a5..4f63f145e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,8 +20,6 @@ What's New in astroid 2.12.12? ============================== Release date: TBA - - * Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. Refs PyCQA/pylint#4039 @@ -30,6 +28,12 @@ Release date: TBA Refs PyCQA/pylint#7592 +* Fix inferring attributes with empty annotation assignments if parent + class contains valid assignment. + + Refs PyCQA/pylint#7631 + + What's New in astroid 2.12.11? ============================== Release date: 2022-10-10 diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index f3fc3c100..475ae46b0 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -46,6 +46,7 @@ from astroid.nodes.scoped_nodes.mixin import ComprehensionScope, LocalsDictNodeNG from astroid.nodes.scoped_nodes.utils import builtin_lookup from astroid.nodes.utils import Position +from astroid.typing import InferenceResult if sys.version_info >= (3, 8): from functools import cached_property @@ -2519,7 +2520,12 @@ def instantiate_class(self) -> bases.Instance: pass return bases.Instance(self) - def getattr(self, name, context=None, class_context=True): + def getattr( + self, + name: str, + context: InferenceContext | None = None, + class_context: bool = True, + ) -> list[NodeNG]: """Get an attribute from this class, using Python's attribute semantic. This method doesn't look in the :attr:`instance_attrs` dictionary @@ -2535,13 +2541,10 @@ def getattr(self, name, context=None, class_context=True): metaclass will be done. :param name: The attribute to look for. - :type name: str :param class_context: Whether the attribute can be accessed statically. - :type class_context: bool :returns: The attribute. - :rtype: list(NodeNG) :raises AttributeInferenceError: If the attribute cannot be inferred. """ @@ -2564,17 +2567,16 @@ def getattr(self, name, context=None, class_context=True): if class_context: values += self._metaclass_lookup_attribute(name, context) - if not values: - raise AttributeInferenceError(target=self, attribute=name, context=context) - - # Look for AnnAssigns, which are not attributes in the purest sense. - for value in values: + # Remove AnnAssigns without value, which are not attributes in the purest sense. + for value in values.copy(): if isinstance(value, node_classes.AssignName): stmt = value.statement(future=True) if isinstance(stmt, node_classes.AnnAssign) and stmt.value is None: - raise AttributeInferenceError( - target=self, attribute=name, context=context - ) + values.pop(values.index(value)) + + if not values: + raise AttributeInferenceError(target=self, attribute=name, context=context) + return values def _metaclass_lookup_attribute(self, name, context): @@ -2616,14 +2618,17 @@ def _get_attribute_from_metaclass(self, cls, name, context): else: yield bases.BoundMethod(attr, self) - def igetattr(self, name, context=None, class_context=True): + def igetattr( + self, + name: str, + context: InferenceContext | None = None, + class_context: bool = True, + ) -> Iterator[InferenceResult]: """Infer the possible values of the given variable. :param name: The name of the variable to infer. - :type name: str :returns: The inferred possible values. - :rtype: iterable(NodeNG or Uninferable) """ # set lookup name since this is necessary to infer on import nodes for # instance diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index 9e33de6ec..f3b4288ef 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -1268,6 +1268,22 @@ class Past(Present): self.assertIsInstance(attr1, nodes.AssignName) self.assertEqual(attr1.name, "attr") + @staticmethod + def test_getattr_with_enpty_annassign() -> None: + code = """ + class Parent: + attr: int = 2 + + class Child(Parent): #@ + attr: int + """ + child = extract_node(code) + attr = child.getattr("attr") + assert len(attr) == 1 + assert isinstance(attr[0], nodes.AssignName) + assert attr[0].name == "attr" + assert attr[0].lineno == 3 + def test_function_with_decorator_lineno(self) -> None: data = """ @f(a=2,