Skip to content

Commit 70cf55f

Browse files
committed
Infer members of Enums as instances of the Enum they belong to
1 parent 386dc01 commit 70cf55f

File tree

6 files changed

+114
-122
lines changed

6 files changed

+114
-122
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ Release date: TBA
4444

4545
Closes PyCQA/pylint#5776
4646

47+
* Members of ``Enums`` are now correctly inferred as instances of the ``Enum`` they belong
48+
to instead of instances of their own class.
49+
50+
Closes #744
51+
4752
* Rename ``ModuleSpec`` -> ``module_type`` constructor parameter to match attribute
4853
name and improve typing. Use ``type`` instead.
4954

astroid/bases.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,26 @@ class BaseInstance(Proxy):
181181

182182
special_attributes = None
183183

184+
def __init__(self, proxied=None):
185+
self._explicit_instance_attrs: dict[str, list[nodes.NodeNG]] = {}
186+
"""Attributes that have been explicitly set during initialization
187+
of the specific instance.
188+
189+
This dictionary can be used to differentiate between attributes assosciated to
190+
the proxy and attributes that are specific to the instantiated instance.
191+
"""
192+
super().__init__(proxied)
193+
184194
def display_type(self):
185195
return "Instance of"
186196

187197
def getattr(self, name, context=None, lookupclass=True):
198+
# See if the attribute is set explicitly for this instance
199+
try:
200+
return self._explicit_instance_attrs[name]
201+
except KeyError:
202+
pass
203+
188204
try:
189205
values = self._proxied.instance_attr(name, context)
190206
except AttributeInferenceError as exc:

astroid/brain/brain_namedtuple_enum.py

Lines changed: 46 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from textwrap import dedent
1313

