Skip to content

Commit

Permalink
Improve inference of Enum members called "name" and "value" (#1020)
Browse files Browse the repository at this point in the history
* Add DynamicClassAttribute to list of properties

DynamicClassAttribute is a descriptor defined in Python's Lib/types.py
which changes the behaviour of an attribute depending on if it is looked
up on the class or on an instance.

* Add fake "name" property to enum.Enum subclasses

Ref pylint-dev/pylint#1932. Ref pylint-dev/pylint#2062. The enum.Enum class itself
defines two @DynamicClassAttribute data-descriptors "name" and "value"
which behave differently when looked up on an instance or on the class.
When dealing with inference of an arbitrary instance of the enum class,
e.g. in a method defined in the class body like:

    class SomeEnum(enum.Enum):
        def method(self):
            self.name  # <- here

we should assume that "self.name" is the string name of some enum
member, unless the enum itself defines a "name" member.

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
  • Loading branch information
nelfin and Pierre-Sassoulas committed Jun 13, 2021
1 parent 032fb3a commit 4c9b9b5
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 1 deletion.
8 changes: 8 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ What's New in astroid 2.6.0?
============================
Release Date: TBA


* Update enum brain to improve inference of .name and .value dynamic class
attributes

Closes PyCQA/pylint#1932
Closes PyCQA/pylint#2062

* Removed ``Repr``, ``Exec``, and ``Print`` nodes as the ``ast`` nodes
they represented have been removed with the change to Python 3

Expand All @@ -18,6 +25,7 @@ Release Date: TBA
and ``ast.NamedConstant`` were merged into ``ast.Constant``.



What's New in astroid 2.5.8?
============================
Release Date: 2021-06-07
Expand Down
1 change: 1 addition & 0 deletions astroid/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"LazyProperty",
"lazy",
"cache_readonly",
"DynamicClassAttribute",
}


Expand Down
23 changes: 23 additions & 0 deletions astroid/brain/brain_namedtuple_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ def infer_enum_class(node):
# Skip if the class is directly from enum module.
break
dunder_members = {}
target_names = set()
for local, values in node.locals.items():
if any(not isinstance(value, nodes.AssignName) for value in values):
continue
Expand Down Expand Up @@ -391,6 +392,7 @@ def infer_enum_class(node):
for target in targets:
if isinstance(target, nodes.Starred):
continue
target_names.add(target.name)
# Replace all the assignments with our mocked class.
classdef = dedent(
"""
Expand Down Expand Up @@ -429,6 +431,27 @@ def name(self):
]
)
node.locals["__members__"] = [members]
# The enum.Enum class itself defines two @DynamicClassAttribute data-descriptors
# "name" and "value" (which we override in the mocked class for each enum member
# above). When dealing with inference of an arbitrary instance of the enum
# class, e.g. in a method defined in the class body like:
# class SomeEnum(enum.Enum):
# def method(self):
# self.name # <- here
# In the absence of an enum member called "name" or "value", these attributes
# should resolve to the descriptor on that particular instance, i.e. enum member.
# For "value", we have no idea what that should be, but for "name", we at least
# know that it should be a string, so infer that as a guess.
if "name" not in target_names:
code = dedent(
"""
@property
def name(self):
return ''
"""
)
name_dynamicclassattr = AstroidBuilder(MANAGER).string_build(code)["name"]
node.locals["name"] = [name_dynamicclassattr]
break
return node

Expand Down
56 changes: 55 additions & 1 deletion tests/unittest_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
import pytest

import astroid
from astroid import MANAGER, bases, builder, nodes, test_utils, util
from astroid import MANAGER, bases, builder, nodes, objects, test_utils, util

try:
import multiprocessing # pylint: disable=unused-import
Expand Down Expand Up @@ -993,6 +993,60 @@ class ContentType(Enum):
node = astroid.extract_node(code)
next(node.infer())

def test_enum_name_is_str_on_self(self):
code = """
from enum import Enum
class TestEnum(Enum):
def func(self):
self.name #@
self.value #@
TestEnum.name #@
TestEnum.value #@
"""
i_name, i_value, c_name, c_value = astroid.extract_node(code)

# <instance>.name should be a string, <class>.name should be a property (that
# forwards the lookup to __getattr__)
inferred = next(i_name.infer())
assert isinstance(inferred, nodes.Const)
assert inferred.pytype() == "builtins.str"
inferred = next(c_name.infer())
assert isinstance(inferred, objects.Property)

# Inferring .value should not raise InferenceError. It is probably Uninferable
# but we don't particularly care
next(i_value.infer())
next(c_value.infer())

def test_enum_name_and_value_members_override_dynamicclassattr(self):
code = """
from enum import Enum
class TrickyEnum(Enum):
name = 1
value = 2
def func(self):
self.name #@
self.value #@
TrickyEnum.name #@
TrickyEnum.value #@
"""
i_name, i_value, c_name, c_value = astroid.extract_node(code)

# All of these cases should be inferred as enum members
inferred = next(i_name.infer())
assert isinstance(inferred, bases.Instance)
assert inferred.pytype() == ".TrickyEnum.name"
inferred = next(c_name.infer())
assert isinstance(inferred, bases.Instance)
assert inferred.pytype() == ".TrickyEnum.name"
inferred = next(i_value.infer())
assert isinstance(inferred, bases.Instance)
assert inferred.pytype() == ".TrickyEnum.value"
inferred = next(c_value.infer())
assert isinstance(inferred, bases.Instance)
assert inferred.pytype() == ".TrickyEnum.value"


@unittest.skipUnless(HAS_DATEUTIL, "This test requires the dateutil library.")
class DateutilBrainTest(unittest.TestCase):
Expand Down

0 comments on commit 4c9b9b5

Please sign in to comment.