Skip to content

Commit 1c70358

Browse files
Fix a false positive with user-defined Enum class (#1967)
Co-authored-by: Daniël van Noord <[email protected]> (cherry picked from commit c267397)
1 parent 07cd5c5 commit 1c70358

File tree

4 files changed

+71
-1
lines changed

4 files changed

+71
-1
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ jobs:
9393
- name: Install Qt
9494
if: ${{ matrix.python-version == '3.10' }}
9595
run: |
96+
sudo apt-get update
9697
sudo apt-get install build-essential libgl1-mesa-dev
9798
- name: Generate partial Python venv restore key
9899
id: generate-python-key

ChangeLog

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

1717
Closes #1958
1818

19+
* Fix a false positive when an attribute named ``Enum`` was confused with ``enum.Enum``.
20+
Calls to ``Enum`` are now inferred & the qualified name is checked.
21+
22+
Refs PyCQA/pylint#5719
23+
1924
* Remove unnecessary typing_extensions dependency on Python 3.11 and newer
2025

2126
What's New in astroid 2.13.2?

astroid/brain/brain_namedtuple_enum.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import functools
1010
import keyword
11+
import sys
1112
from collections.abc import Iterator
1213
from textwrap import dedent
1314

@@ -24,7 +25,12 @@
2425
)
2526
from astroid.manager import AstroidManager
2627

27-
TYPING_NAMEDTUPLE_BASENAMES = {"NamedTuple", "typing.NamedTuple"}
28+
if sys.version_info >= (3, 8):
29+
from typing import Final
30+
else:
31+
from typing_extensions import Final
32+
33+
2834
ENUM_BASE_NAMES = {
2935
"Enum",
3036
"IntEnum",
@@ -33,6 +39,8 @@
3339
"IntFlag",
3440
"enum.IntFlag",
3541
}
42+
ENUM_QNAME: Final[str] = "enum.Enum"
43+
TYPING_NAMEDTUPLE_BASENAMES: Final[set[str]] = {"NamedTuple", "typing.NamedTuple"}
3644

3745

3846
def _infer_first(node, context):
@@ -298,6 +306,18 @@ def infer_enum(
298306
node: nodes.Call, context: InferenceContext | None = None
299307
) -> Iterator[bases.Instance]:
300308
"""Specific inference function for enum Call node."""
309+
# Raise `UseInferenceDefault` if `node` is a call to a a user-defined Enum.
310+
try:
311+
inferred = node.func.infer(context)
312+
except (InferenceError, StopIteration) as exc:
313+
raise UseInferenceDefault from exc
314+
315+
if not any(
316+
isinstance(item, nodes.ClassDef) and item.qname() == ENUM_QNAME
317+
for item in inferred
318+
):
319+
raise UseInferenceDefault
320+
301321
enum_meta = _extract_single_node(
302322
"""
303323
class EnumMeta(object):

tests/unittest_brain.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,50 @@ class MyEnum(PyEnum):
12301230
assert isinstance(inferred, bases.Instance)
12311231
assert inferred._proxied.name == "ENUM_KEY"
12321232

1233+
def test_class_named_enum(self) -> None:
1234+
"""Test that the user-defined class named `Enum` is not inferred as `enum.Enum`"""
1235+
astroid.extract_node(
1236+
"""
1237+
class Enum:
1238+
def __init__(self, one, two):
1239+
self.one = one
1240+
self.two = two
1241+
def pear(self):
1242+
...
1243+
""",
1244+
"module_with_class_named_enum",
1245+
)
1246+
1247+
attribute_nodes = astroid.extract_node(
1248+
"""
1249+
import module_with_class_named_enum
1250+
module_with_class_named_enum.Enum("apple", "orange") #@
1251+
typo_module_with_class_named_enum.Enum("apple", "orange") #@
1252+
"""
1253+
)
1254+
1255+
name_nodes = astroid.extract_node(
1256+
"""
1257+
from module_with_class_named_enum import Enum
1258+
Enum("apple", "orange") #@
1259+
TypoEnum("apple", "orange") #@
1260+
"""
1261+
)
1262+
1263+
# Test that both of the successfully inferred `Name` & `Attribute`
1264+
# nodes refer to the user-defined Enum class.
1265+
for inferred in (attribute_nodes[0].inferred()[0], name_nodes[0].inferred()[0]):
1266+
assert isinstance(inferred, astroid.Instance)
1267+
assert inferred.name == "Enum"
1268+
assert inferred.qname() == "module_with_class_named_enum.Enum"
1269+
assert "pear" in inferred.locals
1270+
1271+
# Test that an `InferenceError` is raised when an attempt is made to
1272+
# infer a `Name` or `Attribute` node & they cannot be found.
1273+
for node in (attribute_nodes[1], name_nodes[1]):
1274+
with pytest.raises(InferenceError):
1275+
node.inferred()
1276+
12331277

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

0 commit comments

Comments
 (0)