Skip to content

Inferring property fields in a class context when metaclass is present #941

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

Merged
merged 7 commits into from
May 15, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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