1414
import astroid
15-
from astroid import arguments, inference_tip, nodes, util
15+
from astroid import arguments, bases, inference_tip, nodes, util
1616
from astroid.builder import AstroidBuilder, extract_node
1717
from astroid.context import InferenceContext
1818
from astroid.exceptions import (
@@ -351,106 +351,55 @@ def __mul__(self, other):
351351

352352
def infer_enum_class(node: nodes.ClassDef) -> nodes.ClassDef:
353353
"""Specific inference for enums."""
354-
for basename in (b for cls in node.mro() for b in cls.basenames):
355-
if node.root().name == "enum":
356-
# Skip if the class is directly from enum module.
357-
break
358-
dunder_members = {}
359-
target_names = set()
360-
for local, values in node.locals.items():
361-
if any(not isinstance(value, nodes.AssignName) for value in values):
362-
continue
363-
364-
stmt = values[0].statement(future=True)
365-
if isinstance(stmt, nodes.Assign):
366-
if isinstance(stmt.targets[0], nodes.Tuple):
367-
targets = stmt.targets[0].itered()
368-
else:
369-
targets = stmt.targets
370-
elif isinstance(stmt, nodes.AnnAssign):
371-
targets = [stmt.target]
354+
if node.root().name == "enum":
355+
# Skip if the class is directly from enum module.
356+
return node
357+
dunder_members: dict[str, bases.Instance] = {}
358+
for local, values in node.locals.items():
359+
if any(not isinstance(value, nodes.AssignName) for value in values):
360+
continue
361+
362+
stmt = values[0].statement(future=True)
363+
if isinstance(stmt, nodes.Assign):
364+
if isinstance(stmt.targets[0], nodes.Tuple):
365+
targets: list[nodes.NodeNG] = stmt.targets[0].itered()
372366
else:
367+
targets = stmt.targets
368+
value_node = stmt.value
369+
elif isinstance(stmt, nodes.AnnAssign):
370+
targets = [stmt.target] # type: ignore[list-item] # .target shouldn't be None
371+
value_node = stmt.value
372+
else:
373+
continue
374+
375+
new_targets: list[bases.Instance] = []
376+
for target in targets:
377+
if isinstance(target, nodes.Starred):
373378
continue
374379

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

456405

astroid/nodes/node_classes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ def __init__(
304304
parent=parent,
305305
)
306306

307+
Instance.__init__(self)
308+
307309
def postinit(self, elts: list[NodeNG]) -> None:
308310
"""Do some setup after initialisation.
309311
@@ -1928,6 +1930,8 @@ def __init__(
19281930
parent=parent,
19291931
)
19301932

1933+
Instance.__init__(self)
1934+
19311935
def __getattr__(self, name):
19321936
# This is needed because of Proxy's __getattr__ method.
19331937
# Calling object.__new__ on this class without calling
@@ -2277,6 +2281,7 @@ def __init__(
22772281
end_col_offset=end_col_offset,
22782282
parent=parent,
22792283
)
2284+
Instance.__init__(self)
22802285

22812286
def postinit(self, items: list[tuple[NodeNG, NodeNG]]) -> None:
22822287
"""Do some setup after initialisation.

tests/test_brain_ssl.py

Lines changed: 5 additions & 1 deletion
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

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -751,11 +751,13 @@ def mymethod(self, x):
751751

752752
enumeration = next(module["MyEnum"].infer())
753753
one = enumeration["one"]
754-
self.assertEqual(one.pytype(), ".MyEnum.one")
754+
self.assertEqual(one.pytype(), ".MyEnum")
755755

756756
for propname in ("name", "value"):
757-
prop = next(iter(one.getattr(propname)))
758-
self.assertIn("builtins.property", prop.decoratornames())
757+
# On the base Enum class 'name' and 'value' are properties
758+
# decorated by DynamicClassAttribute.
759+
prop = next(iter(one._proxied.getattr(propname)))
760+
self.assertIn("types.DynamicClassAttribute", prop.decoratornames())
759761

760762
meth = one.getattr("mymethod")[0]
761763
self.assertIsInstance(meth, astroid.FunctionDef)
@@ -1010,18 +1012,17 @@ def func(self):
10101012
"""
10111013
i_name, i_value, c_name, c_value = astroid.extract_node(code)
10121014

1013-
# <instance>.name should be a string, <class>.name should be a property (that
1015+
# <instance>.name should be Uninferable, <class>.name should be a property (that
10141016
# forwards the lookup to __getattr__)
10151017
inferred = next(i_name.infer())
1016-
assert isinstance(inferred, nodes.Const)
1017-
assert inferred.pytype() == "builtins.str"
1018+
assert inferred is util.Uninferable
10181019
inferred = next(c_name.infer())
10191020
assert isinstance(inferred, objects.Property)
10201021

1021-
# Inferring .value should not raise InferenceError. It is probably Uninferable
1022-
# but we don't particularly care
1023-
next(i_value.infer())
1024-
next(c_value.infer())
1022+
inferred = next(i_value.infer())
1023+
assert inferred is util.Uninferable
1024+
inferred = next(c_value.infer())
1025+
assert isinstance(inferred, objects.Property)
10251026

10261027
def test_enum_name_and_value_members_override_dynamicclassattr(self) -> None:
10271028
code = """
@@ -1038,19 +1039,23 @@ def func(self):
10381039
"""
10391040
i_name, i_value, c_name, c_value = astroid.extract_node(code)
10401041

1041-
# All of these cases should be inferred as enum members
1042-
inferred = next(i_name.infer())
1043-
assert isinstance(inferred, bases.Instance)
1044-
assert inferred.pytype() == ".TrickyEnum.name"
1045-
inferred = next(c_name.infer())
1046-
assert isinstance(inferred, bases.Instance)
1047-
assert inferred.pytype() == ".TrickyEnum.name"
1048-
inferred = next(i_value.infer())
1049-
assert isinstance(inferred, bases.Instance)
1050-
assert inferred.pytype() == ".TrickyEnum.value"
1051-
inferred = next(c_value.infer())
1052-
assert isinstance(inferred, bases.Instance)
1053-
assert inferred.pytype() == ".TrickyEnum.value"
1042+
# All of these cases should be inferred as enum instances
1043+
# and refer to the same instance
1044+
name_inner = next(i_name.infer())
1045+
assert isinstance(name_inner, bases.Instance)
1046+
assert name_inner.pytype() == ".TrickyEnum"
1047+
name_outer = next(c_name.infer())
1048+
assert isinstance(name_outer, bases.Instance)
1049+
assert name_outer.pytype() == ".TrickyEnum"
1050+
assert name_inner == name_outer
1051+
1052+
value_inner = next(i_value.infer())
1053+
assert isinstance(value_inner, bases.Instance)
1054+
assert value_inner.pytype() == ".TrickyEnum"
1055+
value_outer = next(c_value.infer())
1056+
assert isinstance(value_outer, bases.Instance)
1057+
assert value_outer.pytype() == ".TrickyEnum"
1058+
assert value_inner == value_outer
10541059

10551060
def test_enum_subclass_member_name(self) -> None:
10561061
ast_node = astroid.extract_node(
@@ -1168,7 +1173,15 @@ class MyEnum(PyEnum):
11681173
)
11691174
inferred = next(ast_node.infer())
11701175
assert isinstance(inferred, bases.Instance)
1171-
assert inferred._proxied.name == "ENUM_KEY"
1176+
assert inferred._proxied.name == "MyEnum"
1177+
1178+
name_node = inferred.getattr("name")[0]
1179+
assert isinstance(name_node, nodes.Const)
1180+
assert name_node.value == "ENUM_KEY"
1181+
1182+
value_node = inferred.getattr("value")[0]
1183+
assert isinstance(value_node, nodes.Const)
1184+
assert value_node.value == "enum_value"
11721185

11731186

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

0 commit comments

Comments
 (0)