Skip to content

Commit 1f0b406

Browse files
jpy-gitPierre-SassoulasDanielNoord
authored
C2801: New check for manual __dunder__ methods (#5938)
Co-authored-by: Pierre Sassoulas <[email protected]> Co-authored-by: Daniël van Noord <[email protected]>
1 parent 5324fec commit 1f0b406

File tree

14 files changed

+227
-7
lines changed

14 files changed

+227
-7
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Release date: TBA
1010
Put new features here and also in 'doc/whatsnew/2.14.rst'
1111

1212

13+
* Add new check ``unnecessary-dunder-call`` for unnecessary dunder method calls.
14+
15+
Closes #5936
16+
1317
* ``potential-index-error``: Emitted when the index of a list or tuple exceeds its length.
1418
This checker is currently quite conservative to avoid false positives. We welcome
1519
suggestions for improvements.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
three = 3.0.__str__() # [unnecessary-dunder-call]
2+
twelve = "1".__add__("2") # [unnecessary-dunder-call]
3+
4+
5+
def is_bigger_than_two(x):
6+
return x.__gt__(2) # [unnecessary-dunder-call]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
three = str(3.0)
2+
twelve = "1" + "2"
3+
4+
5+
def is_bigger_than_two(x):
6+
return x > 2

doc/whatsnew/2.14.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Summary -- Release highlights
1212
New checkers
1313
============
1414

15+
* Add new check ``unnecessary-dunder-call`` for unnecessary dunder method calls.
16+
17+
Closes #5936
18+
1519
* ``potential-index-error``: Emitted when the index of a list or tuple exceeds its length.
1620
This checker is currently quite conservative to avoid false positives. We welcome
1721
suggestions for improvements.

pylint/checkers/base_checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __init__(
6161

6262
def __gt__(self, other):
6363
"""Permit to sort a list of Checker by name."""
64-
return f"{self.name}{self.msgs}".__gt__(f"{other.name}{other.msgs}")
64+
return f"{self.name}{self.msgs}" > (f"{other.name}{other.msgs}")
6565

6666
def __repr__(self):
6767
status = "Checker" if self.enabled else "Disabled checker"

pylint/checkers/dunder_methods.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
2+
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
3+
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
4+
5+
from typing import TYPE_CHECKING
6+
7+
from astroid import nodes
8+
9+
from pylint.checkers import BaseChecker
10+
from pylint.interfaces import HIGH, IAstroidChecker
11+
12+
if TYPE_CHECKING:
13+
from pylint.lint import PyLinter
14+
15+
16+
class DunderCallChecker(BaseChecker):
17+
"""Check for unnecessary dunder method calls.
18+
19+
Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization
20+
We exclude __init__, __new__, __subclasses__, __init_subclass__,
21+
__set_name__, __class_getitem__, __missing__, __exit__, __await__,
22+
__del__, __aexit__, __getnewargs_ex__, __getnewargs__, __getstate__,
23+
__setstate__, __reduce__, __reduce_ex__
24+
since these either have no alternative method of being called or
25+
have a genuine use case for being called manually.
26+
"""
27+
28+
__implements__ = IAstroidChecker
29+
30+
includedict = {
31+
"__repr__": "Use repr built-in function",
32+
"__str__": "Use str built-in function",
33+
"__bytes__": "Use bytes built-in function",
34+
"__format__": "Use format built-in function, format string method, or f-string",
35+
"__lt__": "Use < operator",
36+
"__le__": "Use <= operator",
37+
"__eq__": "Use == operator",
38+
"__ne__": "Use != operator",
39+
"__gt__": "Use > operator",
40+
"__ge__": "Use >= operator",
41+
"__hash__": "Use hash built-in function",
42+
"__bool__": "Use bool built-in function",
43+
"__getattr__": "Access attribute directly or use getattr built-in function",
44+
"__getattribute__": "Access attribute directly or use getattr built-in function",
45+
"__setattr__": "Set attribute directly or use setattr built-in function",
46+
"__delattr__": "Use del keyword",
47+
"__dir__": "Use dir built-in function",
48+
"__get__": "Use get method",
49+
"__set__": "Use set method",
50+
"__delete__": "Use del keyword",
51+
"__instancecheck__": "Use isinstance built-in function",
52+
"__subclasscheck__": "Use issubclass built-in function",
53+
"__call__": "Invoke instance directly",
54+
"__len__": "Use len built-in function",
55+
"__length_hint__": "Use length_hint method",
56+
"__getitem__": "Access item via subscript",
57+
"__setitem__": "Set item via subscript",
58+
"__delitem__": "Use del keyword",
59+
"__iter__": "Use iter built-in function",
60+
"__next__": "Use next built-in function",
61+
"__reversed__": "Use reversed built-in funciton",
62+
"__contains__": "Use in keyword",
63+
"__add__": "Use + operator",
64+
"__sub__": "Use - operator",
65+
"__mul__": "Use * operator",
66+
"__matmul__": "Use @ operator",
67+
"__truediv__": "Use / operator",
68+
"__floordiv__": "Use // operator",
69+
"__mod__": "Use % operator",
70+
"__divmod__": "Use divmod built-in function",
71+
"__pow__": "Use ** operator or pow built-in function",
72+
"__lshift__": "Use << operator",
73+
"__rshift__": "Use >> operator",
74+
"__and__": "Use & operator",
75+
"__xor__": "Use ^ operator",
76+
"__or__": "Use | operator",
77+
"__radd__": "Use + operator",
78+
"__rsub__": "Use - operator",
79+
"__rmul__": "Use * operator",
80+
"__rmatmul__": "Use @ operator",
81+
"__rtruediv__": "Use / operator",
82+
"__rfloordiv__": "Use // operator",
83+
"__rmod__": "Use % operator",
84+
"__rdivmod__": "Use divmod built-in function",
85+
"__rpow__": "Use ** operator or pow built-in function",
86+
"__rlshift__": "Use << operator",
87+
"__rrshift__": "Use >> operator",
88+
"__rand__": "Use & operator",
89+
"__rxor__": "Use ^ operator",
90+
"__ror__": "Use | operator",
91+
"__iadd__": "Use += operator",
92+
"__isub__": "Use -= operator",
93+
"__imul__": "Use *= operator",
94+
"__imatmul__": "Use @= operator",
95+
"__itruediv__": "Use /= operator",
96+
"__ifloordiv__": "Use //= operator",
97+
"__imod__": "Use %= operator",
98+
"__ipow__": "Use **= operator",
99+
"__ilshift__": "Use <<= operator",
100+
"__irshift__": "Use >>= operator",
101+
"__iand__": "Use &= operator",
102+
"__ixor__": "Use ^= operator",
103+
"__ior__": "Use |= operator",
104+
"__neg__": "Multiply by -1 instead",
105+
"__pos__": "Multiply by +1 instead",
106+
"__abs__": "Use abs built-in function",
107+
"__invert__": "Use ~ operator",
108+
"__complex__": "Use complex built-in function",
109+
"__int__": "Use int built-in function",
110+
"__float__": "Use float built-in function",
111+
"__index__": "Use index method",
112+
"__round__": "Use round built-in function",
113+
"__trunc__": "Use math.trunc function",
114+
"__floor__": "Use math.floor function",
115+
"__ceil__": "Use math.ceil function",
116+
"__enter__": "Invoke context manager directly",
117+
"__aiter__": "Use iter built-in function",
118+
"__anext__": "Use next built-in function",
119+
"__aenter__": "Invoke context manager directly",
120+
"__copy__": "Use copy.copy function",
121+
"__deepcopy__": "Use copy.deepcopy function",
122+
"__fspath__": "Use os.fspath function instead",
123+
}
124+
name = "unnecessary-dunder-call"
125+
priority = -1
126+
msgs = {
127+
"C2801": (
128+
"Unnecessarily calls dunder method %s. %s.",
129+
"unnecessary-dunder-call",
130+
"Used when a dunder method is manually called instead "
131+
"of using the corresponding function/method/operator.",
132+
),
133+
}
134+
options = ()
135+
136+
def visit_call(self, node: nodes.Call) -> None:
137+
"""Check if method being called is an unnecessary dunder method."""
138+
if (
139+
isinstance(node.func, nodes.Attribute)
140+
and node.func.attrname in self.includedict
141+
):
142+
self.add_message(
143+
"unnecessary-dunder-call",
144+
node=node,
145+
args=(node.func.attrname, self.includedict[node.func.attrname]),
146+
confidence=HIGH,
147+
)
148+
149+
150+
def register(linter: "PyLinter") -> None:
151+
linter.register_checker(DunderCallChecker(linter))

tests/functional/a/assigning/assigning_non_slot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" Checks assigning attributes not found in class slots
22
will trigger assigning-non-slot warning.
33
"""
4-
# pylint: disable=too-few-public-methods, no-init, missing-docstring, import-error, useless-object-inheritance, redundant-u-string-prefix
4+
# pylint: disable=too-few-public-methods, no-init, missing-docstring, import-error, useless-object-inheritance, redundant-u-string-prefix, unnecessary-dunder-call
55
from collections import deque
66

77
from missing import Unknown

tests/functional/c/class_members_py30.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
""" Various tests for class members access. """
2-
# pylint: disable=too-few-public-methods,import-error,no-init,missing-docstring, wrong-import-position,wrong-import-order, useless-object-inheritance
2+
# pylint: disable=too-few-public-methods,import-error,no-init,missing-docstring, wrong-import-position,wrong-import-order, useless-object-inheritance, unnecessary-dunder-call
33
from missing import Missing
44
class MyClass(object):
55
"""class docstring"""

tests/functional/g/generic_alias/generic_alias_typing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Test generic alias support for typing.py types."""
22
# flake8: noqa
33
# pylint: disable=missing-docstring,pointless-statement
4-
# pylint: disable=too-few-public-methods,multiple-statements,line-too-long
4+
# pylint: disable=too-few-public-methods,multiple-statements,line-too-long, unnecessary-dunder-call
55
import abc
66
import typing
77

tests/functional/i/inner_classes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-pass
1+
# pylint: disable=too-few-public-methods, useless-object-inheritance, unnecessary-pass, unnecessary-dunder-call
22
"""Backend Base Classes for the schwelm user DB"""
33

44
__revision__ = "alpha"

tests/functional/n/non/non_init_parent_called.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# pylint: disable=protected-access,import-self,too-few-public-methods,line-too-long
2-
# pylint: disable=wrong-import-order, useless-object-inheritance,
2+
# pylint: disable=wrong-import-order, useless-object-inheritance, unnecessary-dunder-call
33
"""test for call to __init__ from a non ancestor class
44
"""
55
from __future__ import print_function

tests/functional/t/too/too_many_arguments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# pylint: disable=missing-docstring,wrong-import-position
1+
# pylint: disable=missing-docstring,wrong-import-position,unnecessary-dunder-call
22

33
def stupid_function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9): # [too-many-arguments]
44
return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Checks for unnecessary-dunder-call."""
2+
# pylint: disable=too-few-public-methods, undefined-variable, useless-object-inheritance
3+
# pylint: disable=missing-class-docstring, missing-function-docstring
4+
5+
# Test includelisted dunder methods raise lint when manually called.
6+
num_str = some_num.__str__() # [unnecessary-dunder-call]
7+
num_repr = some_num.__add__(2) # [unnecessary-dunder-call]
8+
my_repr = my_module.my_object.__repr__() # [unnecessary-dunder-call]
9+
10+
# Test unknown/user-defined dunder methods don't raise lint.
11+
my_woohoo = my_object.__woohoo__()
12+
13+
# Test allowed dunder methods don't raise lint.
14+
class Foo1(object):
15+
def __init__(self):
16+
object.__init__(self)
17+
18+
class Foo2(object):
19+
def __init__(self):
20+
super().__init__(self)
21+
22+
class Bar1(object):
23+
def __new__(cls):
24+
object.__new__(cls)
25+
26+
class Bar2(object):
27+
def __new__(cls):
28+
super().__new__(cls)
29+
30+
class Base:
31+
@classmethod
32+
def get_first_subclass(cls):
33+
for subklass in cls.__subclasses__():
34+
return subklass
35+
return object
36+
37+
class PluginBase(object):
38+
subclasses = []
39+
40+
def __init_subclass__(cls, **kwargs):
41+
super().__init_subclass__(**kwargs)
42+
cls.subclasses.append(cls)
43+
44+
# Test no lint raised for attributes.
45+
my_instance_name = x.__class__.__name__
46+
my_pkg_version = pkg.__version__
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
unnecessary-dunder-call:6:10:6:28::Unnecessarily calls dunder method __str__. Use str built-in function.:HIGH
2+
unnecessary-dunder-call:7:11:7:30::Unnecessarily calls dunder method __add__. Use + operator.:HIGH
3+
unnecessary-dunder-call:8:10:8:40::Unnecessarily calls dunder method __repr__. Use repr built-in function.:HIGH

0 commit comments

Comments
 (0)