Skip to content

Commit 45e7296

Browse files
committed
Fix abstract-class-instantiated false positive when class defines __new__ method.
Closes #3060
1 parent 1baa10e commit 45e7296

File tree

4 files changed

+92
-9
lines changed

4 files changed

+92
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix ``abstract-class-instantiated`` false positive when class defines ``__new__`` method.
2+
3+
Closes #3060

pylint/checkers/base/basic_error_checker.py

+42-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from pylint.checkers import utils
1818
from pylint.checkers.base.basic_checker import _BasicChecker
1919
from pylint.checkers.utils import infer_all
20-
from pylint.interfaces import HIGH
20+
from pylint.interfaces import HIGH, INFERENCE
2121

2222
ABC_METACLASSES = {"_py_abc.ABCMeta", "abc.ABCMeta"} # Python 3.7+,
2323
# List of methods which can be redefined
@@ -78,6 +78,38 @@ def _has_abstract_methods(node: nodes.ClassDef) -> bool:
7878
return len(utils.unimplemented_abstract_methods(node)) > 0
7979

8080

81+
def _new_correctly_implemented(node: nodes.ClassDef) -> bool:
82+
"""Check if node implements `__new__`.
83+
84+
If `__new__` is implemented, check if it calls `super().__new__(cls)`.
85+
"""
86+
if "__new__" not in node.locals:
87+
return False
88+
89+
new = next(node.igetattr("__new__"))
90+
if not isinstance(new, astroid.UnboundMethod):
91+
return False
92+
93+
calls = new.nodes_of_class(
94+
nodes.Call, skip_klass=(nodes.FunctionDef, nodes.ClassDef)
95+
)
96+
for call in calls:
97+
if not isinstance(call.func, nodes.Attribute):
98+
continue
99+
if call.func.attrname != "__new__":
100+
continue
101+
102+
super_call = utils.safe_infer(call.func.expr)
103+
if not isinstance(super_call, astroid.objects.Super):
104+
continue
105+
106+
# `__new__` calls `super().__new__(cls)`.
107+
return False
108+
109+
# If we get to this point, it means `__new__` has no calls to `super().__new__(cls)`.
110+
return True
111+
112+
81113
def redefined_by_decorator(node: nodes.FunctionDef) -> bool:
82114
"""Return True if the object is a method redefined via decorator.
83115
@@ -446,7 +478,6 @@ def _check_inferred_class_is_abstract(
446478
# body, we're expecting that it knows what it is doing.
447479
return
448480

449-
# __init__ was called
450481
abstract_methods = _has_abstract_methods(inferred)
451482

452483
if not abstract_methods:
@@ -467,8 +498,16 @@ def _check_inferred_class_is_abstract(
467498
return
468499

469500
if metaclass.qname() in ABC_METACLASSES:
501+
if _new_correctly_implemented(inferred):
502+
# A class that implements `__new__` without calling `super().__new__(cls)`
503+
# should not emit the message.
504+
return
505+
470506
self.add_message(
471-
"abstract-class-instantiated", args=(inferred.name,), node=node
507+
"abstract-class-instantiated",
508+
args=(inferred.name,),
509+
node=node,
510+
confidence=INFERENCE,
472511
)
473512

474513
def _check_yield_outside_func(self, node: nodes.Yield) -> None:

tests/functional/a/abstract/abstract_class_instantiated.py

+40
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import abc
1010
import weakref
1111
from lala import Bala
12+
import pandas as pd
1213

1314

1415
class GoodClass(metaclass=abc.ABCMeta):
@@ -141,3 +142,42 @@ def test(self):
141142
def main_two():
142143
""" do nothing """
143144
BadClassTwo() # [abstract-class-instantiated]
145+
146+
147+
# Testcase from https://github.com/PyCQA/pylint/issues/3060
148+
with pd.ExcelWriter("demo.xlsx") as writer:
149+
print(writer)
150+
151+
class GoodWithNew(metaclass=abc.ABCMeta):
152+
def __new__(cls):
153+
pass
154+
155+
@property
156+
@abc.abstractmethod
157+
def sheets(self):
158+
"""sheets."""
159+
160+
@property
161+
@abc.abstractmethod
162+
def book(self):
163+
"""book."""
164+
165+
test = GoodWithNew()
166+
167+
168+
class BadWithNew(metaclass=abc.ABCMeta):
169+
def __new__(cls):
170+
print("Test.__new__")
171+
return super().__new__(cls)
172+
173+
@property
174+
@abc.abstractmethod
175+
def sheets(self):
176+
"""sheets."""
177+
178+
@property
179+
@abc.abstractmethod
180+
def book(self):
181+
"""book."""
182+
183+
test = BadWithNew() # [abstract-class-instantiated]
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
abstract-class-instantiated:108:4:108:27:main:Abstract class 'BadMroAbstractMethods' with abstract methods instantiated:UNDEFINED
2-
abstract-class-instantiated:109:4:109:14:main:Abstract class 'BadClass' with abstract methods instantiated:UNDEFINED
3-
abstract-class-instantiated:110:4:110:20:main:Abstract class 'SecondBadClass' with abstract methods instantiated:UNDEFINED
4-
abstract-class-instantiated:111:4:111:19:main:Abstract class 'ThirdBadClass' with abstract methods instantiated:UNDEFINED
5-
abstract-class-instantiated:128:4:128:20:main2:Abstract class 'FourthBadClass' with abstract methods instantiated:UNDEFINED
6-
abstract-class-instantiated:143:4:143:17:main_two:Abstract class 'BadClassTwo' with abstract methods instantiated:UNDEFINED
1+
abstract-class-instantiated:109:4:109:27:main:Abstract class 'BadMroAbstractMethods' with abstract methods instantiated:INFERENCE
2+
abstract-class-instantiated:110:4:110:14:main:Abstract class 'BadClass' with abstract methods instantiated:INFERENCE
3+
abstract-class-instantiated:111:4:111:20:main:Abstract class 'SecondBadClass' with abstract methods instantiated:INFERENCE
4+
abstract-class-instantiated:112:4:112:19:main:Abstract class 'ThirdBadClass' with abstract methods instantiated:INFERENCE
5+
abstract-class-instantiated:129:4:129:20:main2:Abstract class 'FourthBadClass' with abstract methods instantiated:INFERENCE
6+
abstract-class-instantiated:144:4:144:17:main_two:Abstract class 'BadClassTwo' with abstract methods instantiated:INFERENCE
7+
abstract-class-instantiated:183:7:183:19::Abstract class 'BadWithNew' with abstract methods instantiated:INFERENCE

0 commit comments

Comments
 (0)