|
1 | 1 | import ast |
| 2 | +import builtins |
2 | 3 | from collections import namedtuple |
3 | 4 | from contextlib import suppress |
4 | 5 | from functools import lru_cache, partial |
@@ -136,7 +137,50 @@ def visit(self, node): |
136 | 137 |
|
137 | 138 | def visit_ExceptHandler(self, node): |
138 | 139 | if node.type is None: |
139 | | - self.errors.append(B001(node.lineno, node.col_offset)) |
| 140 | + self.errors.append( |
| 141 | + B001(node.lineno, node.col_offset, vars=("bare `except:`",)) |
| 142 | + ) |
| 143 | + elif isinstance(node.type, ast.Tuple): |
| 144 | + names = [] |
| 145 | + for e in node.type.elts: |
| 146 | + if isinstance(e, ast.Name): |
| 147 | + names.append(e.id) |
| 148 | + else: |
| 149 | + assert isinstance(e, ast.Attribute) |
| 150 | + names.append("{}.{}".format(e.value.id, e.attr)) |
| 151 | + as_ = " as " + node.name if node.name is not None else "" |
| 152 | + if len(names) == 0: |
| 153 | + vs = ("`except (){}:`".format(as_),) |
| 154 | + self.errors.append(B001(node.lineno, node.col_offset, vars=vs)) |
| 155 | + elif len(names) == 1: |
| 156 | + self.errors.append(B013(node.lineno, node.col_offset, vars=names)) |
| 157 | + else: |
| 158 | + # See if any of the given exception names could be removed, e.g. from: |
| 159 | + # (MyError, MyError) # duplicate names |
| 160 | + # (MyError, BaseException) # everything derives from the Base |
| 161 | + # (Exception, TypeError) # builtins where one subclasses another |
| 162 | + # but note that other cases are impractical to hande from the AST. |
| 163 | + # We expect this is mostly useful for users who do not have the |
| 164 | + # builtin exception hierarchy memorised, and include a 'shadowed' |
| 165 | + # subtype without realising that it's redundant. |
| 166 | + good = sorted(set(names), key=names.index) |
| 167 | + if "BaseException" in good: |
| 168 | + good = ["BaseException"] |
| 169 | + for name, other in itertools.permutations(tuple(good), 2): |
| 170 | + if issubclass( |
| 171 | + getattr(builtins, name, type), getattr(builtins, other, ()) |
| 172 | + ): |
| 173 | + if name in good: |
| 174 | + good.remove(name) |
| 175 | + if good != names: |
| 176 | + desc = good[0] if len(good) == 1 else "({})".format(", ".join(good)) |
| 177 | + self.errors.append( |
| 178 | + B014( |
| 179 | + node.lineno, |
| 180 | + node.col_offset, |
| 181 | + vars=(", ".join(names), as_, desc), |
| 182 | + ) |
| 183 | + ) |
140 | 184 | self.generic_visit(node) |
141 | 185 |
|
142 | 186 | def visit_UAdd(self, node): |
@@ -459,7 +503,7 @@ def visit(self, node): |
459 | 503 |
|
460 | 504 |
|
461 | 505 | B001 = Error( |
462 | | - message="B001 Do not use bare `except:`, it also catches unexpected " |
| 506 | + message="B001 Do not use {}, it also catches unexpected " |
463 | 507 | "events like memory errors, interrupts, system exit, and so on. " |
464 | 508 | "Prefer `except Exception:`. If you're sure what you're doing, " |
465 | 509 | "be explicit and write `except BaseException:`." |
@@ -542,6 +586,14 @@ def visit(self, node): |
542 | 586 | "to be silenced. Exceptions should be silenced in except blocks. Control " |
543 | 587 | "statements can be moved outside the finally block." |
544 | 588 | ) |
| 589 | +B013 = Error( |
| 590 | + message="B013 A length-one tuple literal is redundant. " |
| 591 | + "Write `except {0}:` instead of `except ({0},):`." |
| 592 | +) |
| 593 | +B014 = Error( |
| 594 | + message="B014 Redundant exception types in `except ({0}){1}:`. " |
| 595 | + "Write `except {2}{1}:`, which catches exactly the same exceptions." |
| 596 | +) |
545 | 597 |
|
546 | 598 | # Those could be false positives but it's more dangerous to let them slip |
547 | 599 | # through if they're not. |
|
0 commit comments