Skip to content

Commit

Permalink
Inferring property fields in a class context when metaclass is present (
Browse files Browse the repository at this point in the history
#941)

* Add tests of failing property inference

Ref #940

* Fix inference of properties in a class context

Ref #940. If we are accessing an attribute and it is found on
type(A).__dict__ *and* it is a data descriptor, then we resolve that
descriptor. For the case of inferring a property which is accessed as a
class attribute, this equates to the property being defined on the
metaclass (which may be anywhere in the class hierarchy).

* Fix inference of Enum.__members__ property for subclasses

Ref pylint-dev/pylint#3535. Ref pylint-dev/pylint#4358. This updates the
namedtuple/enum brain to add a dictionary for __members__
  • Loading branch information
nelfin committed May 15, 2021
1 parent 962becc commit 8181dc9
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 4 deletions.
9 changes: 9 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ Release Date: TBA

Closes #898

* Fix property inference in class contexts for properties defined on the metaclass

Closes #940

* Update enum brain to fix definition of __members__ for subclass-defined Enums

Closes PyCQA/pylint#3535
Closes PyCQA/pylint#4358


What's New in astroid 2.5.6?
============================
Expand Down
10 changes: 10 additions & 0 deletions astroid/brain/brain_namedtuple_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ def infer_enum_class(node):
if node.root().name == "enum":
# Skip if the class is directly from enum module.
break
dunder_members = {}
for local, values in node.locals.items():
if any(not isinstance(value, nodes.AssignName) for value in values):
continue
Expand Down Expand Up @@ -372,7 +373,16 @@ def name(self):
for method in node.mymethods():
fake.locals[method.name] = [method]
new_targets.append(fake.instantiate_class())
dunder_members[local] = fake
node.locals[local] = new_targets
members = nodes.Dict(parent=node)
members.postinit(
[
(nodes.Const(k, parent=members), nodes.Name(v.name, parent=members))
for k, v in dunder_members.items()
]
)
node.locals["__members__"] = [members]
break
return node

Expand Down
11 changes: 7 additions & 4 deletions astroid/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2554,7 +2554,7 @@ def igetattr(self, name, context=None, class_context=True):
context = contextmod.copy_context(context)
context.lookupname = name

metaclass = self.declared_metaclass(context=context)
metaclass = self.metaclass(context=context)
try:
attributes = self.getattr(name, context, class_context=class_context)
# If we have more than one attribute, make sure that those starting from
Expand Down Expand Up @@ -2587,9 +2587,12 @@ def igetattr(self, name, context=None, class_context=True):
yield from function.infer_call_result(
caller=self, context=context
)
# If we have a metaclass, we're accessing this attribute through
# the class itself, which means we can solve the property
elif metaclass:
# If we're in a class context, we need to determine if the property
# was defined in the metaclass (a derived class must be a subclass of
# the metaclass of all its bases), in which case we can resolve the
# property. If not, i.e. the property is defined in some base class
# instead, then we return the property object
elif metaclass and function.parent.scope() is metaclass:
# Resolve a property as long as it is not accessed through
# the class itself.
yield from function.infer_call_result(
Expand Down
147 changes: 147 additions & 0 deletions tests/unittest_scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1923,6 +1923,153 @@ def update(self):
builder.parse(data)


def test_issue940_metaclass_subclass_property():
node = builder.extract_node(
"""
class BaseMeta(type):
@property
def __members__(cls):
return ['a', 'property']
class Parent(metaclass=BaseMeta):
pass
class Derived(Parent):
pass
Derived.__members__
"""
)
inferred = next(node.infer())
assert isinstance(inferred, nodes.List)
assert [c.value for c in inferred.elts] == ["a", "property"]


def test_issue940_property_grandchild():
node = builder.extract_node(
"""
class Grandparent:
@property
def __members__(self):
return ['a', 'property']
class Parent(Grandparent):
pass
class Child(Parent):
pass
Child().__members__
"""
)
inferred = next(node.infer())
assert isinstance(inferred, nodes.List)
assert [c.value for c in inferred.elts] == ["a", "property"]


def test_issue940_metaclass_property():
node = builder.extract_node(
"""
class BaseMeta(type):
@property
def __members__(cls):
return ['a', 'property']
class Parent(metaclass=BaseMeta):
pass
Parent.__members__
"""
)
inferred = next(node.infer())
assert isinstance(inferred, nodes.List)
assert [c.value for c in inferred.elts] == ["a", "property"]


def test_issue940_with_metaclass_class_context_property():
node = builder.extract_node(
"""
class BaseMeta(type):
pass
class Parent(metaclass=BaseMeta):
@property
def __members__(self):
return ['a', 'property']
class Derived(Parent):
pass
Derived.__members__
"""
)
inferred = next(node.infer())
assert not isinstance(inferred, nodes.List)
assert isinstance(inferred, objects.Property)


def test_issue940_metaclass_values_funcdef():
node = builder.extract_node(
"""
class BaseMeta(type):
def __members__(cls):
return ['a', 'func']
class Parent(metaclass=BaseMeta):
pass
Parent.__members__()
"""
)
inferred = next(node.infer())
assert isinstance(inferred, nodes.List)
assert [c.value for c in inferred.elts] == ["a", "func"]


def test_issue940_metaclass_derived_funcdef():
node = builder.extract_node(
"""
class BaseMeta(type):
def __members__(cls):
return ['a', 'func']
class Parent(metaclass=BaseMeta):
pass
class Derived(Parent):
pass
Derived.__members__()
"""
)
inferred_result = next(node.infer())
assert isinstance(inferred_result, nodes.List)
assert [c.value for c in inferred_result.elts] == ["a", "func"]


def test_issue940_metaclass_funcdef_is_not_datadescriptor():
node = builder.extract_node(
"""
class BaseMeta(type):
def __members__(cls):
return ['a', 'property']
class Parent(metaclass=BaseMeta):
@property
def __members__(cls):
return BaseMeta.__members__()
class Derived(Parent):
pass
Derived.__members__
"""
)
# Here the function is defined on the metaclass, but the property
# is defined on the base class. When loading the attribute in a
# class context, this should return the property object instead of
# resolving the data descriptor
inferred = next(node.infer())
assert isinstance(inferred, objects.Property)


def test_issue940_enums_as_a_real_world_usecase():
node = builder.extract_node(
"""
from enum import Enum
class Sounds(Enum):
bee = "buzz"
cat = "meow"
Sounds.__members__
"""
)
inferred_result = next(node.infer())
assert isinstance(inferred_result, nodes.Dict)
actual = [k.value for k, _ in inferred_result.items]
assert sorted(actual) == ["bee", "cat"]


def test_metaclass_cannot_infer_call_yields_an_instance():
node = builder.extract_node(
"""
Expand Down

0 comments on commit 8181dc9

Please sign in to comment.