Skip to content

Commit 004fc74

Browse files
Add bad-dunder-name extension checker (#7642)
With a 'good-dunder-name' option Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent 5c7f980 commit 004fc74

File tree

10 files changed

+291
-106
lines changed

10 files changed

+291
-106
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class Apples:
2+
def _init_(self): # [bad-dunder-name]
3+
pass
4+
5+
def __hello__(self): # [bad-dunder-name]
6+
print("hello")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class Apples:
2+
def __init__(self):
3+
pass
4+
5+
def hello(self):
6+
print("hello")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[MAIN]
2+
load-plugins=pylint.extensions.dunder
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added ``bad-dunder-name`` extension check, which flags bad or misspelled dunder methods.
2+
You can use the ``good-dunder-names`` option to allow specific dunder names.
3+
4+
Closes #3038

pylint/checkers/dunder_methods.py

Lines changed: 3 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -10,122 +10,19 @@
1010

1111
from pylint.checkers import BaseChecker
1212
from pylint.checkers.utils import safe_infer
13+
from pylint.constants import DUNDER_METHODS
1314
from pylint.interfaces import HIGH
1415

1516
if TYPE_CHECKING:
1617
from pylint.lint import PyLinter
1718

1819

19-
DUNDER_METHODS: dict[tuple[int, int], dict[str, str]] = {
20-
(0, 0): {
21-
"__init__": "Instantiate class directly",
22-
"__del__": "Use del keyword",
23-
"__repr__": "Use repr built-in function",
24-
"__str__": "Use str built-in function",
25-
"__bytes__": "Use bytes built-in function",
26-
"__format__": "Use format built-in function, format string method, or f-string",
27-
"__lt__": "Use < operator",
28-
"__le__": "Use <= operator",
29-
"__eq__": "Use == operator",
30-
"__ne__": "Use != operator",
31-
"__gt__": "Use > operator",
32-
"__ge__": "Use >= operator",
33-
"__hash__": "Use hash built-in function",
34-
"__bool__": "Use bool built-in function",
35-
"__getattr__": "Access attribute directly or use getattr built-in function",
36-
"__getattribute__": "Access attribute directly or use getattr built-in function",
37-
"__setattr__": "Set attribute directly or use setattr built-in function",
38-
"__delattr__": "Use del keyword",
39-
"__dir__": "Use dir built-in function",
40-
"__get__": "Use get method",
41-
"__set__": "Use set method",
42-
"__delete__": "Use del keyword",
43-
"__instancecheck__": "Use isinstance built-in function",
44-
"__subclasscheck__": "Use issubclass built-in function",
45-
"__call__": "Invoke instance directly",
46-
"__len__": "Use len built-in function",
47-
"__length_hint__": "Use length_hint method",
48-
"__getitem__": "Access item via subscript",
49-
"__setitem__": "Set item via subscript",
50-
"__delitem__": "Use del keyword",
51-
"__iter__": "Use iter built-in function",
52-
"__next__": "Use next built-in function",
53-
"__reversed__": "Use reversed built-in function",
54-
"__contains__": "Use in keyword",
55-
"__add__": "Use + operator",
56-
"__sub__": "Use - operator",
57-
"__mul__": "Use * operator",
58-
"__matmul__": "Use @ operator",
59-
"__truediv__": "Use / operator",
60-
"__floordiv__": "Use // operator",
61-
"__mod__": "Use % operator",
62-
"__divmod__": "Use divmod built-in function",
63-
"__pow__": "Use ** operator or pow built-in function",
64-
"__lshift__": "Use << operator",
65-
"__rshift__": "Use >> operator",
66-
"__and__": "Use & operator",
67-
"__xor__": "Use ^ operator",
68-
"__or__": "Use | operator",
69-
"__radd__": "Use + operator",
70-
"__rsub__": "Use - operator",
71-
"__rmul__": "Use * operator",
72-
"__rmatmul__": "Use @ operator",
73-
"__rtruediv__": "Use / operator",
74-
"__rfloordiv__": "Use // operator",
75-
"__rmod__": "Use % operator",
76-
"__rdivmod__": "Use divmod built-in function",
77-
"__rpow__": "Use ** operator or pow built-in function",
78-
"__rlshift__": "Use << operator",
79-
"__rrshift__": "Use >> operator",
80-
"__rand__": "Use & operator",
81-
"__rxor__": "Use ^ operator",
82-
"__ror__": "Use | operator",
83-
"__iadd__": "Use += operator",
84-
"__isub__": "Use -= operator",
85-
"__imul__": "Use *= operator",
86-
"__imatmul__": "Use @= operator",
87-
"__itruediv__": "Use /= operator",
88-
"__ifloordiv__": "Use //= operator",
89-
"__imod__": "Use %= operator",
90-
"__ipow__": "Use **= operator",
91-
"__ilshift__": "Use <<= operator",
92-
"__irshift__": "Use >>= operator",
93-
"__iand__": "Use &= operator",
94-
"__ixor__": "Use ^= operator",
95-
"__ior__": "Use |= operator",
96-
"__neg__": "Multiply by -1 instead",
97-
"__pos__": "Multiply by +1 instead",
98-
"__abs__": "Use abs built-in function",
99-
"__invert__": "Use ~ operator",
100-
"__complex__": "Use complex built-in function",
101-
"__int__": "Use int built-in function",
102-
"__float__": "Use float built-in function",
103-
"__round__": "Use round built-in function",
104-
"__trunc__": "Use math.trunc function",
105-
"__floor__": "Use math.floor function",
106-
"__ceil__": "Use math.ceil function",
107-
"__enter__": "Invoke context manager directly",
108-
"__aenter__": "Invoke context manager directly",
109-
"__copy__": "Use copy.copy function",
110-
"__deepcopy__": "Use copy.deepcopy function",
111-
"__fspath__": "Use os.fspath function instead",
112-
},
113-
(3, 10): {
114-
"__aiter__": "Use aiter built-in function",
115-
"__anext__": "Use anext built-in function",
116-
},
117-
}
118-
119-
12020
class DunderCallChecker(BaseChecker):
12121
"""Check for unnecessary dunder method calls.
12222
12323
Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization
124-
We exclude __new__, __subclasses__, __init_subclass__, __set_name__,
125-
__class_getitem__, __missing__, __exit__, __await__,
126-
__aexit__, __getnewargs_ex__, __getnewargs__, __getstate__,
127-
__setstate__, __reduce__, __reduce_ex__,
128-
and __index__ (see https://github.com/PyCQA/pylint/issues/6795)
24+
We exclude names in list pylint.constants.EXTRA_DUNDER_METHODS such as
25+
__index__ (see https://github.com/PyCQA/pylint/issues/6795)
12926
since these either have no alternative method of being called or
13027
have a genuine use case for being called manually.
13128

pylint/constants.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,133 @@ def _get_pylint_home() -> str:
168168
"typing_extensions.Never",
169169
)
170170
)
171+
172+
DUNDER_METHODS: dict[tuple[int, int], dict[str, str]] = {
173+
(0, 0): {
174+
"__init__": "Instantiate class directly",
175+
"__del__": "Use del keyword",
176+
"__repr__": "Use repr built-in function",
177+
"__str__": "Use str built-in function",
178+
"__bytes__": "Use bytes built-in function",
179+
"__format__": "Use format built-in function, format string method, or f-string",
180+
"__lt__": "Use < operator",
181+
"__le__": "Use <= operator",
182+
"__eq__": "Use == operator",
183+
"__ne__": "Use != operator",
184+
"__gt__": "Use > operator",
185+
"__ge__": "Use >= operator",
186+
"__hash__": "Use hash built-in function",
187+
"__bool__": "Use bool built-in function",
188+
"__getattr__": "Access attribute directly or use getattr built-in function",
189+
"__getattribute__": "Access attribute directly or use getattr built-in function",
190+
"__setattr__": "Set attribute directly or use setattr built-in function",
191+
"__delattr__": "Use del keyword",
192+
"__dir__": "Use dir built-in function",
193+
"__get__": "Use get method",
194+
"__set__": "Use set method",
195+
"__delete__": "Use del keyword",
196+
"__instancecheck__": "Use isinstance built-in function",
197+
"__subclasscheck__": "Use issubclass built-in function",
198+
"__call__": "Invoke instance directly",
199+
"__len__": "Use len built-in function",
200+
"__length_hint__": "Use length_hint method",
201+
"__getitem__": "Access item via subscript",
202+
"__setitem__": "Set item via subscript",
203+
"__delitem__": "Use del keyword",
204+
"__iter__": "Use iter built-in function",
205+
"__next__": "Use next built-in function",
206+
"__reversed__": "Use reversed built-in function",
207+
"__contains__": "Use in keyword",
208+
"__add__": "Use + operator",
209+
"__sub__": "Use - operator",
210+
"__mul__": "Use * operator",
211+
"__matmul__": "Use @ operator",
212+
"__truediv__": "Use / operator",
213+
"__floordiv__": "Use // operator",
214+
"__mod__": "Use % operator",
215+
"__divmod__": "Use divmod built-in function",
216+
"__pow__": "Use ** operator or pow built-in function",
217+
"__lshift__": "Use << operator",
218+
"__rshift__": "Use >> operator",
219+
"__and__": "Use & operator",
220+
"__xor__": "Use ^ operator",
221+
"__or__": "Use | operator",
222+
"__radd__": "Use + operator",
223+
"__rsub__": "Use - operator",
224+
"__rmul__": "Use * operator",
225+
"__rmatmul__": "Use @ operator",
226+
"__rtruediv__": "Use / operator",
227+
"__rfloordiv__": "Use // operator",
228+
"__rmod__": "Use % operator",
229+
"__rdivmod__": "Use divmod built-in function",
230+
"__rpow__": "Use ** operator or pow built-in function",
231+
"__rlshift__": "Use << operator",
232+
"__rrshift__": "Use >> operator",
233+
"__rand__": "Use & operator",
234+
"__rxor__": "Use ^ operator",
235+
"__ror__": "Use | operator",
236+
"__iadd__": "Use += operator",
237+
"__isub__": "Use -= operator",
238+
"__imul__": "Use *= operator",
239+
"__imatmul__": "Use @= operator",
240+
"__itruediv__": "Use /= operator",
241+
"__ifloordiv__": "Use //= operator",
242+
"__imod__": "Use %= operator",
243+
"__ipow__": "Use **= operator",
244+
"__ilshift__": "Use <<= operator",
245+
"__irshift__": "Use >>= operator",
246+
"__iand__": "Use &= operator",
247+
"__ixor__": "Use ^= operator",
248+
"__ior__": "Use |= operator",
249+
"__neg__": "Multiply by -1 instead",
250+
"__pos__": "Multiply by +1 instead",
251+
"__abs__": "Use abs built-in function",
252+
"__invert__": "Use ~ operator",
253+
"__complex__": "Use complex built-in function",
254+
"__int__": "Use int built-in function",
255+
"__float__": "Use float built-in function",
256+
"__round__": "Use round built-in function",
257+
"__trunc__": "Use math.trunc function",
258+
"__floor__": "Use math.floor function",
259+
"__ceil__": "Use math.ceil function",
260+
"__enter__": "Invoke context manager directly",
261+
"__aenter__": "Invoke context manager directly",
262+
"__copy__": "Use copy.copy function",
263+
"__deepcopy__": "Use copy.deepcopy function",
264+
"__fspath__": "Use os.fspath function instead",
265+
},
266+
(3, 10): {
267+
"__aiter__": "Use aiter built-in function",
268+
"__anext__": "Use anext built-in function",
269+
},
270+
}
271+
272+
EXTRA_DUNDER_METHODS = [
273+
"__new__",
274+
"__subclasses__",
275+
"__init_subclass__",
276+
"__set_name__",
277+
"__class_getitem__",
278+
"__missing__",
279+
"__exit__",
280+
"__await__",
281+
"__aexit__",
282+
"__getnewargs_ex__",
283+
"__getnewargs__",
284+
"__getstate__",
285+
"__setstate__",
286+
"__reduce__",
287+
"__reduce_ex__",
288+
"__post_init__", # part of `dataclasses` module
289+
]
290+
291+
DUNDER_PROPERTIES = [
292+
"__class__",
293+
"__dict__",
294+
"__doc__",
295+
"__format__",
296+
"__module__",
297+
"__sizeof__",
298+
"__subclasshook__",
299+
"__weakref__",
300+
]

pylint/extensions/dunder.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 __future__ import annotations
6+
7+
from typing import TYPE_CHECKING
8+
9+
from astroid import nodes
10+
11+
from pylint.checkers import BaseChecker
12+
from pylint.constants import DUNDER_METHODS, DUNDER_PROPERTIES, EXTRA_DUNDER_METHODS
13+
from pylint.interfaces import HIGH
14+
15+
if TYPE_CHECKING:
16+
from pylint.lint import PyLinter
17+
18+
19+
class DunderChecker(BaseChecker):
20+
"""Checks related to dunder methods."""
21+
22+
name = "dunder"
23+
priority = -1
24+
msgs = {
25+
"W3201": (
26+
"Bad or misspelled dunder method name %s.",
27+
"bad-dunder-name",
28+
"Used when a dunder method is misspelled or defined with a name "
29+
"not within the predefined list of dunder names.",
30+
),
31+
}
32+
options = (
33+
(
34+
"good-dunder-names",
35+
{
36+
"default": [],
37+
"type": "csv",
38+
"metavar": "<comma-separated names>",
39+
"help": "Good dunder names which should always be accepted.",
40+
},
41+
),
42+
)
43+
44+
def open(self) -> None:
45+
self._dunder_methods = (
46+
EXTRA_DUNDER_METHODS
47+
+ DUNDER_PROPERTIES
48+
+ self.linter.config.good_dunder_names
49+
)
50+
for since_vers, dunder_methods in DUNDER_METHODS.items():
51+
if since_vers <= self.linter.config.py_version:
52+
self._dunder_methods.extend(list(dunder_methods.keys()))
53+
54+
def visit_functiondef(self, node: nodes.FunctionDef) -> None:
55+
"""Check if known dunder method is misspelled or dunder name is not one
56+
of the pre-defined names.
57+
"""
58+
# ignore module-level functions
59+
if not node.is_method():
60+
return
61+
62+
# Detect something that could be a bad dunder method
63+
if (
64+
node.name.startswith("_")
65+
and node.name.endswith("_")
66+
and node.name not in self._dunder_methods
67+
):
68+
self.add_message(
69+
"bad-dunder-name",
70+
node=node,
71+
args=(node.name),
72+
confidence=HIGH,
73+
)
74+
75+
76+
def register(linter: PyLinter) -> None:
77+
linter.register_checker(DunderChecker(linter))

0 commit comments

Comments
 (0)