Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix getattr inference with empty annotation assignments #1834

Merged
merged 1 commit into from Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog
Expand Up @@ -33,6 +33,11 @@ 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?
==============================
Expand Down
35 changes: 20 additions & 15 deletions astroid/nodes/scoped_nodes/scoped_nodes.py
Expand Up @@ -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
Expand Down Expand Up @@ -2525,7 +2526,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]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure about this? I belive locals is actually SuccesfulInferenceResult but typed incorrectly currently...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I belive locals is actually SuccesfulInferenceResult but typed incorrectly currently...

Don't think so. AFAICT locals is only modified via set_local which takes a NodeNG argument. Thus the typing should be correct. We can always come back to it if we discover that it's wrong later.
https://github.com/PyCQA/astroid/blob/a29ee0f3fbbe3aa580280c979d3f01b0146dd00f/astroid/nodes/scoped_nodes/mixin.py#L91-L103

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment in other issue. This is not always the case and I think the typing should be updated here.

"""Get an attribute from this class, using Python's attribute semantic.

This method doesn't look in the :attr:`instance_attrs` dictionary
Expand All @@ -2541,13 +2547,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.
"""
Expand All @@ -2570,17 +2573,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):
Expand Down Expand Up @@ -2622,14 +2624,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
Expand Down
16 changes: 16 additions & 0 deletions tests/unittest_scoped_nodes.py
Expand Up @@ -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,
Expand Down