summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.txt1
-rw-r--r--ChangeLog4
-rw-r--r--doc/whatsnew/2.14.rst4
-rw-r--r--pylint/checkers/typecheck.py75
-rw-r--r--tests/functional/e/enum_self_defined_member_5138.py63
-rw-r--r--tests/functional/e/enum_self_defined_member_5138.txt3
6 files changed, 150 insertions, 0 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 26df78598..81f97daf5 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -439,6 +439,7 @@ contributors:
- Jacques Kvam <jwkvam@gmail.com>
- Jace Browning <jacebrowning@gmail.com>: updated default report format with clickable paths
- JT Olds <jtolds@xnet5.com>
+- Huw Jones <huw@huwcbjones.co.uk> (huwcbjones)
- Hayden Richards <62866982+SupImDos@users.noreply.github.com> (SupImDos)
* Fixed "no-self-use" for async methods
* Fixed "docparams" extension for async functions and methods
diff --git a/ChangeLog b/ChangeLog
index 5c91451b6..0d8df9e0a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -249,6 +249,10 @@ Release date: TBA
Closes #5219
+* Fixed false positive ``no-member`` for Enums with self-defined members.
+
+ Closes #5138
+
* Fix false negative for ``no-member`` when attempting to assign an instance
attribute to itself without any prior assignment.
diff --git a/doc/whatsnew/2.14.rst b/doc/whatsnew/2.14.rst
index 5d0d0b371..cc6193855 100644
--- a/doc/whatsnew/2.14.rst
+++ b/doc/whatsnew/2.14.rst
@@ -260,6 +260,10 @@ Other Changes
Closes #5219
+* Fixed false positive ``no-member`` for Enums with self-defined members.
+
+ Closes #5138
+
* Fix false negative for ``no-member`` when attempting to assign an instance
attribute to itself without any prior assignment.
diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py
index d01899634..8375ec809 100644
--- a/pylint/checkers/typecheck.py
+++ b/pylint/checkers/typecheck.py
@@ -450,6 +450,8 @@ def _emit_no_member(
except astroid.MroError:
return False
if metaclass:
+ if _enum_has_attribute(owner, node):
+ return False
# Renamed in Python 3.10 to `EnumType`
return metaclass.qname() in {"enum.EnumMeta", "enum.EnumType"}
return False
@@ -523,6 +525,79 @@ def _emit_no_member(
return True
+def _get_all_attribute_assignments(
+ node: nodes.FunctionDef, name: str | None = None
+) -> set[str]:
+ attributes: set[str] = set()
+ for child in node.nodes_of_class((nodes.Assign, nodes.AnnAssign)):
+ targets = []
+ if isinstance(child, nodes.Assign):
+ targets = child.targets
+ elif isinstance(child, nodes.AnnAssign):
+ targets = [child.target]
+ for assign_target in targets:
+ if isinstance(assign_target, nodes.Tuple):
+ targets.extend(assign_target.elts)
+ continue
+ if (
+ isinstance(assign_target, nodes.AssignAttr)
+ and isinstance(assign_target.expr, nodes.Name)
+ and (name is None or assign_target.expr.name == name)
+ ):
+ attributes.add(assign_target.attrname)
+ return attributes
+
+
+def _enum_has_attribute(
+ owner: astroid.Instance | nodes.ClassDef, node: nodes.Attribute
+) -> bool:
+ if isinstance(owner, astroid.Instance):
+ enum_def = next(
+ (b.parent for b in owner.bases if isinstance(b.parent, nodes.ClassDef)),
+ None,
+ )
+
+ if enum_def is None:
+ # We don't inherit from anything, so try to find the parent
+ # class definition and roll with that
+ enum_def = node
+ while enum_def is not None and not isinstance(enum_def, nodes.ClassDef):
+ enum_def = enum_def.parent
+
+ # If this blows, something is clearly wrong
+ assert enum_def is not None, "enum_def unexpectedly None"
+ else:
+ enum_def = owner
+
+ # Find __new__ and __init__
+ dunder_new = next((m for m in enum_def.methods() if m.name == "__new__"), None)
+ dunder_init = next((m for m in enum_def.methods() if m.name == "__init__"), None)
+
+ enum_attributes: set[str] = set()
+
+ # Find attributes defined in __new__
+ if dunder_new:
+ # Get the object returned in __new__
+ returned_obj_name = next(
+ (c.value for c in dunder_new.get_children() if isinstance(c, nodes.Return)),
+ None,
+ )
+ if returned_obj_name is not None:
+ # Find all attribute assignments to the returned object
+ enum_attributes |= _get_all_attribute_assignments(
+ dunder_new, returned_obj_name.name
+ )
+
+ # Find attributes defined in __init__
+ if dunder_init and dunder_init.body and dunder_init.args:
+ # Grab the name referring to `self` from the function def
+ enum_attributes |= _get_all_attribute_assignments(
+ dunder_init, dunder_init.args.arguments[0].name
+ )
+
+ return node.attrname in enum_attributes
+
+
def _determine_callable(
callable_obj: nodes.NodeNG,
) -> tuple[CallableObjects, int, str]:
diff --git a/tests/functional/e/enum_self_defined_member_5138.py b/tests/functional/e/enum_self_defined_member_5138.py
new file mode 100644
index 000000000..4a49903c6
--- /dev/null
+++ b/tests/functional/e/enum_self_defined_member_5138.py
@@ -0,0 +1,63 @@
+"""Tests for self-defined Enum members (https://github.com/PyCQA/pylint/issues/5138)"""
+# pylint: disable=missing-docstring
+from enum import IntEnum, Enum
+
+
+class Day(IntEnum):
+ MONDAY = (1, "Mon")
+ TUESDAY = (2, "Tue")
+ WEDNESDAY = (3, "Wed")
+ THURSDAY = (4, "Thu")
+ FRIDAY = (5, "Fri")
+ SATURDAY = (6, "Sat")
+ SUNDAY = (7, "Sun")
+
+ def __new__(cls, value, abbr=None):
+ obj = int.__new__(cls, value)
+ obj._value_ = value
+ if abbr:
+ obj.abbr = abbr
+ else:
+ obj.abbr = ""
+ return obj
+
+ def __repr__(self):
+ return f"{self._value_}: {self.foo}" # [no-member]
+
+
+print(Day.FRIDAY.abbr)
+print(Day.FRIDAY.foo) # [no-member]
+
+
+class Length(Enum):
+ METRE = "metre", "m"
+ MILE = "mile", "m", True
+
+ def __init__(self, text: str, unit: str, is_imperial: bool = False):
+ self.text: str = text
+ self.unit: str = unit
+ if is_imperial:
+ self.suffix = " (imp)"
+ else:
+ self.suffix = ""
+
+
+print(f"100 {Length.METRE.unit}{Length.METRE.suffix}")
+print(Length.MILE.foo) # [no-member]
+
+
+class Binary(int, Enum):
+ ZERO = 0
+ ONE = 1
+
+ def __init__(self, value: int) -> None:
+ super().__init__()
+ self.str, self.inverse = str(value), abs(value - 1)
+
+ def no_op(_value):
+ pass
+ value_squared = value ** 2
+ no_op(value_squared)
+
+
+print(f"1={Binary.ONE.value} (Inverted: {Binary.ONE.inverse}")
diff --git a/tests/functional/e/enum_self_defined_member_5138.txt b/tests/functional/e/enum_self_defined_member_5138.txt
new file mode 100644
index 000000000..72fbbdc67
--- /dev/null
+++ b/tests/functional/e/enum_self_defined_member_5138.txt
@@ -0,0 +1,3 @@
+no-member:25:34:25:42:Day.__repr__:Instance of 'Day' has no 'foo' member:INFERENCE
+no-member:29:6:29:20::Instance of 'FRIDAY' has no 'foo' member:INFERENCE
+no-member:46:6:46:21::Instance of 'MILE' has no 'foo' member:INFERENCE