diff --git a/doc/whatsnew/fragments/8753.bugfix b/doc/whatsnew/fragments/8753.bugfix new file mode 100644 index 0000000000..7b0ba8cc78 --- /dev/null +++ b/doc/whatsnew/fragments/8753.bugfix @@ -0,0 +1,3 @@ +Fix a false positive for ``method-hidden`` when using ``cached_property`` decorator. + +Closes #8753 diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index bdfb0968a4..377673f4b7 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -49,6 +49,7 @@ _AccessNodes = Union[nodes.Attribute, nodes.AssignAttr] INVALID_BASE_CLASSES = {"bool", "range", "slice", "memoryview"} +ALLOWED_PROPERTIES = {"bultins.property", "functools.cached_property"} BUILTIN_DECORATORS = {"builtins.property", "builtins.classmethod"} ASTROID_TYPE_COMPARATORS = { nodes.Const: lambda a, b: a.value == b.value, @@ -1252,11 +1253,15 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: # attribute affectation will call this method, not hiding it return if isinstance(decorator, nodes.Name): - if decorator.name == "property": + if decorator.name in ALLOWED_PROPERTIES: # attribute affectation will either call a setter or raise # an attribute error, anyway not hiding the function return + if isinstance(decorator, nodes.Attribute): + if self._check_functools_or_not(decorator): + return + # Infer the decorator and see if it returns something useful inferred = safe_infer(decorator) if not inferred: @@ -1454,6 +1459,24 @@ def _check_invalid_overridden_method( node=function_node, ) + def _check_functools_or_not(self, decorator: nodes.Attribute) -> bool: + if decorator.attrname != "cached_property": + return False + + if not isinstance(decorator.expr, nodes.Name): + return False + + _, import_nodes = decorator.expr.lookup(decorator.expr.name) + + if not import_nodes: + return False + import_node = import_nodes[0] + + if not isinstance(import_node, (astroid.Import, astroid.ImportFrom)): + return False + + return "functools" in dict(import_node.names) + def _check_slots(self, node: nodes.ClassDef) -> None: if "__slots__" not in node.locals: return diff --git a/tests/functional/m/method_hidden.py b/tests/functional/m/method_hidden.py index ea94b3ca26..19fd60c722 100644 --- a/tests/functional/m/method_hidden.py +++ b/tests/functional/m/method_hidden.py @@ -2,6 +2,8 @@ # pylint: disable=unused-private-member """check method hiding ancestor attribute """ +import functools as ft +import something_else as functools # pylint: disable=import-error class Abcd: @@ -106,6 +108,7 @@ def default(self, o): class Parent: def __init__(self): self._protected = None + self._protected_two = None class Child(Parent): @@ -113,6 +116,16 @@ def _protected(self): # [method-hidden] pass +class CachedChild(Parent): + @ft.cached_property + def _protected(self): + pass + + @functools.cached_property + def _protected_two(self): + pass + + class ParentTwo: def __init__(self): self.__private = None diff --git a/tests/functional/m/method_hidden.txt b/tests/functional/m/method_hidden.txt index abce3cd407..41a3c2bb5b 100644 --- a/tests/functional/m/method_hidden.txt +++ b/tests/functional/m/method_hidden.txt @@ -1,3 +1,3 @@ -method-hidden:17:4:17:12:Cdef.abcd:An attribute defined in functional.m.method_hidden line 11 hides this method:UNDEFINED -method-hidden:85:4:85:11:One.one:An attribute defined in functional.m.method_hidden line 83 hides this method:UNDEFINED -method-hidden:112:4:112:18:Child._protected:An attribute defined in functional.m.method_hidden line 108 hides this method:UNDEFINED +method-hidden:19:4:19:12:Cdef.abcd:An attribute defined in functional.m.method_hidden line 13 hides this method:UNDEFINED +method-hidden:87:4:87:11:One.one:An attribute defined in functional.m.method_hidden line 85 hides this method:UNDEFINED +method-hidden:115:4:115:18:Child._protected:An attribute defined in functional.m.method_hidden line 110 hides this method:UNDEFINED diff --git a/tests/functional/m/method_hidden_py39.py b/tests/functional/m/method_hidden_py39.py new file mode 100644 index 0000000000..ac087d0d6c --- /dev/null +++ b/tests/functional/m/method_hidden_py39.py @@ -0,0 +1,16 @@ +# pylint: disable=too-few-public-methods,missing-docstring +"""check method hiding ancestor attribute +""" +import something_else as functools # pylint: disable=import-error + + +class Parent: + def __init__(self): + self._protected = None + + +class Child(Parent): + @functools().cached_property + def _protected(self): + # This test case is only valid for python3.9 and above + pass diff --git a/tests/functional/m/method_hidden_py39.rc b/tests/functional/m/method_hidden_py39.rc new file mode 100644 index 0000000000..15ad50f5ab --- /dev/null +++ b/tests/functional/m/method_hidden_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver = 3.9