Skip to content

Commit 735ea11

Browse files
committed
Infer members of Enums as instances of the Enum they belong to
1 parent 01ca8b6 commit 735ea11

File tree

6 files changed

+117
-121
lines changed

6 files changed

+117
-121
lines changed

ChangeLog

+5
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ Release date: TBA
6767

6868
Closes PyCQA/pylint#5776
6969

70+
* Members of ``Enums`` are now correctly inferred as instances of the ``Enum`` they belong
71+
to instead of instances of their own class.
72+
73+
Closes #744
74+
7075
* Rename ``ModuleSpec`` -> ``module_type`` constructor parameter to match attribute
7176
name and improve typing. Use ``type`` instead.
7277

astroid/bases.py

+18
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,28 @@ class BaseInstance(Proxy):
200200

201201
special_attributes = None
202202

203+
_proxied: nodes.ClassDef
204+
205+
def __init__(self, proxied: nodes.ClassDef | None = None) -> None:
206+
self._explicit_instance_attrs: dict[str, list[nodes.NodeNG]] = {}
207+
"""Attributes that have been explicitly set during initialization
208+
of the specific instance.
209+
210+
This dictionary can be used to differentiate between attributes assosciated to
211+
the proxy and attributes that are specific to the instantiated instance.
212+
"""
213+
super().__init__(proxied)
214+
203215
def display_type(self):
204216
return "Instance of"
205217

206218
def getattr(self, name, context=None, lookupclass=True):
219+
# See if the attribute is set explicitly for this instance
220+
try:
221+
return self._explicit_instance_attrs[name]
222+
except KeyError:
223+
pass
224+
207225
try:
208226
values = self._proxied.instance_attr(name, context)
209227
except AttributeInferenceError as exc:

astroid/brain/brain_namedtuple_enum.py

+44-95
Original file line numberDiff line numberDiff line change
@@ -357,106 +357,55 @@ def __mul__(self, other):
357357

358358
def infer_enum_class(node: nodes.ClassDef) -> nodes.ClassDef:
359359
"""Specific inference for enums."""
360-
for basename in (b for cls in node.mro() for b in cls.basenames):
361-
if node.root().name == "enum":
362-
# Skip if the class is directly from enum module.
363-
break
364-
dunder_members = {}
365-
target_names = set()
366-
for local, values in node.locals.items():
367-
if any(not isinstance(value, nodes.AssignName) for value in values):
368-
continue
360+
if node.root().name == "enum":
361+
# Skip if the class is directly from enum module.
362+
return node
363+
dunder_members: dict[str, bases.Instance] = {}
364+
for local, values in node.locals.items():
365+
if any(not isinstance(value, nodes.AssignName) for value in values):
366+
continue
369367

370-
stmt = values[0].statement(future=True)
371-
if isinstance(stmt, nodes.Assign):
372-
if isinstance(stmt.targets[0], nodes.Tuple):
373-
targets = stmt.targets[0].itered()
374-
else:
375-
targets = stmt.targets
376-
elif isinstance(stmt, nodes.AnnAssign):
377-
targets = [stmt.target]
368+
stmt = values[0].statement(future=True)
369+
if isinstance(stmt, nodes.Assign):
370+
if isinstance(stmt.targets[0], nodes.Tuple):
371+
targets: list[nodes.NodeNG] = stmt.targets[0].itered()
378372
else:
373+
targets = stmt.targets
374+
value_node = stmt.value
375+
elif isinstance(stmt, nodes.AnnAssign):
376+
targets = [stmt.target] # type: ignore[list-item] # .target shouldn't be None
377+
value_node = stmt.value
378+
else:
379+
continue
380+
381+
new_targets: list[bases.Instance] = []
382+
for target in targets:
383+
if isinstance(target, nodes.Starred):
379384
continue
380385

