diff options
-rw-r--r-- | CONTRIBUTORS.txt | 1 | ||||
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | doc/whatsnew/2.14.rst | 4 | ||||
-rw-r--r-- | pylint/checkers/typecheck.py | 75 | ||||
-rw-r--r-- | tests/functional/e/enum_self_defined_member_5138.py | 63 | ||||
-rw-r--r-- | tests/functional/e/enum_self_defined_member_5138.txt | 3 |
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 @@ -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 |