@@ -189,13 +189,59 @@ def _is_identifier(arg):
189189 return re .match (r"^[A-Za-z_][A-Za-z0-9_]*$" , arg .s ) is not None
190190
191191
192+ def _flatten_excepthandler (node ):
193+ if isinstance (node , ast .Tuple ):
194+ for elt in node .elts :
195+ yield from _flatten_excepthandler (elt )
196+ else :
197+ yield node
198+
199+
200+ def _check_redundant_excepthandlers (names , node ):
201+ # See if any of the given exception names could be removed, e.g. from:
202+ # (MyError, MyError) # duplicate names
203+ # (MyError, BaseException) # everything derives from the Base
204+ # (Exception, TypeError) # builtins where one subclasses another
205+ # (IOError, OSError) # IOError is an alias of OSError since Python3.3
206+ # but note that other cases are impractical to handle from the AST.
207+ # We expect this is mostly useful for users who do not have the
208+ # builtin exception hierarchy memorised, and include a 'shadowed'
209+ # subtype without realising that it's redundant.
210+ good = sorted (set (names ), key = names .index )
211+ if "BaseException" in good :
212+ good = ["BaseException" ]
213+ # Remove redundant exceptions that the automatic system either handles
214+ # poorly (usually aliases) or can't be checked (e.g. it's not an
215+ # built-in exception).
216+ for primary , equivalents in B014 .redundant_exceptions .items ():
217+ if primary in good :
218+ good = [g for g in good if g not in equivalents ]
219+
220+ for name , other in itertools .permutations (tuple (good ), 2 ):
221+ if _typesafe_issubclass (
222+ getattr (builtins , name , type ), getattr (builtins , other , ())
223+ ):
224+ if name in good :
225+ good .remove (name )
226+ if good != names :
227+ desc = good [0 ] if len (good ) == 1 else "({})" .format (", " .join (good ))
228+ as_ = " as " + node .name if node .name is not None else ""
229+ return B014 (
230+ node .lineno ,
231+ node .col_offset ,
232+ vars = (", " .join (names ), as_ , desc ),
233+ )
234+ return None
235+
236+
192237def _to_name_str (node ):
193238 # Turn Name and Attribute nodes to strings, e.g "ValueError" or
194239 # "pkg.mod.error", handling any depth of attribute accesses.
195240 if isinstance (node , ast .Name ):
196241 return node .id
197242 if isinstance (node , ast .Call ):
198243 return _to_name_str (node .func )
244+ assert isinstance (node , ast .Attribute ), f"Unexpected node type: { type (node )} "
199245 try :
200246 return _to_name_str (node .value ) + "." + node .attr
201247 except AttributeError :
@@ -277,48 +323,27 @@ def visit(self, node):
277323 def visit_ExceptHandler (self , node ):
278324 if node .type is None :
279325 self .errors .append (B001 (node .lineno , node .col_offset ))
280- elif isinstance (node .type , ast .Tuple ):
281- names = [_to_name_str (e ) for e in node .type .elts ]
282- as_ = " as " + node .name if node .name is not None else ""
283- if len (names ) == 0 :
284- self .errors .append (B029 (node .lineno , node .col_offset ))
285- elif len (names ) == 1 :
286- self .errors .append (B013 (node .lineno , node .col_offset , vars = names ))
326+ self .generic_visit (node )
327+ return
328+ handlers = _flatten_excepthandler (node .type )
329+ good_handlers = []
330+ bad_handlers = []
331+ for handler in handlers :
332+ if isinstance (handler , (ast .Name , ast .Attribute )):
333+ good_handlers .append (handler )
287334 else :
288- # See if any of the given exception names could be removed, e.g. from:
289- # (MyError, MyError) # duplicate names
290- # (MyError, BaseException) # everything derives from the Base
291- # (Exception, TypeError) # builtins where one subclasses another
292- # (IOError, OSError) # IOError is an alias of OSError since Python3.3
293- # but note that other cases are impractical to handle from the AST.
294- # We expect this is mostly useful for users who do not have the
295- # builtin exception hierarchy memorised, and include a 'shadowed'
296- # subtype without realising that it's redundant.
297- good = sorted (set (names ), key = names .index )
298- if "BaseException" in good :
299- good = ["BaseException" ]
300- # Remove redundant exceptions that the automatic system either handles
301- # poorly (usually aliases) or can't be checked (e.g. it's not an
302- # built-in exception).
303- for primary , equivalents in B014 .redundant_exceptions .items ():
304- if primary in good :
305- good = [g for g in good if g not in equivalents ]
306-
307- for name , other in itertools .permutations (tuple (good ), 2 ):
308- if _typesafe_issubclass (
309- getattr (builtins , name , type ), getattr (builtins , other , ())
310- ):
311- if name in good :
312- good .remove (name )
313- if good != names :
314- desc = good [0 ] if len (good ) == 1 else "({})" .format (", " .join (good ))
315- self .errors .append (
316- B014 (
317- node .lineno ,
318- node .col_offset ,
319- vars = (", " .join (names ), as_ , desc ),
320- )
321- )
335+ bad_handlers .append (handler )
336+ if bad_handlers :
337+ self .errors .append (B030 (node .lineno , node .col_offset ))
338+ names = [_to_name_str (e ) for e in good_handlers ]
339+ if len (names ) == 0 and not bad_handlers :
340+ self .errors .append (B029 (node .lineno , node .col_offset ))
341+ elif len (names ) == 1 and not bad_handlers and isinstance (node .type , ast .Tuple ):
342+ self .errors .append (B013 (node .lineno , node .col_offset , vars = names ))
343+ else :
344+ maybe_error = _check_redundant_excepthandlers (names , node )
345+ if maybe_error is not None :
346+ self .errors .append (maybe_error )
322347 self .generic_visit (node )
323348
324349 def visit_UAdd (self , node ):
@@ -1533,6 +1558,7 @@ def visit_Lambda(self, node):
15331558 "anything. Add exceptions to handle."
15341559 )
15351560)
1561+ B030 = Error (message = "B030 Except handlers should only be names of exception classes" )
15361562
15371563# Warnings disabled by default.
15381564B901 = Error (
0 commit comments