@@ -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 = (
0 commit comments