Skip to content

Commit 0afdcce

Browse files
jakkdlcooperleeskurtmckee
authored
b042: ignore overloaded init, ignore if str+pickle dunder, improve README (#531)
* ignore overloaded init, accept exceptions with str+pickle dunder, improve README blurb * Update README.rst Co-authored-by: Kurt McKee <[email protected]> --------- Co-authored-by: Cooper Lees <[email protected]> Co-authored-by: Kurt McKee <[email protected]>
1 parent 2d2fd4d commit 0afdcce

File tree

3 files changed

+78
-3
lines changed

3 files changed

+78
-3
lines changed

README.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,12 @@ second usage. Save the result to a list if the result is needed multiple times.
292292

293293
.. _B042:
294294

295-
**B042**: Remember to call super().__init__() in custom exceptions initalizer.
295+
**B042**: Exception classes with a custom `__init__` should pass all args to `super().__init__()` to work correctly with `copy.copy` and `pickle`.
296+
Both `BaseException.__reduce__` and `BaseException.__str__` rely on the `args` attribute being set correctly, which is set in `BaseException.__new__` and `BaseException.__init__`.
297+
If you define `__init__` yourself without passing all arguments to `super().__init__` it is very easy to break pickling, especially if they pass keyword arguments which both
298+
`BaseException.__new__` and `BaseException.__init__` ignore. It's also important that `__init__` not accept any keyword-only parameters.
299+
Alternately you can define both `__str__` and `__reduce__` to bypass the need for correct handling of `args`.
300+
If you define `__str__/__reduce__` in super classes this check is unable to detect it, and we advise disabling it.
296301

297302
.. _B043:
298303

bugbear.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1756,10 +1756,39 @@ def is_exception(s: str):
17561756
else:
17571757
return
17581758

1759+
# if the user defines __str__ + a pickle dunder they're probably in the clear.
1760+
has_pickle_dunder = False
1761+
has_str = False
1762+
for fun in node.body:
1763+
if isinstance(fun, ast.FunctionDef) and fun.name in (
1764+
"__getnewargs_ex__",
1765+
"__getnewargs__",
1766+
"__getstate__",
1767+
"__setstate__",
1768+
"__reduce__",
1769+
"__reduce_ex__",
1770+
):
1771+
if has_str:
1772+
return
1773+
has_pickle_dunder = True
1774+
elif isinstance(fun, ast.FunctionDef) and fun.name == "__str__":
1775+
if has_pickle_dunder:
1776+
return
1777+
has_str = True
1778+
17591779
# iterate body nodes looking for __init__
17601780
for fun in node.body:
17611781
if not (isinstance(fun, ast.FunctionDef) and fun.name == "__init__"):
17621782
continue
1783+
if any(
1784+
(isinstance(decorator, ast.Name) and decorator.id == "overload")
1785+
or (
1786+
isinstance(decorator, ast.Attribute)
1787+
and decorator.attr == "overload"
1788+
)
1789+
for decorator in fun.decorator_list
1790+
):
1791+
continue
17631792
if fun.args.kwonlyargs or fun.args.kwarg:
17641793
# kwargs cannot be passed to super().__init__()
17651794
self.add_error("B042", fun)
@@ -2418,7 +2447,7 @@ def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> erro
24182447
"B042": Error(
24192448
message=(
24202449
"B042 Exception class with `__init__` should pass all args to "
2421-
"`super().__init__()` in order to work with `copy.copy()`. "
2450+
"`super().__init__()` to work in edge cases of `pickle` and `copy.copy()`. "
24222451
"It should also not take any kwargs."
24232452
)
24242453
),

tests/eval_files/b042.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import typing
2+
from typing import overload
3+
4+
15
class MyError_no_args(Exception):
26
def __init__(self): # safe
37
...
@@ -46,6 +50,25 @@ class MyError_posonlyargs(Exception):
4650
def __init__(self, x, /, y):
4751
super().__init__(x, y)
4852

53+
# ignore overloaded __init__
54+
class MyException(Exception):
55+
@overload
56+
def __init__(self, x: int): ...
57+
@overload
58+
def __init__(self, x: float): ...
59+
60+
def __init__(self, x):
61+
super().__init__(x)
62+
63+
class MyException2(Exception):
64+
@typing.overload
65+
def __init__(self, x: int): ...
66+
@typing.overload
67+
def __init__(self, x: float): ...
68+
69+
def __init__(self, x):
70+
super().__init__(x)
71+
4972
# triggers if class name ends with, or
5073
# if it inherits from a class whose name ends with, any of
5174
# 'Error', 'Exception', 'ExceptionGroup', 'Warning', 'ExceptionGroup'
@@ -70,5 +93,23 @@ def __init__(self, x): ... # B042: 4
7093
class ExceptionHandler(Anything):
7194
def __init__(self, x): ... # safe
7295

73-
class FooException:
96+
class FooException: # safe, doesn't inherit from anything
97+
def __init__(self, x): ...
98+
99+
### Ignore classes that define __str__ + any pickle dunder
100+
class HasReduceStr(Exception):
101+
def __reduce__(self): ...
102+
def __str__(self): ...
103+
def __init__(self, x): ...
104+
105+
class HasReduce(Exception):
106+
def __reduce__(self): ...
107+
def __init__(self, x): ... # B042: 4
108+
class HasStr(Exception):
109+
def __str__(self): ...
110+
def __init__(self, x): ... # B042: 4
111+
112+
class HasStrReduceEx(Exception):
113+
def __reduce_ex__(self, protocol): ...
114+
def __str__(self): ...
74115
def __init__(self, x): ...

0 commit comments

Comments
 (0)