Skip to content

Commit 8231e06

Browse files
authored
Fix false positives for 'unnecessary-dunder-call' in lambdas (#9034)
* Add exceptions to C2801 for certain dunders in a lambda definition * Split tests based on implementation and version to pass checks and have finer control
1 parent 6dac300 commit 8231e06

13 files changed

+388
-11
lines changed

doc/whatsnew/fragments/8769.bugfix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Dunder methods defined in lambda do not trigger ``unnecessary-dunder-call`` anymore, if they cannot be replaced by the non-dunder call.
2+
3+
Closes #8769

pylint/checkers/dunder_methods.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from pylint.checkers import BaseChecker
1313
from pylint.checkers.utils import safe_infer
14-
from pylint.constants import DUNDER_METHODS
14+
from pylint.constants import DUNDER_METHODS, UNNECESSARY_DUNDER_CALL_LAMBDA_EXCEPTIONS
1515
from pylint.interfaces import HIGH
1616

1717
if TYPE_CHECKING:
@@ -52,25 +52,33 @@ def open(self) -> None:
5252
self._dunder_methods.update(dunder_methods)
5353

5454
@staticmethod
55-
def within_dunder_def(node: nodes.NodeNG) -> bool:
55+
def within_dunder_or_lambda_def(node: nodes.NodeNG) -> bool:
5656
"""Check if dunder method call is within a dunder method definition."""
5757
parent = node.parent
5858
while parent is not None:
5959
if (
6060
isinstance(parent, nodes.FunctionDef)
6161
and parent.name.startswith("__")
6262
and parent.name.endswith("__")
63+
or DunderCallChecker.is_lambda_rule_exception(parent, node)
6364
):
6465
return True
6566
parent = parent.parent
6667
return False
6768

69+
@staticmethod
70+
def is_lambda_rule_exception(ancestor: nodes.NodeNG, node: nodes.NodeNG) -> bool:
71+
return (
72+
isinstance(ancestor, nodes.Lambda)
73+
and node.func.attrname in UNNECESSARY_DUNDER_CALL_LAMBDA_EXCEPTIONS
74+
)
75+
6876
def visit_call(self, node: nodes.Call) -> None:
6977
"""Check if method being called is an unnecessary dunder method."""
7078
if (
7179
isinstance(node.func, nodes.Attribute)
7280
and node.func.attrname in self._dunder_methods
73-
and not self.within_dunder_def(node)
81+
and not self.within_dunder_or_lambda_def(node)
7482
and not (
7583
isinstance(node.func.expr, nodes.Call)
7684
and isinstance(node.func.expr.func, nodes.Name)

pylint/constants.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,28 @@ def _get_pylint_home() -> str:
249249
"__subclasshook__",
250250
"__weakref__",
251251
]
252+
253+
# C2801 rule exceptions as their corresponding function/method/operator
254+
# is not valid python syntax in a lambda definition
255+
UNNECESSARY_DUNDER_CALL_LAMBDA_EXCEPTIONS = [
256+
"__init__",
257+
"__del__",
258+
"__delattr__",
259+
"__set__",
260+
"__delete__",
261+
"__setitem__",
262+
"__delitem__",
263+
"__iadd__",
264+
"__isub__",
265+
"__imul__",
266+
"__imatmul__",
267+
"__itruediv__",
268+
"__ifloordiv__",
269+
"__imod__",
270+
"__ipow__",
271+
"__ilshift__",
272+
"__irshift__",
273+
"__iand__",
274+
"__ixor__",
275+
"__ior__",
276+
]

tests/functional/u/unnecessary/unnecessary_dunder_call.txt

Lines changed: 0 additions & 8 deletions
This file was deleted.

tests/functional/u/unnecessary/unnecessary_dunder_call.py renamed to tests/functional/u/unnecessary/unnecessary_dunder_call_py38.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Checks for unnecessary-dunder-call."""
22
# pylint: disable=too-few-public-methods, undefined-variable
33
# pylint: disable=missing-class-docstring, missing-function-docstring
4+
# pylint: disable=protected-access, unnecessary-lambda-assignment, unnecessary-lambda
45
from collections import OrderedDict
56
from typing import Any
67

@@ -128,3 +129,16 @@ class MyString(str):
128129
def rjust(self, width, fillchar= ' '):
129130
"""Acceptable call to __index__"""
130131
width = width.__index__()
132+
133+
# Test no lint raised for these dunders within lambdas
134+
lambda1 = lambda x: x.__setitem__(1,2)
135+
lambda2 = lambda x: x.__del__(1)
136+
lambda3 = lambda x,y: x.__ipow__(y)
137+
lambda4 = lambda u,v: u.__setitem__(v())
138+
139+
# Test lint raised for these dunders within lambdas
140+
lambda5 = lambda x: x.__gt__(3) # [unnecessary-dunder-call]
141+
lambda6 = lambda x,y: x.__or__(y) # [unnecessary-dunder-call]
142+
lambda7 = lambda x: x.__iter__() # [unnecessary-dunder-call]
143+
lambda8 = lambda z: z.__hash__() # [unnecessary-dunder-call]
144+
lambda9 = lambda n: (4).__rmul__(n) # [unnecessary-dunder-call]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[testoptions]
2+
max_pyver=3.8
3+
except_implementations=PyPy
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
unnecessary-dunder-call:9:10:9:28::Unnecessarily calls dunder method __str__. Use str built-in function.:HIGH
2+
unnecessary-dunder-call:10:11:10:30::Unnecessarily calls dunder method __add__. Use + operator.:HIGH
3+
unnecessary-dunder-call:11:10:11:40::Unnecessarily calls dunder method __repr__. Use repr built-in function.:HIGH
4+
unnecessary-dunder-call:13:18:13:43::Unnecessarily calls dunder method __contains__. Use in keyword.:HIGH
5+
unnecessary-dunder-call:18:0:18:31::Unnecessarily calls dunder method __init__. Instantiate class directly.:HIGH
6+
unnecessary-dunder-call:26:11:26:24:is_bigger_than_two:Unnecessarily calls dunder method __gt__. Use > operator.:HIGH
7+
unnecessary-dunder-call:119:20:119:39::Unnecessarily calls dunder method __add__. Use + operator.:HIGH
8+
unnecessary-dunder-call:120:0:120:44::Unnecessarily calls dunder method __setitem__. Set item via subscript.:HIGH
9+
unnecessary-dunder-call:140:20:140:31:<lambda>:Unnecessarily calls dunder method __gt__. Use > operator.:HIGH
10+
unnecessary-dunder-call:141:22:141:33:<lambda>:Unnecessarily calls dunder method __or__. Use | operator.:HIGH
11+
unnecessary-dunder-call:142:20:142:32:<lambda>:Unnecessarily calls dunder method __iter__. Use iter built-in function.:HIGH
12+
unnecessary-dunder-call:143:20:143:32:<lambda>:Unnecessarily calls dunder method __hash__. Use hash built-in function.:HIGH
13+
unnecessary-dunder-call:144:20:144:35:<lambda>:Unnecessarily calls dunder method __rmul__. Use * operator.:HIGH
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""Checks for unnecessary-dunder-call."""
2+
# pylint: disable=too-few-public-methods, undefined-variable
3+
# pylint: disable=missing-class-docstring, missing-function-docstring
4+
# pylint: disable=protected-access, unnecessary-lambda-assignment, unnecessary-lambda
5+
from collections import OrderedDict
6+
from typing import Any
7+
8+
# Test includelisted dunder methods raise lint when manually called.
9+
num_str = some_num.__str__() # [unnecessary-dunder-call]
10+
num_repr = some_num.__add__(2) # [unnecessary-dunder-call]
11+
my_repr = my_module.my_object.__repr__() # [unnecessary-dunder-call]
12+
13+
MY_CONTAINS_BAD = {1, 2, 3}.__contains__(1) # [unnecessary-dunder-call]
14+
MY_CONTAINS_GOOD = 1 in {1, 2, 3}
15+
16+
# Just instantiate like a normal person please
17+
my_list_bad = []
18+
my_list_bad.__init__({1, 2, 3}) # [unnecessary-dunder-call]
19+
my_list_good = list({1, 2, 3})
20+
21+
# Test unknown/user-defined dunder methods don't raise lint.
22+
my_woohoo = my_object.__woohoo__()
23+
24+
# Test lint raised within function.
25+
def is_bigger_than_two(val):
26+
return val.__gt__(2) # [unnecessary-dunder-call]
27+
28+
# Test dunder methods don't raise lint
29+
# if within a dunder method definition.
30+
class Foo1:
31+
def __init__(self):
32+
object.__init__(self)
33+
34+
class Foo2:
35+
def __init__(self):
36+
super().__init__(self)
37+
38+
class Bar1:
39+
def __new__(cls):
40+
object.__new__(cls)
41+
42+
class Bar2:
43+
def __new__(cls):
44+
super().__new__(cls)
45+
46+
class CustomRegistry(dict):
47+
def __init__(self) -> None:
48+
super().__init__()
49+
self._entry_ids = {}
50+
51+
def __setitem__(self, key, entry) -> None:
52+
super().__setitem__(key, entry)
53+
self._entry_ids.__setitem__(entry.id, entry)
54+
self._entry_ids.__delitem__(entry.id)
55+
56+
def __delitem__(self, key: str) -> None:
57+
entry = self[key]
58+
self._entry_ids.__delitem__(entry.id)
59+
super().__delitem__(key)
60+
61+
class CustomState:
62+
def __init__(self, state):
63+
self._state = state
64+
65+
def __eq__(self, other: Any) -> bool:
66+
return self._state.__eq__(other)
67+
68+
class CustomDict(OrderedDict):
69+
def __init__(self, *args, **kwds):
70+
OrderedDict.__init__(self, *args, **kwds)
71+
72+
def __setitem__(self, key, value):
73+
OrderedDict.__setitem__(self, key, value)
74+
75+
76+
class MyClass(list):
77+
def __contains__(self, item):
78+
print("do some special checks")
79+
return super().__contains__(item)
80+
81+
class PluginBase:
82+
subclasses = []
83+
84+
def __init_subclass__(cls, **kwargs):
85+
super().__init_subclass__(**kwargs)
86+
cls.subclasses.append(cls)
87+
88+
# Validate that dunder call is allowed
89+
# at any depth within dunder definition
90+
class SomeClass:
91+
def __init__(self):
92+
self.my_attr = object()
93+
94+
def __setattr__(self, name, value):
95+
def nested_function():
96+
self.my_attr.__setattr__(name, value)
97+
98+
nested_function()
99+
100+
# Allow use of dunder methods that don't
101+
# have an alternate method of being called
102+
class Base:
103+
@classmethod
104+
def get_first_subclass(cls):
105+
for subklass in cls.__subclasses__():
106+
return subklass
107+
return object
108+
109+
# Test no lint raised for attributes.
110+
my_instance_name = x.__class__.__name__
111+
my_pkg_version = pkg.__version__
112+
113+
# Allow use of dunder methods on non instantiated classes
114+
MANUAL_SELF = int.__add__(1, 1)
115+
MY_DICT = {"a": 1, "b": 2}
116+
dict.__setitem__(MY_DICT, "key", "value")
117+
118+
# Still flag instantiated classes
119+
INSTANTIATED_SELF = int("1").__add__(1) # [unnecessary-dunder-call]
120+
{"a": 1, "b": 2}.__setitem__("key", "value") # [unnecessary-dunder-call]
121+
122+
# We also exclude dunder methods called on super
123+
# since we can't apply alternate operators/functions here.
124+
a = [1, 2, 3]
125+
assert super(type(a), a).__str__() == "[1, 2, 3]"
126+
127+
class MyString(str):
128+
"""Custom str implementation"""
129+
def rjust(self, width, fillchar= ' '):
130+
"""Acceptable call to __index__"""
131+
width = width.__index__()
132+
133+
# Test no lint raised for these dunders within lambdas
134+
lambda1 = lambda x: x.__setitem__(1,2)
135+
lambda2 = lambda x: x.__del__(1)
136+
lambda3 = lambda x,y: x.__ipow__(y)
137+
lambda4 = lambda u,v: u.__setitem__(v())
138+
139+
# Test lint raised for these dunders within lambdas
140+
lambda5 = lambda x: x.__gt__(3) # [unnecessary-dunder-call]
141+
lambda6 = lambda x,y: x.__or__(y) # [unnecessary-dunder-call]
142+
lambda7 = lambda x: x.__iter__() # [unnecessary-dunder-call]
143+
lambda8 = lambda z: z.__hash__() # [unnecessary-dunder-call]
144+
lambda9 = lambda n: (4).__rmul__(n) # [unnecessary-dunder-call]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[testoptions]
2+
max_pyver=3.8
3+
except_implementations=CPython
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
unnecessary-dunder-call:9:10:None:None::Unnecessarily calls dunder method __str__. Use str built-in function.:HIGH
2+
unnecessary-dunder-call:10:11:None:None::Unnecessarily calls dunder method __add__. Use + operator.:HIGH
3+
unnecessary-dunder-call:11:10:None:None::Unnecessarily calls dunder method __repr__. Use repr built-in function.:HIGH
4+
unnecessary-dunder-call:13:18:None:None::Unnecessarily calls dunder method __contains__. Use in keyword.:HIGH
5+
unnecessary-dunder-call:18:0:None:None::Unnecessarily calls dunder method __init__. Instantiate class directly.:HIGH
6+
unnecessary-dunder-call:26:11:None:None:is_bigger_than_two:Unnecessarily calls dunder method __gt__. Use > operator.:HIGH
7+
unnecessary-dunder-call:119:20:None:None::Unnecessarily calls dunder method __add__. Use + operator.:HIGH
8+
unnecessary-dunder-call:120:0:None:None::Unnecessarily calls dunder method __setitem__. Set item via subscript.:HIGH
9+
unnecessary-dunder-call:140:20:None:None:<lambda>:Unnecessarily calls dunder method __gt__. Use > operator.:HIGH
10+
unnecessary-dunder-call:141:22:None:None:<lambda>:Unnecessarily calls dunder method __or__. Use | operator.:HIGH
11+
unnecessary-dunder-call:142:20:None:None:<lambda>:Unnecessarily calls dunder method __iter__. Use iter built-in function.:HIGH
12+
unnecessary-dunder-call:143:20:None:None:<lambda>:Unnecessarily calls dunder method __hash__. Use hash built-in function.:HIGH
13+
unnecessary-dunder-call:144:21:None:None:<lambda>:Unnecessarily calls dunder method __rmul__. Use * operator.:HIGH

0 commit comments

Comments
 (0)