diff --git a/ChangeLog b/ChangeLog index 40e05883b1..0bc7f85f6d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -223,6 +223,11 @@ Release date: TBA Closes #4434 Closes #5370 +* Fix false positive ``super-init-not-called`` for classes that inherit their ``init`` from + a parent. + + Closes #4941 + * ``encoding`` can now be supplied as a positional argument to calls that open files without triggering ``unspecified-encoding``. diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index a31078850a..c9204109ec 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -128,6 +128,11 @@ Other Changes Closes #5483 +* Fix false positive ``super-init-not-called`` for classes that inherit their ``init`` from + a parent. + + Closes #4941 + * Fix ``unnecessary_dict_index_lookup`` false positive when deleting a dictionary's entry. Closes #4716 diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index c39ecafe46..a33efb910c 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -52,7 +52,7 @@ """Classes checker for Python code""" import collections from itertools import chain, zip_longest -from typing import Dict, List, Pattern +from typing import Dict, List, Pattern, Set import astroid from astroid import bases, nodes @@ -1939,6 +1939,7 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No return to_call = _ancestors_to_call(klass_node) not_called_yet = dict(to_call) + parents_with_called_inits: Set[bases.UnboundMethod] = set() for stmt in node.nodes_of_class(nodes.Call): expr = stmt.func if not isinstance(expr, nodes.Attribute) or expr.attrname != "__init__": @@ -1971,7 +1972,9 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No if isinstance(klass, astroid.objects.Super): return try: - del not_called_yet[klass] + method = not_called_yet.pop(klass) + # Record that the class' init has been called + parents_with_called_inits.add(node_frame_class(method)) except KeyError: if klass not in to_call: self.add_message( @@ -1980,6 +1983,11 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No except astroid.InferenceError: continue for klass, method in not_called_yet.items(): + # Check if the init of the class that defines this init has already + # been called. + if node_frame_class(method) in parents_with_called_inits: + return + # Return if klass is protocol if klass.qname() in utils.TYPING_PROTOCOLS: return diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index b0daf4bd6e..951504324d 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -427,9 +427,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): ), ) - def __init__( - self, linter: Optional["PyLinter"] = None - ): # pylint: disable=super-init-not-called # See https://github.com/PyCQA/pylint/issues/4941 + def __init__(self, linter: Optional["PyLinter"] = None) -> None: BaseChecker.__init__(self, linter) self.import_graph: collections.defaultdict = collections.defaultdict(set) self._imports_stack: List[Tuple[Any, Any]] = [] diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index a203f2d07c..11c755ecfd 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -463,9 +463,7 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): ), } - def __init__( - self, linter: Optional["PyLinter"] = None - ): # pylint: disable=super-init-not-called # See https://github.com/PyCQA/pylint/issues/4941 + def __init__(self, linter: Optional["PyLinter"] = None) -> None: BaseChecker.__init__(self, linter) self._deprecated_methods: Set[Any] = set() self._deprecated_methods.update(DEPRECATED_METHODS[0]) diff --git a/tests/functional/s/super/super_init_not_called.py b/tests/functional/s/super/super_init_not_called.py index d2e9cffd87..c8e9f09d1e 100644 --- a/tests/functional/s/super/super_init_not_called.py +++ b/tests/functional/s/super/super_init_not_called.py @@ -1,5 +1,5 @@ """Tests for super-init-not-called.""" -# pylint: disable=too-few-public-methods +# pylint: disable=too-few-public-methods, missing-class-docstring import ctypes @@ -20,3 +20,31 @@ class UninferableChild(UninferableParent): # [undefined-variable] def __init__(self): ... + + +# Tests for not calling the init of a parent that does not define one +# but inherits it. +class GrandParentWithInit: + def __init__(self): + print(self) + + +class ParentWithoutInit(GrandParentWithInit): + pass + + +class ChildOne(ParentWithoutInit, GrandParentWithInit): + """Since ParentWithoutInit calls GrandParentWithInit it doesn't need to be called.""" + + def __init__(self): + GrandParentWithInit.__init__(self) + + +class ChildTwo(ParentWithoutInit): + def __init__(self): + ParentWithoutInit.__init__(self) + + +class ChildThree(ParentWithoutInit): + def __init__(self): # [super-init-not-called] + ... diff --git a/tests/functional/s/super/super_init_not_called.txt b/tests/functional/s/super/super_init_not_called.txt index b453d8a856..b6a220c69f 100644 --- a/tests/functional/s/super/super_init_not_called.txt +++ b/tests/functional/s/super/super_init_not_called.txt @@ -1 +1,2 @@ undefined-variable:18:23:18:40:UninferableChild:Undefined variable 'UninferableParent':UNDEFINED +super-init-not-called:49:4:50:11:ChildThree.__init__:__init__ method from base class 'ParentWithoutInit' is not called:INFERENCE