381-
inferred_return_value = None
382-
if isinstance(stmt, nodes.Assign):
383-
if isinstance(stmt.value, nodes.Const):
384-
if isinstance(stmt.value.value, str):
385-
inferred_return_value = repr(stmt.value.value)
386-
else:
387-
inferred_return_value = stmt.value.value
388-
else:
389-
inferred_return_value = stmt.value.as_string()
390-
391-
new_targets = []
392-
for target in targets:
393-
if isinstance(target, nodes.Starred):
394-
continue
395-
target_names.add(target.name)
396-
# Replace all the assignments with our mocked class.
397-
classdef = dedent(
398-
"""
399-
class {name}({types}):
400-
@property
401-
def value(self):
402-
return {return_value}
403-
@property
404-
def name(self):
405-
return "{name}"
406-
""".format(
407-
name=target.name,
408-
types=", ".join(node.basenames),
409-
return_value=inferred_return_value,
410-
)
411-
)
412-
if "IntFlag" in basename:
413-
# Alright, we need to add some additional methods.
414-
# Unfortunately we still can't infer the resulting objects as
415-
# Enum members, but once we'll be able to do that, the following
416-
# should result in some nice symbolic execution
417-
classdef += INT_FLAG_ADDITION_METHODS.format(name=target.name)
418-
419-
fake = AstroidBuilder(
420-
AstroidManager(), apply_transforms=False
421-
).string_build(classdef)[target.name]
422-
fake.parent = target.parent
423-
for method in node.mymethods():
424-
fake.locals[method.name] = [method]
425-
new_targets.append(fake.instantiate_class())
426-
dunder_members[local] = fake
427-
node.locals[local] = new_targets
428-
members = nodes.Dict(parent=node)
429-
members.postinit(
430-
[
431-
(nodes.Const(k, parent=members), nodes.Name(v.name, parent=members))
432-
for k, v in dunder_members.items()
386+
# Instantiate a class of the Enum with the value and name
387+
# attributes set to the values of the assignment
388+
# See: https://docs.python.org/3/library/enum.html#creating-an-enum
389+
target_node = node.instantiate_class()
390+
target_node._explicit_instance_attrs["value"] = [value_node]
391+
target_node._explicit_instance_attrs["name"] = [
392+
nodes.const_factory(target.name)
433393
]
434-
)
435-
node.locals["__members__"] = [members]
436-
# The enum.Enum class itself defines two @DynamicClassAttribute data-descriptors
437-
# "name" and "value" (which we override in the mocked class for each enum member
438-
# above). When dealing with inference of an arbitrary instance of the enum
439-
# class, e.g. in a method defined in the class body like:
440-
# class SomeEnum(enum.Enum):
441-
# def method(self):
442-
# self.name # <- here
443-
# In the absence of an enum member called "name" or "value", these attributes
444-
# should resolve to the descriptor on that particular instance, i.e. enum member.
445-
# For "value", we have no idea what that should be, but for "name", we at least
446-
# know that it should be a string, so infer that as a guess.
447-
if "name" not in target_names:
448-
code = dedent(
449-
"""
450-
@property
451-
def name(self):
452-
return ''
453-
"""
454-
)
455-
name_dynamicclassattr = AstroidBuilder(AstroidManager()).string_build(code)[
456-
"name"
457-
]
458-
node.locals["name"] = [name_dynamicclassattr]
459-
break
394+
395+
new_targets.append(target_node)
396+
dunder_members[local] = target_node
397+
398+
node.locals[local] = new_targets
399+
400+
# Creation of the __members__ attribute of the Enum node
401+
members = nodes.Dict(parent=node)
402+
members.postinit(
403+
[
404+
(nodes.Const(k, parent=members), nodes.Name(v.name, parent=members))
405+
for k, v in dunder_members.items()
406+
]
407+
)
408+
node.locals["__members__"] = [members]
460409
return node
461410

462411

astroid/nodes/node_classes.py

+3
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ def __init__(
287287
parent=parent,
288288
)
289289

290+
Instance.__init__(self, self._proxied)
291+
290292
def postinit(self, elts: list[NodeNG]) -> None:
291293
"""Do some setup after initialisation.
292294
@@ -2269,6 +2271,7 @@ def __init__(
22692271
end_col_offset=end_col_offset,
22702272
parent=parent,
22712273
)
2274+
Instance.__init__(self, self._proxied)
22722275

22732276
def postinit(
22742277
self, items: list[tuple[SuccessfulInferenceResult, SuccessfulInferenceResult]]

tests/test_brain_ssl.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,8 @@ def test_ssl_brain() -> None:
4040
# TLSVersion is inferred from the main module, not from the brain
4141
inferred_cert_required = next(module.body[4].value.infer())
4242
assert isinstance(inferred_cert_required, bases.Instance)
43-
assert inferred_cert_required._proxied.name == "CERT_REQUIRED"
43+
assert inferred_cert_required._proxied.name == "VerifyMode"
44+
45+
value_node = inferred_cert_required.getattr("value")[0]
46+
assert isinstance(value_node, nodes.Const)
47+
assert value_node.value == 2

tests/unittest_brain.py

+42-25
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import astroid
1919
from astroid import MANAGER, bases, builder, nodes, objects, test_utils, util
2020
from astroid.bases import Instance
21-
from astroid.const import PY39_PLUS
21+
from astroid.const import PY39_PLUS, PY311_PLUS
2222
from astroid.exceptions import (
2323
AttributeInferenceError,
2424
InferenceError,
@@ -771,11 +771,17 @@ def mymethod(self, x):
771771

772772
enumeration = next(module["MyEnum"].infer())
773773
one = enumeration["one"]
774-
self.assertEqual(one.pytype(), ".MyEnum.one")
774+
self.assertEqual(one.pytype(), ".MyEnum")
775775

776776
for propname in ("name", "value"):
777-
prop = next(iter(one.getattr(propname)))
778-
self.assertIn("builtins.property", prop.decoratornames())
777+
# On the base Enum class 'name' and 'value' are properties
778+
# decorated by DynamicClassAttribute in < 3.11.
779+
prop = next(iter(one._proxied.getattr(propname)))
780+
if PY311_PLUS:
781+
expected_name = "enum.property"
782+
else:
783+
expected_name = "types.DynamicClassAttribute"
784+
self.assertIn(expected_name, prop.decoratornames())
779785

780786
meth = one.getattr("mymethod")[0]
781787
self.assertIsInstance(meth, astroid.FunctionDef)
@@ -1030,18 +1036,17 @@ def func(self):
10301036
"""
10311037
i_name, i_value, c_name, c_value = astroid.extract_node(code)
10321038

1033-
# <instance>.name should be a string, <class>.name should be a property (that
1039+
# <instance>.name should be Uninferable, <class>.name should be a property (that
10341040
# forwards the lookup to __getattr__)
10351041
inferred = next(i_name.infer())
1036-
assert isinstance(inferred, nodes.Const)
1037-
assert inferred.pytype() == "builtins.str"
1042+
assert inferred is util.Uninferable
10381043
inferred = next(c_name.infer())
10391044
assert isinstance(inferred, objects.Property)
10401045

1041-
# Inferring .value should not raise InferenceError. It is probably Uninferable
1042-
# but we don't particularly care
1043-
next(i_value.infer())
1044-
next(c_value.infer())
1046+
inferred = next(i_value.infer())
1047+
assert inferred is util.Uninferable
1048+
inferred = next(c_value.infer())
1049+
assert isinstance(inferred, objects.Property)
10451050

10461051
def test_enum_name_and_value_members_override_dynamicclassattr(self) -> None:
10471052
code = """
@@ -1058,19 +1063,23 @@ def func(self):
10581063
"""
10591064
i_name, i_value, c_name, c_value = astroid.extract_node(code)
10601065

1061-
# All of these cases should be inferred as enum members
1062-
inferred = next(i_name.infer())
1063-
assert isinstance(inferred, bases.Instance)
1064-
assert inferred.pytype() == ".TrickyEnum.name"
1065-
inferred = next(c_name.infer())
1066-
assert isinstance(inferred, bases.Instance)
1067-
assert inferred.pytype() == ".TrickyEnum.name"
1068-
inferred = next(i_value.infer())
1069-
assert isinstance(inferred, bases.Instance)
1070-
assert inferred.pytype() == ".TrickyEnum.value"
1071-
inferred = next(c_value.infer())
1072-
assert isinstance(inferred, bases.Instance)
1073-
assert inferred.pytype() == ".TrickyEnum.value"
1066+
# All of these cases should be inferred as enum instances
1067+
# and refer to the same instance
1068+
name_inner = next(i_name.infer())
1069+
assert isinstance(name_inner, bases.Instance)
1070+
assert name_inner.pytype() == ".TrickyEnum"
1071+
name_outer = next(c_name.infer())
1072+
assert isinstance(name_outer, bases.Instance)
1073+
assert name_outer.pytype() == ".TrickyEnum"
1074+
assert name_inner == name_outer
1075+
1076+
value_inner = next(i_value.infer())
1077+
assert isinstance(value_inner, bases.Instance)
1078+
assert value_inner.pytype() == ".TrickyEnum"
1079+
value_outer = next(c_value.infer())
1080+
assert isinstance(value_outer, bases.Instance)
1081+
assert value_outer.pytype() == ".TrickyEnum"
1082+
assert value_inner == value_outer
10741083

10751084
def test_enum_subclass_member_name(self) -> None:
10761085
ast_node = astroid.extract_node(
@@ -1188,7 +1197,15 @@ class MyEnum(PyEnum):
11881197
)
11891198
inferred = next(ast_node.infer())
11901199
assert isinstance(inferred, bases.Instance)
1191-
assert inferred._proxied.name == "ENUM_KEY"
1200+
assert inferred._proxied.name == "MyEnum"
1201+
1202+
name_node = inferred.getattr("name")[0]
1203+
assert isinstance(name_node, nodes.Const)
1204+
assert name_node.value == "ENUM_KEY"
1205+
1206+
value_node = inferred.getattr("value")[0]
1207+
assert isinstance(value_node, nodes.Const)
1208+
assert value_node.value == "enum_value"
11921209

11931210

11941211
@unittest.skipUnless(HAS_DATEUTIL, "This test requires the dateutil library.")

0 commit comments

Comments
 (0)