Skip to content

Commit

Permalink
Fix super-init-not-called if parent or self is a Protocol (
Browse files Browse the repository at this point in the history
…#5697)

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
  • Loading branch information
DanielNoord and Pierre-Sassoulas committed Jan 24, 2022
1 parent 7611ec4 commit 6977e48
Show file tree
Hide file tree
Showing 15 changed files with 128 additions and 17 deletions.
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ Release date: TBA
* The ``PyLinter`` class will now be initialized with a ``TextReporter``
as its reporter if none is provided.

* Fix ``super-init-not-called`` when parent or ``self`` is a ``Protocol``

Closes #4790

* Fix false positive ``not-callable`` with attributes that alias ``NamedTuple``

Partially closes #1730
Expand Down
4 changes: 4 additions & 0 deletions doc/whatsnew/2.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ Other Changes

.. _`emacs file locks`: https://www.gnu.org/software/emacs/manual/html_node/elisp/File-Locks.html

* Fix ``super-init-not-called`` when parent or ``self`` is a ``Protocol``

Closes #4790

* An astroid issue where symlinks were not being taken into account
was fixed

Expand Down
39 changes: 29 additions & 10 deletions pylint/checkers/classes/class_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@
"""Classes checker for Python code"""
import collections
from itertools import chain, zip_longest
from typing import List, Pattern
from typing import Dict, List, Pattern

import astroid
from astroid import nodes
from astroid import bases, nodes

from pylint.checkers import BaseChecker, utils
from pylint.checkers.utils import (
Expand All @@ -81,7 +81,7 @@
unimplemented_abstract_methods,
uninferable_final_decorators,
)
from pylint.interfaces import IAstroidChecker
from pylint.interfaces import INFERENCE, IAstroidChecker
from pylint.utils import get_global_option

INVALID_BASE_CLASSES = {"bool", "range", "slice", "memoryview"}
Expand Down Expand Up @@ -1086,12 +1086,13 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None:
self._check_useless_super_delegation(node)
self._check_property_with_parameters(node)

klass = node.parent.frame(future=True)
# 'is_method()' is called and makes sure that this is a 'nodes.ClassDef'
klass = node.parent.frame(future=True) # type: nodes.ClassDef
self._meth_could_be_func = True
# check first argument is self if this is actually a method
self._check_first_arg_for_type(node, klass.type == "metaclass")
if node.name == "__init__":
self._check_init(node)
self._check_init(node, klass)
return
# check signature if the method overloads inherited method
for overridden in klass.local_attr_ancestors(node.name):
Expand Down Expand Up @@ -1928,15 +1929,14 @@ def is_abstract(method):
continue
self.add_message("abstract-method", node=node, args=(name, owner.name))

def _check_init(self, node):
def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> None:
"""check that the __init__ method call super or ancestors'__init__
method (unless it is used for type hinting with `typing.overload`)
"""
if not self.linter.is_message_enabled(
"super-init-not-called"
) and not self.linter.is_message_enabled("non-parent-init-called"):
return
klass_node = node.parent.frame(future=True)
to_call = _ancestors_to_call(klass_node)
not_called_yet = dict(to_call)
for stmt in node.nodes_of_class(nodes.Call):
Expand Down Expand Up @@ -1980,12 +1980,29 @@ def _check_init(self, node):
except astroid.InferenceError:
continue
for klass, method in not_called_yet.items():
# Return if klass is protocol
if klass.qname() in utils.TYPING_PROTOCOLS:
return

# Return if any of the klass' first-order bases is protocol
for base in klass.bases:
# We don't need to catch InferenceError here as _ancestors_to_call
# already does this for us.
for inf_base in base.infer():
if inf_base.qname() in utils.TYPING_PROTOCOLS:
return

if decorated_with(node, ["typing.overload"]):
continue
cls = node_frame_class(method)
if klass.name == "object" or (cls and cls.name == "object"):
continue
self.add_message("super-init-not-called", args=klass.name, node=node)
self.add_message(
"super-init-not-called",
args=klass.name,
node=node,
confidence=INFERENCE,
)

def _check_signature(self, method1, refmethod, class_type, cls):
"""check that the signature of the two given methods match"""
Expand Down Expand Up @@ -2092,11 +2109,13 @@ def _is_mandatory_method_param(self, node: nodes.NodeNG) -> bool:
return isinstance(node, nodes.Name) and node.name == first_attr


def _ancestors_to_call(klass_node, method="__init__"):
def _ancestors_to_call(
klass_node: nodes.ClassDef, method="__init__"
) -> Dict[nodes.ClassDef, bases.UnboundMethod]:
"""return a dictionary where keys are the list of base classes providing
the queried method, and so that should/may be called from the method node
"""
to_call = {}
to_call: Dict[nodes.ClassDef, bases.UnboundMethod] = {}
for base_node in klass_node.ancestors(recurs=False):
try:
to_call[base_node] = next(base_node.igetattr(method))
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/i/init_not_called.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
super-init-not-called:25:4:26:27:ZZZZ.__init__:__init__ method from base class 'BBBB' is not called:UNDEFINED
super-init-not-called:58:4:59:20:AssignedInit.__init__:__init__ method from base class 'NewStyleC' is not called:UNDEFINED
super-init-not-called:25:4:26:27:ZZZZ.__init__:__init__ method from base class 'BBBB' is not called:INFERENCE
super-init-not-called:58:4:59:20:AssignedInit.__init__:__init__ method from base class 'NewStyleC' is not called:INFERENCE
2 changes: 1 addition & 1 deletion tests/functional/n/non/non_init_parent_called.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import-error:7:0:7:18::Unable to import 'nonexistant':UNDEFINED
non-parent-init-called:15:8:15:26:AAAA.__init__:__init__ method from a non direct base class 'BBBBMixin' is called:UNDEFINED
no-member:23:50:23:77:CCC:Module 'functional.n.non.non_init_parent_called' has no 'BBBB' member:INFERENCE
no-member:28:8:28:35:CCC.__init__:Module 'functional.n.non.non_init_parent_called' has no 'BBBB' member:INFERENCE
super-init-not-called:49:4:51:25:Super2.__init__:__init__ method from base class 'dict' is not called:UNDEFINED
super-init-not-called:49:4:51:25:Super2.__init__:__init__ method from base class 'dict' is not called:INFERENCE
no-member:51:8:51:23:Super2.__init__:Super of 'Super2' has no '__woohoo__' member:INFERENCE
17 changes: 13 additions & 4 deletions tests/functional/s/super/super_init_not_called.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
"""This should not emit a super-init-not-called warning. It previously did this, because
``next(node.infer())`` was used in that checker's logic and the first inferred node
was an Uninferable object, leading to this false positive."""
"""Tests for super-init-not-called."""
# pylint: disable=too-few-public-methods

import ctypes


class Foo(ctypes.BigEndianStructure):
"""A class"""
"""This class should not emit a super-init-not-called warning.
It previously did, because ``next(node.infer())`` was used in that checker's logic
and the first inferred node was an Uninferable object, leading to this false positive.
"""

def __init__(self):
ctypes.BigEndianStructure.__init__(self)


class UninferableChild(UninferableParent): # [undefined-variable]
"""An implementation that test if we don't crash on uninferable parents."""

def __init__(self):
...
1 change: 1 addition & 0 deletions tests/functional/s/super/super_init_not_called.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
undefined-variable:18:23:18:40:UninferableChild:Undefined variable 'UninferableParent':UNDEFINED
22 changes: 22 additions & 0 deletions tests/functional/s/super/super_init_not_called_extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Tests for super-init-not-called."""
# pylint: disable=too-few-public-methods

from typing_extensions import Protocol as ExtensionProtocol


class TestProto(ExtensionProtocol):
"""A protocol without __init__ using Protocol from typing_extensions."""


class TestParent(TestProto):
"""An implementation."""

def __init__(self):
...


class TestChild(TestParent):
"""An implementation which should call the init of TestParent."""

def __init__(self): # [super-init-not-called]
...
2 changes: 2 additions & 0 deletions tests/functional/s/super/super_init_not_called_extensions.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[testoptions]
max_pyver=3.9
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
super-init-not-called:21:4:22:11:TestChild.__init__:__init__ method from base class 'TestParent' is not called:INFERENCE
22 changes: 22 additions & 0 deletions tests/functional/s/super/super_init_not_called_extensions_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Tests for super-init-not-called."""
# pylint: disable=too-few-public-methods

from typing_extensions import Protocol as ExtensionProtocol


class TestProto(ExtensionProtocol):
"""A protocol without __init__ using Protocol from typing_extensions."""


class TestParent(TestProto):
"""An implementation."""

def __init__(self):
...


class TestChild(TestParent):
"""An implementation which should call the init of TestParent."""

def __init__(self): # [super-init-not-called]
...
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[testoptions]
# Windows test environments on >= 3.10 don't have typing_extensions
exclude_platforms=win32
min_pyver=3.10
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
super-init-not-called:21:4:22:11:TestChild.__init__:__init__ method from base class 'TestParent' is not called:INFERENCE
20 changes: 20 additions & 0 deletions tests/functional/s/super/super_init_not_called_py38.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Tests for super-init-not-called with Protocol."""
# pylint: disable=too-few-public-methods

from abc import abstractmethod
from typing import Protocol


class MyProtocol(Protocol):
"""A protocol."""

@abstractmethod
def __init__(self) -> None:
raise NotImplementedError


class ProtocolImplimentation(MyProtocol):
"""An implementation."""

def __init__(self) -> None:
...
2 changes: 2 additions & 0 deletions tests/functional/s/super/super_init_not_called_py38.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.8

0 comments on commit 6977e48

Please sign in to comment.