Skip to content

Commit 6178e41

Browse files
Define Protocol as abstract to prevent abstract-method FP (#7839) (#7879)
(cherry picked from commit 85e7d93) Co-authored-by: Dani Alcala <[email protected]>
1 parent 438025d commit 6178e41

File tree

6 files changed

+71
-15
lines changed

6 files changed

+71
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixes false positive ``abstract-method`` on Protocol classes.
2+
3+
Closes #7209

pylint/checkers/classes/class_checker.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -2085,19 +2085,9 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No
20852085
if node_frame_class(method) in parents_with_called_inits:
20862086
return
20872087

2088-
# Return if klass is protocol
2089-
if klass.qname() in utils.TYPING_PROTOCOLS:
2088+
if utils.is_protocol_class(klass):
20902089
return
20912090

2092-
# Return if any of the klass' first-order bases is protocol
2093-
for base in klass.bases:
2094-
try:
2095-
for inf_base in base.infer():
2096-
if inf_base.qname() in utils.TYPING_PROTOCOLS:
2097-
return
2098-
except astroid.InferenceError:
2099-
continue
2100-
21012091
if decorated_with(node, ["typing.overload"]):
21022092
continue
21032093
self.add_message(

pylint/checkers/utils.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,10 @@ def class_is_abstract(node: nodes.ClassDef) -> bool:
11511151
"""Return true if the given class node should be considered as an abstract
11521152
class.
11531153
"""
1154+
# Protocol classes are considered "abstract"
1155+
if is_protocol_class(node):
1156+
return True
1157+
11541158
# Only check for explicit metaclass=ABCMeta on this specific class
11551159
meta = node.declared_metaclass()
11561160
if meta is not None:
@@ -1595,14 +1599,23 @@ def is_protocol_class(cls: nodes.NodeNG) -> bool:
15951599
"""Check if the given node represents a protocol class.
15961600
15971601
:param cls: The node to check
1598-
:returns: True if the node is a typing protocol class, false otherwise.
1602+
:returns: True if the node is or inherits from typing.Protocol directly, false otherwise.
15991603
"""
16001604
if not isinstance(cls, nodes.ClassDef):
16011605
return False
16021606

1603-
# Use .ancestors() since not all protocol classes can have
1604-
# their mro deduced.
1605-
return any(parent.qname() in TYPING_PROTOCOLS for parent in cls.ancestors())
1607+
# Return if klass is protocol
1608+
if cls.qname() in TYPING_PROTOCOLS:
1609+
return True
1610+
1611+
for base in cls.bases:
1612+
try:
1613+
for inf_base in base.infer():
1614+
if inf_base.qname() in TYPING_PROTOCOLS:
1615+
return True
1616+
except astroid.InferenceError:
1617+
continue
1618+
return False
16061619

16071620

16081621
def is_call_of_name(node: nodes.NodeNG, name: str) -> bool:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Test that classes inheriting directly from Protocol should not warn about abstract-method."""
2+
3+
# pylint: disable=too-few-public-methods,disallowed-name,invalid-name
4+
5+
from abc import abstractmethod, ABCMeta
6+
from typing import Protocol, Literal
7+
8+
9+
class FooProtocol(Protocol):
10+
"""Foo Protocol"""
11+
12+
@abstractmethod
13+
def foo(self) -> Literal["foo"]:
14+
"""foo method"""
15+
16+
def foo_no_abstract(self) -> Literal["foo"]:
17+
"""foo not abstract method"""
18+
19+
20+
class BarProtocol(Protocol):
21+
"""Bar Protocol"""
22+
@abstractmethod
23+
def bar(self) -> Literal["bar"]:
24+
"""bar method"""
25+
26+
27+
class FooBarProtocol(FooProtocol, BarProtocol, Protocol):
28+
"""FooBar Protocol"""
29+
30+
class BarParent(BarProtocol): # [abstract-method]
31+
"""Doesn't subclass typing.Protocol directly"""
32+
33+
class IndirectProtocol(FooProtocol): # [abstract-method]
34+
"""Doesn't subclass typing.Protocol directly"""
35+
36+
class AbcProtocol(FooProtocol, metaclass=ABCMeta):
37+
"""Doesn't subclass typing.Protocol but uses metaclass directly"""
38+
39+
class FooBar(FooBarProtocol):
40+
"""FooBar object"""
41+
42+
def bar(self) -> Literal["bar"]:
43+
return "bar"
44+
45+
def foo(self) -> Literal["foo"]:
46+
return "foo"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
min_pyver=3.8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
abstract-method:30:0:30:15:BarParent:Method 'bar' is abstract in class 'BarProtocol' but is not overridden:UNDEFINED
2+
abstract-method:33:0:33:22:IndirectProtocol:Method 'foo' is abstract in class 'FooProtocol' but is not overridden:UNDEFINED

0 commit comments

Comments
 (0)