@@ -189,13 +189,59 @@ def _is_identifier(arg):
189
189
return re .match (r"^[A-Za-z_][A-Za-z0-9_]*$" , arg .s ) is not None
190
190
191
191
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
+
192
237
def _to_name_str (node ):
193
238
# Turn Name and Attribute nodes to strings, e.g "ValueError" or
194
239
# "pkg.mod.error", handling any depth of attribute accesses.
195
240
if isinstance (node , ast .Name ):
196
241
return node .id
197
242
if isinstance (node , ast .Call ):
198
243
return _to_name_str (node .func )
244
+ assert isinstance (node , ast .Attribute ), f"Unexpected node type: { type (node )} "
199
245
try :
200
246
return _to_name_str (node .value ) + "." + node .attr
201
247
except AttributeError :
@@ -277,48 +323,27 @@ def visit(self, node):
277
323
def visit_ExceptHandler (self , node ):
278
324
if node .type is None :
279
325
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 )
287
334
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 )
322
347
self .generic_visit (node )
323
348
324
349
def visit_UAdd (self , node ):
@@ -1533,6 +1558,7 @@ def visit_Lambda(self, node):
1533
1558
"anything. Add exceptions to handle."
1534
1559
)
1535
1560
)
1561
+ B030 = Error (message = "B030 Except handlers should only be names of exception classes" )
1536
1562
1537
1563
# Warnings disabled by default.
1538
1564
B901 = Error (
0 commit comments