Skip to content

Commit 55c3bf3

Browse files
authored
add B042: exception class with no, or malformed, super().__init__() (#512)
* add B042, exception class with no super().__init__() * fix logic
1 parent c2ecd69 commit 55c3bf3

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

bugbear.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
613613
self.check_for_b903(node)
614614
self.check_for_b021(node)
615615
self.check_for_b024_and_b027(node)
616+
self.check_for_b042(node)
616617
self.generic_visit(node)
617618

618619
def visit_Try(self, node) -> None:
@@ -1721,6 +1722,70 @@ def check(num_args: int, param_name: str) -> None:
17211722
elif func.attr == "split":
17221723
check(2, "maxsplit")
17231724

1725+
def check_for_b042(self, node: ast.ClassDef) -> None: # noqa: C901 # too-complex
1726+
def is_exception(s: str):
1727+
for ending in "Exception", "Error", "Warning", "ExceptionGroup":
1728+
if s.endswith(ending):
1729+
return True
1730+
return False
1731+
1732+
# A class must inherit from a super class to be an exception, and we also
1733+
# require the class name or any of the base names to look like an exception name.
1734+
if not (is_exception(node.name) and node.bases):
1735+
for base in node.bases:
1736+
if isinstance(base, ast.Name) and is_exception(base.id):
1737+
break
1738+
else:
1739+
return
1740+
1741+
# iterate body nodes looking for __init__
1742+
for fun in node.body:
1743+
if not (isinstance(fun, ast.FunctionDef) and fun.name == "__init__"):
1744+
continue
1745+
if fun.args.kwonlyargs or fun.args.kwarg:
1746+
# kwargs cannot be passed to super().__init__()
1747+
self.add_error("B042", fun)
1748+
return
1749+
# -1 to exclude the `self` argument
1750+
expected_arg_count = (
1751+
len(fun.args.posonlyargs)
1752+
+ len(fun.args.args)
1753+
- 1
1754+
+ (1 if fun.args.vararg else 0)
1755+
)
1756+
if expected_arg_count == 0:
1757+
# no arguments, don't need to call super().__init__()
1758+
return
1759+
1760+
# Look for super().__init__()
1761+
# We only check top-level nodes instead of doing an `ast.walk`.
1762+
# Small risk of false alarm if the user does something weird.
1763+
for b in fun.body:
1764+
if (
1765+
isinstance(b, ast.Expr)
1766+
and isinstance(b.value, ast.Call)
1767+
and isinstance(b.value.func, ast.Attribute)
1768+
and isinstance(b.value.func.value, ast.Call)
1769+
and isinstance(b.value.func.value.func, ast.Name)
1770+
and b.value.func.value.func.id == "super"
1771+
and b.value.func.attr == "__init__"
1772+
):
1773+
if len(b.value.args) != expected_arg_count:
1774+
self.add_error("B042", fun)
1775+
elif fun.args.vararg:
1776+
for arg in b.value.args:
1777+
if isinstance(arg, ast.Starred):
1778+
return
1779+
else:
1780+
# no Starred argument despite vararg
1781+
self.add_error("B042", fun)
1782+
return
1783+
else:
1784+
# no super().__init__() found
1785+
self.add_error("B042", fun)
1786+
return
1787+
# no `def __init__` found, which is fine
1788+
17241789
def check_for_b909(self, node: ast.For) -> None:
17251790
if isinstance(node.iter, ast.Name):
17261791
name = _to_name_str(node.iter)
@@ -2332,6 +2397,13 @@ def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> erro
23322397
message="B040 Exception with added note not used. Did you forget to raise it?"
23332398
),
23342399
"B041": Error(message=("B041 Repeated key-value pair in dictionary literal.")),
2400+
"B042": Error(
2401+
message=(
2402+
"B042 Exception class with `__init__` should pass all args to "
2403+
"`super().__init__()` in order to work with `copy.copy()`. "
2404+
"It should also not take any kwargs."
2405+
)
2406+
),
23352407
# Warnings disabled by default.
23362408
"B901": Error(
23372409
message=(

tests/eval_files/b042.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
class MyError_no_args(Exception):
2+
def __init__(self): # safe
3+
...
4+
5+
6+
class MyError_args_good(Exception):
7+
def __init__(self, foo, bar=3):
8+
super().__init__(foo, bar)
9+
10+
11+
class MyError_args_bad(Exception):
12+
def __init__(self, foo, bar=3): # B042: 4
13+
super().__init__(foo)
14+
15+
16+
class MyError_kwonlyargs(Exception):
17+
def __init__(self, *, foo): # B042: 4
18+
super().__init__(foo=foo)
19+
20+
21+
class MyError_kwargs(Exception):
22+
def __init__(self, **kwargs): # B042: 4
23+
super().__init__(**kwargs)
24+
25+
26+
class MyError_vararg_good(Exception):
27+
def __init__(self, *args): # safe
28+
super().__init__(*args)
29+
30+
31+
class MyError_vararg_bad(Exception):
32+
def __init__(self, *args): # B042: 4
33+
super().__init__()
34+
35+
36+
class MyError_args_nothing(Exception):
37+
def __init__(self, *args): ... # B042: 4
38+
39+
40+
class MyError_nested_init(Exception):
41+
def __init__(self, x): # B042: 4
42+
if True:
43+
super().__init__(x)
44+
45+
class MyError_posonlyargs(Exception):
46+
def __init__(self, x, /, y):
47+
super().__init__(x, y)
48+
49+
# triggers if class name ends with, or
50+
# if it inherits from a class whose name ends with, any of
51+
# 'Error', 'Exception', 'ExceptionGroup', 'Warning', 'ExceptionGroup'
52+
class Anything(ValueError):
53+
def __init__(self, x): ... # B042: 4
54+
class Anything2(BaseException):
55+
def __init__(self, x): ... # B042: 4
56+
class Anything3(ExceptionGroup):
57+
def __init__(self, x): ... # B042: 4
58+
class Anything4(UserWarning):
59+
def __init__(self, x): ... # B042: 4
60+
61+
class MyError(Anything):
62+
def __init__(self, x): ... # B042: 4
63+
class MyException(Anything):
64+
def __init__(self, x): ... # B042: 4
65+
class MyExceptionGroup(Anything):
66+
def __init__(self, x): ... # B042: 4
67+
class MyWarning(Anything):
68+
def __init__(self, x): ... # B042: 4
69+
70+
class ExceptionHandler(Anything):
71+
def __init__(self, x): ... # safe
72+
73+
class FooException:
74+
def __init__(self, x): ...

0 commit comments

Comments
 (0)