diff options
author | Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> | 2023-01-15 18:26:47 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-15 18:26:47 +0100 |
commit | c267397eda848544bcbea04e889815ac4faa6ba8 (patch) | |
tree | c1508f72cee5d2d92b58beb57f55811fb734c3ee | |
parent | 8581d9d26ae76ea3d5719bdd9a0e103bf79e1528 (diff) | |
download | astroid-git-c267397eda848544bcbea04e889815ac4faa6ba8.tar.gz |
Fix a false positive with user-defined `Enum` class (#1967)
Co-authored-by: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>
-rw-r--r-- | .github/workflows/ci.yaml | 1 | ||||
-rw-r--r-- | ChangeLog | 5 | ||||
-rw-r--r-- | astroid/brain/brain_namedtuple_enum.py | 22 | ||||
-rw-r--r-- | tests/unittest_brain.py | 44 |
4 files changed, 71 insertions, 1 deletions
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 01c0be83..a4be3e87 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,6 +93,7 @@ jobs: - name: Install Qt if: ${{ matrix.python-version == '3.10' }} run: | + sudo apt-get update sudo apt-get install build-essential libgl1-mesa-dev - name: Generate partial Python venv restore key id: generate-python-key @@ -16,6 +16,11 @@ Release date: TBA Closes #1958 +* Fix a false positive when an attribute named ``Enum`` was confused with ``enum.Enum``. + Calls to ``Enum`` are now inferred & the qualified name is checked. + + Refs PyCQA/pylint#5719 + * Remove unnecessary typing_extensions dependency on Python 3.11 and newer What's New in astroid 2.13.2? diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index ed80e783..c7e847f8 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -8,6 +8,7 @@ from __future__ import annotations import functools import keyword +import sys from collections.abc import Iterator from textwrap import dedent @@ -24,7 +25,12 @@ from astroid.exceptions import ( ) from astroid.manager import AstroidManager -TYPING_NAMEDTUPLE_BASENAMES = {"NamedTuple", "typing.NamedTuple"} +if sys.version_info >= (3, 8): + from typing import Final +else: + from typing_extensions import Final + + ENUM_BASE_NAMES = { "Enum", "IntEnum", @@ -33,6 +39,8 @@ ENUM_BASE_NAMES = { "IntFlag", "enum.IntFlag", } +ENUM_QNAME: Final[str] = "enum.Enum" +TYPING_NAMEDTUPLE_BASENAMES: Final[set[str]] = {"NamedTuple", "typing.NamedTuple"} def _infer_first(node, context): @@ -298,6 +306,18 @@ def infer_enum( node: nodes.Call, context: InferenceContext | None = None ) -> Iterator[bases.Instance]: """Specific inference function for enum Call node.""" + # Raise `UseInferenceDefault` if `node` is a call to a a user-defined Enum. + try: + inferred = node.func.infer(context) + except (InferenceError, StopIteration) as exc: + raise UseInferenceDefault from exc + + if not any( + isinstance(item, nodes.ClassDef) and item.qname() == ENUM_QNAME + for item in inferred + ): + raise UseInferenceDefault + enum_meta = _extract_single_node( """ class EnumMeta(object): diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index 11dd1139..9ee0f98f 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -1230,6 +1230,50 @@ class EnumBrainTest(unittest.TestCase): assert isinstance(inferred, bases.Instance) assert inferred._proxied.name == "ENUM_KEY" + def test_class_named_enum(self) -> None: + """Test that the user-defined class named `Enum` is not inferred as `enum.Enum`""" + astroid.extract_node( + """ + class Enum: + def __init__(self, one, two): + self.one = one + self.two = two + def pear(self): + ... + """, + "module_with_class_named_enum", + ) + + attribute_nodes = astroid.extract_node( + """ + import module_with_class_named_enum + module_with_class_named_enum.Enum("apple", "orange") #@ + typo_module_with_class_named_enum.Enum("apple", "orange") #@ + """ + ) + + name_nodes = astroid.extract_node( + """ + from module_with_class_named_enum import Enum + Enum("apple", "orange") #@ + TypoEnum("apple", "orange") #@ + """ + ) + + # Test that both of the successfully inferred `Name` & `Attribute` + # nodes refer to the user-defined Enum class. + for inferred in (attribute_nodes[0].inferred()[0], name_nodes[0].inferred()[0]): + assert isinstance(inferred, astroid.Instance) + assert inferred.name == "Enum" + assert inferred.qname() == "module_with_class_named_enum.Enum" + assert "pear" in inferred.locals + + # Test that an `InferenceError` is raised when an attempt is made to + # infer a `Name` or `Attribute` node & they cannot be found. + for node in (attribute_nodes[1], name_nodes[1]): + with pytest.raises(InferenceError): + node.inferred() + @unittest.skipUnless(HAS_DATEUTIL, "This test requires the dateutil library.") class DateutilBrainTest(unittest.TestCase): |