From 33f83590f29a13744f137d0208a26f3370e69b10 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 22 May 2025 16:38:33 +0200 Subject: [PATCH 1/7] revamp testing infra --- bugbear.py | 862 ++++++++++++++--------------- tests/b001.py | 9 +- tests/b002.py | 9 +- tests/b003.py | 2 +- tests/b004.py | 4 +- tests/b005.py | 50 +- tests/b006_b008.py | 36 +- tests/b007.py | 6 +- tests/b008_extended.py | 4 +- tests/b009_b010.py | 14 +- tests/b011.py | 4 +- tests/b012.py | 20 +- tests/b012_py311.py | 6 +- tests/b013.py | 4 +- tests/b013_py311.py | 4 +- tests/b014.py | 14 +- tests/b014_py311.py | 14 +- tests/b015.py | 8 +- tests/b016.py | 8 +- tests/b017.py | 14 +- tests/b018_classes.py | 28 +- tests/b018_functions.py | 28 +- tests/b018_modules.py | 24 +- tests/b018_nested.py | 24 +- tests/b019.py | 18 +- tests/b020.py | 8 +- tests/b021.py | 20 +- tests/b022.py | 2 +- tests/b023.py | 44 +- tests/b024.py | 14 +- tests/b025.py | 6 +- tests/b025_py311.py | 6 +- tests/b026.py | 14 +- tests/b027.py | 10 +- tests/b028.py | 4 +- tests/b029.py | 4 +- tests/b029_py311.py | 4 +- tests/b030.py | 6 +- tests/b030_py311.py | 6 +- tests/b031.py | 6 +- tests/b032.py | 16 +- tests/b033.py | 18 +- tests/b034.py | 18 +- tests/b035.py | 10 +- tests/b036.py | 12 +- tests/b036_py311.py | 12 +- tests/b037.py | 12 +- tests/b039.py | 10 +- tests/b040.py | 12 +- tests/b041.py | 14 +- tests/b901.py | 10 +- tests/b902.py | 20 +- tests/b902_extended.py | 34 +- tests/b902_py38.py | 20 +- tests/b903.py | 4 +- tests/b904.py | 8 +- tests/b904_py311.py | 8 +- tests/b905_py310.py | 16 +- tests/b906.py | 2 +- tests/b907.py | 114 ---- tests/b907_py312.py | 116 ++++ tests/b908.py | 16 +- tests/b909.py | 64 +-- tests/b910.py | 4 +- tests/b911_py313.py | 12 +- tests/b950.py | 12 +- tests/test_bugbear.py | 1144 +++------------------------------------ tox.ini | 2 +- 68 files changed, 1026 insertions(+), 2082 deletions(-) delete mode 100644 tests/b907.py create mode 100644 tests/b907_py312.py diff --git a/bugbear.py b/bugbear.py index 81d6137..49e658d 100644 --- a/bugbear.py +++ b/bugbear.py @@ -12,7 +12,7 @@ from contextlib import suppress from functools import lru_cache from keyword import iskeyword -from typing import Dict, Iterable, Iterator, List, Sequence, Set, Union, cast +from typing import Dict, Iterable, Iterator, List, Protocol, Sequence, Set, Union, cast import attr import pycodestyle # type: ignore[import-untyped] @@ -121,7 +121,9 @@ def gen_line_based_checks(self): if skip and not too_many_leading_white_spaces: continue - yield B950(lineno, length, vars=(length, self.max_line_length)) + yield error_codes["B950"]( + lineno, length, vars=(length, self.max_line_length) + ) @classmethod def adapt_error(cls, e: error) -> tuple[int, int, str, type]: @@ -274,7 +276,7 @@ def _check_redundant_excepthandlers( if good != names: desc = good[0] if len(good) == 1 else "({})".format(", ".join(good)) as_ = " as " + node.name if node.name is not None else "" - return B014( + return error_codes["B014"]( node.lineno, node.col_offset, vars=(", ".join(names), as_, desc, in_trystar), @@ -375,6 +377,11 @@ class B041VariableKeyType: name: str +class AstPositionNode(Protocol): + lineno: int + col_offset: int + + @attr.s class BugBearVisitor(ast.NodeVisitor): filename = attr.ib() @@ -387,7 +394,7 @@ class BugBearVisitor(ast.NodeVisitor): b040_caught_exception: B040CaughtException | None = attr.ib(default=None) NODE_WINDOW_SIZE = 4 - _b023_seen: set[error] = attr.ib(factory=set, init=False) + _b023_seen: set[ast.Name] = attr.ib(factory=set, init=False) _b005_imports: set[str] = attr.ib(factory=set, init=False) # set to "*" when inside a try/except*, for correctly printing errors @@ -400,6 +407,9 @@ def __getattr__(self, name: str): # type: ignore[unreachable] print(name) return self.__getattribute__(name) + def add_error(self, code: str, node: AstPositionNode, *vars: object) -> None: + self.errors.append(error_codes[code](node.lineno, node.col_offset, vars=vars)) + @property def node_stack(self): if len(self.contexts) == 0: @@ -419,17 +429,17 @@ def in_class_init(self) -> bool: def visit_Return(self, node: ast.Return) -> None: if self.in_class_init(): if node.value is not None: - self.errors.append(B037(node.lineno, node.col_offset)) + self.add_error("B037", node) self.generic_visit(node) def visit_Yield(self, node: ast.Yield) -> None: if self.in_class_init(): - self.errors.append(B037(node.lineno, node.col_offset)) + self.add_error("B037", node) self.generic_visit(node) def visit_YieldFrom(self, node: ast.YieldFrom) -> None: if self.in_class_init(): - self.errors.append(B037(node.lineno, node.col_offset)) + self.add_error("B037", node) self.generic_visit(node) def visit(self, node) -> None: @@ -452,7 +462,7 @@ def visit(self, node) -> None: def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None: if node.type is None: - self.errors.append(B001(node.lineno, node.col_offset)) + self.add_error("B001", node) self.generic_visit(node) return @@ -468,7 +478,7 @@ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None: "BaseException" in names and not ExceptBaseExceptionVisitor(node).re_raised() ): - self.errors.append(B036(node.lineno, node.col_offset)) + self.add_error("B036", node) self.generic_visit(node) @@ -476,14 +486,14 @@ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None: self.b040_caught_exception is not None and self.b040_caught_exception.has_note ): - self.errors.append(B040(node.lineno, node.col_offset)) + self.add_error("B040", node) self.b040_caught_exception = old_b040_caught_exception def visit_UAdd(self, node: ast.UAdd) -> None: trailing_nodes = list(map(type, self.node_window[-4:])) if trailing_nodes == [ast.UnaryOp, ast.UAdd, ast.UnaryOp, ast.UAdd]: originator = cast(ast.UnaryOp, self.node_window[-4]) - self.errors.append(B002(originator.lineno, originator.col_offset)) + self.add_error("B002", originator) self.generic_visit(node) def visit_Call(self, node) -> None: @@ -497,14 +507,14 @@ def visit_Call(self, node) -> None: node.func.id in ("getattr", "hasattr") and node.args[1].value == "__call__" ): - self.errors.append(B004(node.lineno, node.col_offset)) + self.add_error("B004", node) if ( node.func.id == "getattr" and len(node.args) == 2 and _is_identifier(node.args[1]) and not iskeyword(node.args[1].value) ): - self.errors.append(B009(node.lineno, node.col_offset)) + self.add_error("B009", node) elif ( not any(isinstance(n, ast.Lambda) for n in self.node_stack) and node.func.id == "setattr" @@ -512,7 +522,7 @@ def visit_Call(self, node) -> None: and _is_identifier(node.args[1]) and not iskeyword(node.args[1].value) ): - self.errors.append(B010(node.lineno, node.col_offset)) + self.add_error("B010", node) self.check_for_b026(node) self.check_for_b028(node) @@ -545,7 +555,7 @@ def visit_Assign(self, node: ast.Assign) -> None: if isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name): if (t.value.id, t.attr) == ("os", "environ"): - self.errors.append(B003(node.lineno, node.col_offset)) + self.add_error("B003", node) self.generic_visit(node) def visit_For(self, node: ast.For) -> None: @@ -682,7 +692,7 @@ def convert_to_value(item): value = convert_to_value(node.values[index]) if value in seen: key_node = node.keys[index] - self.errors.append(B041(key_node.lineno, key_node.col_offset)) + self.add_error("B041", key_node) seen.add(value) def check_for_b005(self, node) -> None: @@ -716,11 +726,13 @@ def check_for_b005(self, node) -> None: if len(value) == len(set(value)): return # no characters appear more than once - self.errors.append(B005(node.lineno, node.col_offset)) + self.add_error("B005", node) def check_for_b006_and_b008(self, node) -> None: visitor = FunctionDefDefaultsVisitor( - B006, B008, self.b008_b039_extend_immutable_calls + error_codes["B006"], + error_codes["B008"], + self.b008_b039_extend_immutable_calls, ) visitor.visit(node.args.defaults + node.args.kw_defaults) self.errors.extend(visitor.errors) @@ -744,7 +756,9 @@ def check_for_b039(self, node: ast.Call) -> None: return visitor = FunctionDefDefaultsVisitor( - B039, B039, self.b008_b039_extend_immutable_calls + error_codes["B039"], + error_codes["B039"], + self.b008_b039_extend_immutable_calls, ) visitor.visit(kw.value) self.errors.extend(visitor.errors) @@ -759,11 +773,11 @@ def check_for_b007(self, node) -> None: used_names = set(body.names) for name in sorted(ctrl_names - used_names): n = targets.names[name][0] - self.errors.append(B007(n.lineno, n.col_offset, vars=(name,))) + self.add_error("B007", n, name) def check_for_b011(self, node) -> None: if isinstance(node.test, ast.Constant) and node.test.value is False: - self.errors.append(B011(node.lineno, node.col_offset)) + self.add_error("B011", node) def check_for_b012(self, node) -> None: def _loop(node, bad_node_types) -> None: @@ -774,9 +788,7 @@ def _loop(node, bad_node_types) -> None: bad_node_types = (ast.Return,) elif isinstance(node, bad_node_types): - self.errors.append( - B012(node.lineno, node.col_offset, vars=(self.in_trystar,)) - ) + self.add_error("B012", node, self.in_trystar) for child in ast.iter_child_nodes(node): _loop(child, bad_node_types) @@ -802,10 +814,12 @@ def check_for_b013_b014_b029_b030(self, node: ast.ExceptHandler) -> list[str]: else: bad_handlers.append(handler) if bad_handlers: - self.errors.append(B030(node.lineno, node.col_offset)) + self.add_error("B030", node) if len(names) == 0 and not bad_handlers and not ignored_handlers: - self.errors.append( - B029(node.lineno, node.col_offset, vars=(self.in_trystar,)) + self.add_error( + "B029", + node, + self.in_trystar, ) elif ( len(names) == 1 @@ -813,15 +827,11 @@ def check_for_b013_b014_b029_b030(self, node: ast.ExceptHandler) -> list[str]: and not ignored_handlers and isinstance(node.type, ast.Tuple) ): - self.errors.append( - B013( - node.lineno, - node.col_offset, - vars=( - *names, - self.in_trystar, - ), - ) + self.add_error( + "B013", + node, + *names, + self.in_trystar, ) else: maybe_error = _check_redundant_excepthandlers(names, node, self.in_trystar) @@ -831,7 +841,7 @@ def check_for_b013_b014_b029_b030(self, node: ast.ExceptHandler) -> list[str]: def check_for_b015(self, node) -> None: if isinstance(self.node_stack[-2], ast.Expr): - self.errors.append(B015(node.lineno, node.col_offset)) + self.add_error("B015", node) def check_for_b016(self, node) -> None: if isinstance(node.exc, ast.JoinedStr) or ( @@ -841,7 +851,7 @@ def check_for_b016(self, node) -> None: or node.exc.value is None ) ): - self.errors.append(B016(node.lineno, node.col_offset)) + self.add_error("B016", node) def check_for_b017(self, node) -> None: """Checks for use of the evil syntax 'with assertRaises(Exception):' @@ -885,7 +895,7 @@ def check_for_b017(self, node) -> None: and item_context.args[0].id in {"Exception", "BaseException"} and not item.optional_vars ): - self.errors.append(B017(node.lineno, node.col_offset)) + self.add_error("B017", node) def check_for_b019(self, node) -> None: if ( @@ -905,12 +915,7 @@ def check_for_b019(self, node) -> None: return if decorator in B019_CACHES: - self.errors.append( - B019( - node.decorator_list[idx].lineno, - node.decorator_list[idx].col_offset, - ) - ) + self.add_error("B019", node.decorator_list[idx]) return def check_for_b020(self, node) -> None: @@ -925,7 +930,7 @@ def check_for_b020(self, node) -> None: for name in sorted(ctrl_names): if name in iterset_names: n = targets.names[name][0] - self.errors.append(B020(n.lineno, n.col_offset, vars=(name,))) + self.add_error("B020", n, name) def check_for_b023( # noqa: C901 self, @@ -1000,22 +1005,20 @@ def check_for_b023( # noqa: C901 for name in body_nodes: if isinstance(name, ast.Name) and name.id not in argnames: if isinstance(name.ctx, ast.Load): - errors.append( - B023(name.lineno, name.col_offset, vars=(name.id,)) - ) + errors.append(name) elif isinstance(name.ctx, ast.Store): argnames.add(name.id) for err in errors: - if err.vars[0] not in argnames and err not in self._b023_seen: + if err.id not in argnames and err not in self._b023_seen: self._b023_seen.add(err) # dedupe across nested loops suspicious_variables.append(err) if suspicious_variables: reassigned_in_loop = set(self._get_assigned_names(loop_node)) - for err in sorted(suspicious_variables): - if reassigned_in_loop.issuperset(err.vars): - self.errors.append(err) + for err in sorted(suspicious_variables, key=lambda n: n.id): + if err.id in reassigned_in_loop: + self.add_error("B023", err, err.id) def check_for_b024_and_b027(self, node: ast.ClassDef) -> None: # noqa: C901 """Check for inheritance from abstract classes in abc and lack of @@ -1092,12 +1095,10 @@ def is_str_or_ellipsis(node): and empty_body(stmt.body) and not any(map(is_overload, stmt.decorator_list)) ): - self.errors.append( - B027(stmt.lineno, stmt.col_offset, vars=(stmt.name,)) - ) + self.add_error("B027", stmt, stmt.name) if has_method and not has_abstract_method: - self.errors.append(B024(node.lineno, node.col_offset, vars=(node.name,))) + self.add_error("B024", node, node.name) def check_for_b026(self, call: ast.Call) -> None: if not call.keywords: @@ -1113,7 +1114,7 @@ def check_for_b026(self, call: ast.Call) -> None: first_keyword.lineno, first_keyword.col_offset, ): - self.errors.append(B026(starred.lineno, starred.col_offset)) + self.add_error("B026", starred) def check_for_b031(self, loop_node) -> None: # noqa: C901 """Check that `itertools.groupby` isn't iterated over more than once. @@ -1149,21 +1150,13 @@ def check_for_b031(self, loop_node) -> None: # noqa: C901 isinstance(nested_node, ast.Name) and nested_node.id == group_name ): - self.errors.append( - B031( - nested_node.lineno, - nested_node.col_offset, - vars=(nested_node.id,), - ) - ) + self.add_error("B031", nested_node, nested_node.id) # Handle multiple uses if isinstance(node, ast.Name) and node.id == group_name: num_usages += 1 if num_usages > 1: - self.errors.append( - B031(node.lineno, node.col_offset, vars=(node.id,)) - ) + self.add_error("B031", node, node.id) def _get_names_from_tuple(self, node: ast.Tuple): for dim in node.elts: @@ -1191,16 +1184,12 @@ def check_for_b035(self, node: ast.DictComp) -> None: or a variable that isn't coming from the generator expression. """ if isinstance(node.key, ast.Constant): - self.errors.append( - B035(node.key.lineno, node.key.col_offset, vars=(node.key.value,)) - ) + self.add_error("B035", node.key, node.key.value) elif isinstance(node.key, ast.Name): if node.key.id not in self._get_dict_comp_loop_and_named_expr_var_names( node ): - self.errors.append( - B035(node.key.lineno, node.key.col_offset, vars=(node.key.id,)) - ) + self.add_error("B035", node.key, node.key.id) def check_for_b040_add_note(self, node: ast.Attribute) -> bool: if ( @@ -1251,9 +1240,7 @@ def check_for_b904(self, node) -> None: and not (isinstance(node.exc, ast.Name) and node.exc.id.islower()) and any(isinstance(n, ast.ExceptHandler) for n in self.node_stack) ): - self.errors.append( - B904(node.lineno, node.col_offset, vars=(self.in_trystar,)) - ) + self.add_error("B904", node, self.in_trystar) def walk_function_body(self, node): def _loop(parent, node): @@ -1301,7 +1288,7 @@ def check_for_b901(self, node: ast.FunctionDef) -> None: return_node = x if has_yield and return_node is not None: - self.errors.append(B901(return_node.lineno, return_node.col_offset)) + self.add_error("B901", return_node) # taken from pep8-naming @classmethod @@ -1361,30 +1348,25 @@ def is_classmethod(decorators: Set[str]) -> bool: if args: actual_first_arg = args[0].arg - lineno = args[0].lineno - col = args[0].col_offset + err_node = args[0] elif vararg: actual_first_arg = "*" + vararg.arg - lineno = vararg.lineno - col = vararg.col_offset + err_node = vararg elif kwarg: actual_first_arg = "**" + kwarg.arg - lineno = kwarg.lineno - col = kwarg.col_offset + err_node = kwarg elif kwonlyargs: actual_first_arg = "*, " + kwonlyargs[0].arg - lineno = kwonlyargs[0].lineno - col = kwonlyargs[0].col_offset + err_node = kwonlyargs[0] else: actual_first_arg = "(none)" - lineno = node.lineno - col = node.col_offset + err_node = node if actual_first_arg not in expected_first_args: if not actual_first_arg.startswith(("(", "*")): actual_first_arg = repr(actual_first_arg) - self.errors.append( - B902(lineno, col, vars=(actual_first_arg, kind, expected_first_args[0])) + self.add_error( + "B902", err_node, actual_first_arg, kind, expected_first_args[0] ) def check_for_b903(self, node) -> None: @@ -1416,7 +1398,7 @@ def check_for_b903(self, node) -> None: if not isinstance(stmt.value, ast.Name): return - self.errors.append(B903(node.lineno, node.col_offset)) + self.add_error("B903", node) def check_for_b018(self, node) -> None: if not isinstance(node, ast.Expr): @@ -1436,13 +1418,7 @@ def check_for_b018(self, node) -> None: or node.value.value is None ) ): - self.errors.append( - B018( - node.lineno, - node.col_offset, - vars=(node.value.__class__.__name__,), - ) - ) + self.add_error("B018", node, node.value.__class__.__name__) def check_for_b021(self, node) -> None: if ( @@ -1450,9 +1426,7 @@ def check_for_b021(self, node) -> None: and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.JoinedStr) ): - self.errors.append( - B021(node.body[0].value.lineno, node.body[0].value.col_offset) - ) + self.add_error("B021", node.body[0].value) def check_for_b022(self, node) -> None: item = node.items[0] @@ -1466,7 +1440,7 @@ def check_for_b022(self, node) -> None: and item_context.func.attr == "suppress" and len(item_context.args) == 0 ): - self.errors.append(B022(node.lineno, node.col_offset)) + self.add_error("B022", node) @staticmethod def _is_assertRaises_like(node: ast.withitem) -> bool: @@ -1499,7 +1473,7 @@ def check_for_b908(self, node: ast.With) -> None: return for node_item in node.items: if self._is_assertRaises_like(node_item): - self.errors.append(B908(node.lineno, node.col_offset)) + self.add_error("B908", node) def check_for_b025(self, node) -> None: seen = [] @@ -1517,9 +1491,7 @@ def check_for_b025(self, node) -> None: # sort to have a deterministic output duplicates = sorted({x for x in seen if seen.count(x) > 1}) for duplicate in duplicates: - self.errors.append( - B025(node.lineno, node.col_offset, vars=(duplicate, self.in_trystar)) - ) + self.add_error("B025", node, duplicate, self.in_trystar) @staticmethod def _is_infinite_iterator(node: ast.expr) -> bool: @@ -1561,7 +1533,7 @@ def check_for_b905(self, node) -> None: if self._is_infinite_iterator(arg): return if not any(kw.arg == "strict" for kw in node.keywords): - self.errors.append(B905(node.lineno, node.col_offset)) + self.add_error("B905", node) def check_for_b906(self, node: ast.FunctionDef) -> None: if not node.name.startswith("visit_"): @@ -1609,7 +1581,7 @@ def check_for_b906(self, node: ast.FunctionDef) -> None: ): break else: - self.errors.append(B906(node.lineno, node.col_offset)) + self.add_error("B906", node) def check_for_b907(self, node: ast.JoinedStr) -> None: # noqa: C901 quote_marks = "'\"" @@ -1626,13 +1598,7 @@ def check_for_b907(self, node: ast.JoinedStr) -> None: # noqa: C901 and variable is not None and value.value[0] == current_mark ): - self.errors.append( - B907( - variable.lineno, - variable.col_offset, - vars=(ast.unparse(variable.value),), - ) - ) + self.add_error("B907", variable, ast.unparse(variable.value)) current_mark = variable = None # don't continue with length>1, so we can detect a new pre-mark # in the same string as a post-mark, e.g. `"{foo}" "{bar}"` @@ -1711,7 +1677,7 @@ def check_for_b028(self, node) -> None: and not any(isinstance(a, ast.Starred) for a in node.args) and not any(kw.arg is None for kw in node.keywords) ): - self.errors.append(B028(node.lineno, node.col_offset)) + self.add_error("B028", node) def check_for_b032(self, node) -> None: if ( @@ -1726,7 +1692,7 @@ def check_for_b032(self, node) -> None: ) ) ): - self.errors.append(B032(node.lineno, node.col_offset)) + self.add_error("B032", node) def check_for_b033(self, node) -> None: seen = set() @@ -1734,9 +1700,7 @@ def check_for_b033(self, node) -> None: if not isinstance(elt, ast.Constant): continue if elt.value in seen: - self.errors.append( - B033(elt.lineno, elt.col_offset, vars=(repr(elt.value),)) - ) + self.add_error("B033", elt, repr(elt.value)) else: seen.add(elt.value) @@ -1750,9 +1714,7 @@ def check_for_b034(self, node: ast.Call) -> None: def check(num_args: int, param_name: str) -> None: if len(node.args) > num_args: arg = node.args[num_args] - self.errors.append( - B034(arg.lineno, arg.col_offset, vars=(func.attr, param_name)) - ) + self.add_error("B034", arg, func.attr, param_name) if func.attr in ("sub", "subn"): check(3, "count") @@ -1773,7 +1735,7 @@ def check_for_b909(self, node: ast.For) -> None: for mutation in itertools.chain.from_iterable( m for m in checker.mutations.values() ): - self.errors.append(B909(mutation.lineno, mutation.col_offset)) + self.add_error("B909", mutation) def check_for_b910(self, node: ast.Call) -> None: if ( @@ -1783,7 +1745,7 @@ def check_for_b910(self, node: ast.Call) -> None: and isinstance(node.args[0], ast.Name) and node.args[0].id == "int" ): - self.errors.append(B910(node.lineno, node.col_offset)) + self.add_error("B910", node) def check_for_b911(self, node: ast.Call) -> None: if ( @@ -1795,7 +1757,7 @@ def check_for_b911(self, node: ast.Call) -> None: and node.func.value.id == "itertools" ) ) and not any(kw.arg == "strict" for kw in node.keywords): - self.errors.append(B911(node.lineno, node.col_offset)) + self.add_error("B911", node) def compose_call_path(node): @@ -2057,72 +2019,8 @@ def visit_Lambda(self, node) -> None: self.names.pop(lambda_arg.arg, None) -error = namedtuple("error", "lineno col message type vars") - - -class Error: - def __init__(self, message: str): - self.message = message - - def __call__(self, lineno: int, col: int, vars: tuple[str, ...] = ()) -> error: - return error(lineno, col, self.message, BugBearChecker, vars=vars) - - -# note: bare except* is a syntax error, so B001 does not need to handle it -B001 = Error( - message=( - "B001 Do not use bare `except:`, it also catches unexpected " - "events like memory errors, interrupts, system exit, and so on. " - "Prefer excepting specific exceptions If you're sure what you're " - "doing, be explicit and write `except BaseException:`." - ) -) - -B002 = Error( - message=( - "B002 Python does not support the unary prefix increment. Writing " - "++n is equivalent to +(+(n)), which equals n. You meant n += 1." - ) -) - -B003 = Error( - message=( - "B003 Assigning to `os.environ` doesn't clear the environment. " - "Subprocesses are going to see outdated variables, in disagreement " - "with the current process. Use `os.environ.clear()` or the `env=` " - "argument to Popen." - ) -) - -B004 = Error( - message=( - "B004 Using `hasattr(x, '__call__')` to test if `x` is callable " - "is unreliable. If `x` implements custom `__getattr__` or its " - "`__call__` is itself not callable, you might get misleading " - "results. Use `callable(x)` for consistent results." - ) -) - -B005 = Error( - message=( - "B005 Using .strip() with multi-character strings is misleading " - "the reader. It looks like stripping a substring. Move your " - "character set to a constant if this is deliberate. Use " - ".replace(), .removeprefix(), .removesuffix(), or regular " - "expressions to remove string fragments." - ) -) B005_METHODS = {"lstrip", "rstrip", "strip"} -B006 = Error( - message=( - "B006 Do not use mutable data structures for argument defaults. They " - "are created during function definition time. All calls to the function " - "reuse this one instance of that data structure, persisting changes " - "between them." - ) -) - # Note: these are also used by B039 B006_MUTABLE_LITERALS = ("Dict", "List", "Set") B006_MUTABLE_COMPREHENSIONS = ("ListComp", "DictComp", "SetComp") @@ -2139,22 +2037,6 @@ def __call__(self, lineno: int, col: int, vars: tuple[str, ...] = ()) -> error: "list", "set", } -B007 = Error( - message=( - "B007 Loop control variable {!r} not used within the loop body. " - "If this is intended, start the name with an underscore." - ) -) -B008 = Error( - message=( - "B008 Do not perform function calls in argument defaults. The call is " - "performed only once at function definition time. All calls to your " - "function will reuse the result of that definition-time function call. If " - "this is intended, assign the function call to a module-level variable and " - "use that variable as a default value." - ) -) - # Note: these are also used by B039 B008_IMMUTABLE_CALLS = { "tuple", @@ -2169,43 +2051,6 @@ def __call__(self, lineno: int, col: int, vars: tuple[str, ...] = ()) -> error: "itemgetter", "methodcaller", } -B009 = Error( - message=( - "B009 Do not call getattr with a constant attribute value, " - "it is not any safer than normal property access." - ) -) -B010 = Error( - message=( - "B010 Do not call setattr with a constant attribute value, " - "it is not any safer than normal property access." - ) -) -B011 = Error( - message=( - "B011 Do not call assert False since python -O removes these calls. " - "Instead callers should raise AssertionError()." - ) -) -B012 = Error( - message=( - "B012 return/continue/break inside finally blocks cause exceptions " - "to be silenced. Exceptions should be silenced in except{0} blocks. Control " - "statements can be moved outside the finally block." - ) -) -B013 = Error( - message=( - "B013 A length-one tuple literal is redundant. " - "Write `except{1} {0}:` instead of `except{1} ({0},):`." - ) -) -B014 = Error( - message=( - "B014 Redundant exception types in `except{3} ({0}){1}:`. " - "Write `except{3} {2}{1}:`, which catches exactly the same exceptions." - ) -) B014_REDUNDANT_EXCEPTIONS = { "OSError": { # All of these are actually aliases of OSError since Python 3.3 @@ -2220,237 +2065,336 @@ def __call__(self, lineno: int, col: int, vars: tuple[str, ...] = ()) -> error: "binascii.Error", }, } -B015 = Error( - message=( - "B015 Result of comparison is not used. This line doesn't do " - "anything. Did you intend to prepend it with assert?" - ) -) -B016 = Error( - message=( - "B016 Cannot raise a literal. Did you intend to return it or raise " - "an Exception?" - ) -) -B017 = Error( - message=( - "B017 `assertRaises(Exception)` and `pytest.raises(Exception)` should " - "be considered evil. They can lead to your test passing even if the " - "code being tested is never executed due to a typo. Assert for a more " - "specific exception (builtin or custom), or use `assertRaisesRegex` " - "(if using `assertRaises`), or add the `match` keyword argument (if " - "using `pytest.raises`), or use the context manager form with a target." - ) -) -B018 = Error( - message=( - "B018 Found useless {} expression. Consider either assigning it to a " - "variable or removing it." - ) -) -B019 = Error( - message=( - "B019 Use of `functools.lru_cache` or `functools.cache` on methods " - "can lead to memory leaks. The cache may retain instance references, " - "preventing garbage collection." - ) -) B019_CACHES = { "functools.cache", "functools.lru_cache", "cache", "lru_cache", } -B020 = Error( - message=( - "B020 Found for loop that reassigns the iterable it is iterating " - + "with each iterable value." - ) -) -B021 = Error( - message=( - "B021 f-string used as docstring. " - "This will be interpreted by python as a joined string rather than a docstring." - ) -) -B022 = Error( - message=( - "B022 No arguments passed to `contextlib.suppress`. " - "No exceptions will be suppressed and therefore this " - "context manager is redundant." - ) -) - -B023 = Error(message="B023 Function definition does not bind loop variable {!r}.") -B024 = Error( - message=( - "B024 {} is an abstract base class, but none of the methods it defines are" - " abstract. This is not necessarily an error, but you might have forgotten to" - " add the @abstractmethod decorator, potentially in conjunction with" - " @classmethod, @property and/or @staticmethod." - ) -) -B025 = Error( - message=( - "B025 Exception `{0}` has been caught multiple times. Only the first except{0}" - " will be considered and all other except{0} catches can be safely removed." - ) -) -B026 = Error( - message=( - "B026 Star-arg unpacking after a keyword argument is strongly discouraged, " - "because it only works when the keyword parameter is declared after all " - "parameters supplied by the unpacked sequence, and this change of ordering can " - "surprise and mislead readers." - ) -) -B027 = Error( - message=( - "B027 {} is an empty method in an abstract base class, but has no abstract" - " decorator. Consider adding @abstractmethod." - ) -) -B028 = Error( - message=( - "B028 No explicit stacklevel argument found. The warn method from the" - " warnings module uses a stacklevel of 1 by default. This will only show a" - " stack trace for the line on which the warn method is called." - " It is therefore recommended to use a stacklevel of 2 or" - " greater to provide more information to the user." - ) -) -B029 = Error( - message=( - "B029 Using `except{0} ():` with an empty tuple does not handle/catch " - "anything. Add exceptions to handle." - ) -) - -B030 = Error(message="B030 Except handlers should only be names of exception classes") - -B031 = Error( - message=( - "B031 Using the generator returned from `itertools.groupby()` more than once" - " will do nothing on the second usage. Save the result to a list, if the" - " result is needed multiple times." - ) -) - -B032 = Error( - message=( - "B032 Possible unintentional type annotation (using `:`). Did you mean to" - " assign (using `=`)?" - ) -) - -B033 = Error( - message=( - "B033 Set should not contain duplicate item {}. Duplicate items will be" - " replaced with a single item at runtime." - ) -) - -B034 = Error( - message=( - "B034 {} should pass `{}` and `flags` as keyword arguments to avoid confusion" - " due to unintuitive argument positions." - ) -) -B035 = Error(message="B035 Static key in dict comprehension {!r}.") - -B036 = Error( - message="B036 Don't except `BaseException` unless you plan to re-raise it." -) - -B037 = Error( - message="B037 Class `__init__` methods must not return or yield any values." -) - -B039 = Error( - message=( - "B039 ContextVar with mutable literal or function call as default. " - "This is only evaluated once, and all subsequent calls to `.get()` " - "will return the same instance of the default." - ) -) - -B040 = Error( - message="B040 Exception with added note not used. Did you forget to raise it?" -) - -B041 = Error(message=("B041 Repeated key-value pair in dictionary literal.")) - -# Warnings disabled by default. -B901 = Error( - message=( - "B901 Using `yield` together with `return x`. Use native " - "`async def` coroutines or put a `# noqa` comment on this " - "line if this was intentional." - ) -) -B902 = Error( - message=( - "B902 Invalid first argument {} used for {} method. Use the " - "canonical first argument name in methods, i.e. {}." - ) -) B902_IMPLICIT_CLASSMETHODS = {"__new__", "__init_subclass__", "__class_getitem__"} B902_SELF = ["self"] # it's a list because the first is preferred B902_CLS = ["cls", "klass"] # ditto. B902_METACLS = ["metacls", "metaclass", "typ", "mcs"] # ditto. -B903 = Error( - message=( - "B903 Data class should either be immutable or use __slots__ to " - "save memory. Use collections.namedtuple to generate an immutable " - "class, or enumerate the attributes in a __slot__ declaration in " - "the class to leave attributes mutable." - ) -) +error = namedtuple("error", "lineno col message type vars") -B904 = Error( - message=( - "B904 Within an `except{0}` clause, raise exceptions with `raise ... from err` or" - " `raise ... from None` to distinguish them from errors in exception handling. " - " See https://docs.python.org/3/tutorial/errors.html#exception-chaining for" - " details." - ) -) -B905 = Error(message="B905 `zip()` without an explicit `strict=` parameter.") +class Error: + def __init__(self, message: str): + self.message = message -B906 = Error( - message=( - "B906 `visit_` function with no further calls to a visit function, which might" - " prevent the `ast` visitor from properly visiting all nodes." - " Consider adding a call to `self.generic_visit(node)`." - ) -) + def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> error: + return error(lineno, col, self.message, BugBearChecker, vars=vars) -B907 = Error( - message=( - "B907 {!r} is manually surrounded by quotes, consider using the `!r` conversion" - " flag." - ) -) -B908 = Error( - message=( - "B908 assertRaises-type context should not contain more than one top-level" - " statement." - ) -) -B909 = Error( - message=( - "B909 editing a loop's mutable iterable often leads to unexpected results/bugs" - ) -) -B910 = Error( - message="B910 Use Counter() instead of defaultdict(int) to avoid excessive memory use" -) -B911 = Error( - message="B911 `itertools.batched()` without an explicit `strict=` parameter." -) -B950 = Error(message="B950 line too long ({} > {} characters)") + +error_codes = { + # note: bare except* is a syntax error, so B001 does not need to handle it + "B001": Error( + message=( + "B001 Do not use bare `except:`, it also catches unexpected " + "events like memory errors, interrupts, system exit, and so on. " + "Prefer excepting specific exceptions If you're sure what you're " + "doing, be explicit and write `except BaseException:`." + ) + ), + "B002": Error( + message=( + "B002 Python does not support the unary prefix increment. Writing " + "++n is equivalent to +(+(n)), which equals n. You meant n += 1." + ) + ), + "B003": Error( + message=( + "B003 Assigning to `os.environ` doesn't clear the environment. " + "Subprocesses are going to see outdated variables, in disagreement " + "with the current process. Use `os.environ.clear()` or the `env=` " + "argument to Popen." + ) + ), + "B004": Error( + message=( + "B004 Using `hasattr(x, '__call__')` to test if `x` is callable " + "is unreliable. If `x` implements custom `__getattr__` or its " + "`__call__` is itself not callable, you might get misleading " + "results. Use `callable(x)` for consistent results." + ) + ), + "B005": Error( + message=( + "B005 Using .strip() with multi-character strings is misleading " + "the reader. It looks like stripping a substring. Move your " + "character set to a constant if this is deliberate. Use " + ".replace(), .removeprefix(), .removesuffix(), or regular " + "expressions to remove string fragments." + ) + ), + "B006": Error( + message=( + "B006 Do not use mutable data structures for argument defaults. They " + "are created during function definition time. All calls to the function " + "reuse this one instance of that data structure, persisting changes " + "between them." + ) + ), + "B007": Error( + message=( + "B007 Loop control variable {!r} not used within the loop body. " + "If this is intended, start the name with an underscore." + ) + ), + "B008": Error( + message=( + "B008 Do not perform function calls in argument defaults. The call is " + "performed only once at function definition time. All calls to your " + "function will reuse the result of that definition-time function call. If " + "this is intended, assign the function call to a module-level variable and " + "use that variable as a default value." + ) + ), + "B009": Error( + message=( + "B009 Do not call getattr with a constant attribute value, " + "it is not any safer than normal property access." + ) + ), + "B010": Error( + message=( + "B010 Do not call setattr with a constant attribute value, " + "it is not any safer than normal property access." + ) + ), + "B011": Error( + message=( + "B011 Do not call assert False since python -O removes these calls. " + "Instead callers should raise AssertionError()." + ) + ), + "B012": Error( + message=( + "B012 return/continue/break inside finally blocks cause exceptions " + "to be silenced. Exceptions should be silenced in except{0} blocks. Control " + "statements can be moved outside the finally block." + ) + ), + "B013": Error( + message=( + "B013 A length-one tuple literal is redundant. " + "Write `except{1} {0}:` instead of `except{1} ({0},):`." + ) + ), + "B014": Error( + message=( + "B014 Redundant exception types in `except{3} ({0}){1}:`. " + "Write `except{3} {2}{1}:`, which catches exactly the same exceptions." + ) + ), + "B015": Error( + message=( + "B015 Result of comparison is not used. This line doesn't do " + "anything. Did you intend to prepend it with assert?" + ) + ), + "B016": Error( + message=( + "B016 Cannot raise a literal. Did you intend to return it or raise " + "an Exception?" + ) + ), + "B017": Error( + message=( + "B017 `assertRaises(Exception)` and `pytest.raises(Exception)` should " + "be considered evil. They can lead to your test passing even if the " + "code being tested is never executed due to a typo. Assert for a more " + "specific exception (builtin or custom), or use `assertRaisesRegex` " + "(if using `assertRaises`), or add the `match` keyword argument (if " + "using `pytest.raises`), or use the context manager form with a target." + ) + ), + "B018": Error( + message=( + "B018 Found useless {} expression. Consider either assigning it to a " + "variable or removing it." + ) + ), + "B019": Error( + message=( + "B019 Use of `functools.lru_cache` or `functools.cache` on methods " + "can lead to memory leaks. The cache may retain instance references, " + "preventing garbage collection." + ) + ), + "B020": Error( + message=( + "B020 Found for loop that reassigns the iterable it is iterating " + + "with each iterable value." + ) + ), + "B021": Error( + message=( + "B021 f-string used as docstring. " + "This will be interpreted by python as a joined string rather than a docstring." + ) + ), + "B022": Error( + message=( + "B022 No arguments passed to `contextlib.suppress`. " + "No exceptions will be suppressed and therefore this " + "context manager is redundant." + ) + ), + "B023": Error(message="B023 Function definition does not bind loop variable {!r}."), + "B024": Error( + message=( + "B024 {} is an abstract base class, but none of the methods it defines are" + " abstract. This is not necessarily an error, but you might have forgotten to" + " add the @abstractmethod decorator, potentially in conjunction with" + " @classmethod, @property and/or @staticmethod." + ) + ), + "B025": Error( + message=( + "B025 Exception `{0}` has been caught multiple times. Only the first except{0}" + " will be considered and all other except{0} catches can be safely removed." + ) + ), + "B026": Error( + message=( + "B026 Star-arg unpacking after a keyword argument is strongly discouraged, " + "because it only works when the keyword parameter is declared after all " + "parameters supplied by the unpacked sequence, and this change of ordering can " + "surprise and mislead readers." + ) + ), + "B027": Error( + message=( + "B027 {} is an empty method in an abstract base class, but has no abstract" + " decorator. Consider adding @abstractmethod." + ) + ), + "B028": Error( + message=( + "B028 No explicit stacklevel argument found. The warn method from the" + " warnings module uses a stacklevel of 1 by default. This will only show a" + " stack trace for the line on which the warn method is called." + " It is therefore recommended to use a stacklevel of 2 or" + " greater to provide more information to the user." + ) + ), + "B029": Error( + message=( + "B029 Using `except{0} ():` with an empty tuple does not handle/catch " + "anything. Add exceptions to handle." + ) + ), + "B030": Error( + message="B030 Except handlers should only be names of exception classes" + ), + "B031": Error( + message=( + "B031 Using the generator returned from `itertools.groupby()` more than once" + " will do nothing on the second usage. Save the result to a list, if the" + " result is needed multiple times." + ) + ), + "B032": Error( + message=( + "B032 Possible unintentional type annotation (using `:`). Did you mean to" + " assign (using `=`)?" + ) + ), + "B033": Error( + message=( + "B033 Set should not contain duplicate item {}. Duplicate items will be" + " replaced with a single item at runtime." + ) + ), + "B034": Error( + message=( + "B034 {} should pass `{}` and `flags` as keyword arguments to avoid confusion" + " due to unintuitive argument positions." + ) + ), + "B035": Error(message="B035 Static key in dict comprehension {!r}."), + "B036": Error( + message="B036 Don't except `BaseException` unless you plan to re-raise it." + ), + "B037": Error( + message="B037 Class `__init__` methods must not return or yield any values." + ), + "B039": Error( + message=( + "B039 ContextVar with mutable literal or function call as default. " + "This is only evaluated once, and all subsequent calls to `.get()` " + "will return the same instance of the default." + ) + ), + "B040": Error( + message="B040 Exception with added note not used. Did you forget to raise it?" + ), + "B041": Error(message=("B041 Repeated key-value pair in dictionary literal.")), + # Warnings disabled by default. + "B901": Error( + message=( + "B901 Using `yield` together with `return x`. Use native " + "`async def` coroutines or put a `# noqa` comment on this " + "line if this was intentional." + ) + ), + "B902": Error( + message=( + "B902 Invalid first argument {} used for {} method. Use the " + "canonical first argument name in methods, i.e. {}." + ) + ), + "B903": Error( + message=( + "B903 Data class should either be immutable or use __slots__ to " + "save memory. Use collections.namedtuple to generate an immutable " + "class, or enumerate the attributes in a __slot__ declaration in " + "the class to leave attributes mutable." + ) + ), + "B904": Error( + message=( + "B904 Within an `except{0}` clause, raise exceptions with `raise ... from err` or" + " `raise ... from None` to distinguish them from errors in exception handling. " + " See https://docs.python.org/3/tutorial/errors.html#exception-chaining for" + " details." + ) + ), + "B905": Error(message="B905 `zip()` without an explicit `strict=` parameter."), + "B906": Error( + message=( + "B906 `visit_` function with no further calls to a visit function, which might" + " prevent the `ast` visitor from properly visiting all nodes." + " Consider adding a call to `self.generic_visit(node)`." + ) + ), + "B907": Error( + message=( + "B907 {!r} is manually surrounded by quotes, consider using the `!r` conversion" + " flag." + ) + ), + "B908": Error( + message=( + "B908 assertRaises-type context should not contain more than one top-level" + " statement." + ) + ), + "B909": Error( + message=( + "B909 editing a loop's mutable iterable often leads to unexpected results/bugs" + ) + ), + "B910": Error( + message="B910 Use Counter() instead of defaultdict(int) to avoid excessive memory use" + ), + "B911": Error( + message="B911 `itertools.batched()` without an explicit `strict=` parameter." + ), + "B950": Error(message="B950 line too long ({} > {} characters)"), +} disabled_by_default = [ diff --git a/tests/b001.py b/tests/b001.py index dbd4407..1ac8525 100644 --- a/tests/b001.py +++ b/tests/b001.py @@ -1,11 +1,6 @@ -""" -Should emit: -B001 - on lines 8 and 40 -""" - try: import something -except: +except: # B001: 0 # should be except ImportError: import something_else as something @@ -37,6 +32,6 @@ def func(**kwargs): try: is_debug = kwargs["debug"] - except: + except: # B001: 4 # should be except KeyError: return diff --git a/tests/b002.py b/tests/b002.py index 718946f..dd7d56f 100644 --- a/tests/b002.py +++ b/tests/b002.py @@ -1,8 +1,3 @@ -""" -Should emit: -B002 - on lines 15 and 20 -""" - def this_is_all_fine(n): x = n + 1 y = 1 + n @@ -11,9 +6,9 @@ def this_is_all_fine(n): def this_is_buggy(n): - x = ++n + x = ++n # B002: 8 return x def this_is_buggy_too(n): - return ++n + return ++n # B002: 11 diff --git a/tests/b003.py b/tests/b003.py index 65bd137..b9f1b60 100644 --- a/tests/b003.py +++ b/tests/b003.py @@ -6,7 +6,7 @@ import os from os import environ -os.environ = {} +os.environ = {} # B003: 0 environ = {} # that's fine, assigning a new meaning to the module-level name diff --git a/tests/b004.py b/tests/b004.py index 6a6e8c8..77fdc71 100644 --- a/tests/b004.py +++ b/tests/b004.py @@ -1,8 +1,8 @@ def this_is_a_bug(): o = object() - if hasattr(o, "__call__"): + if hasattr(o, "__call__"): # B004: 7 print("Ooh, callable! Or is it?") - if getattr(o, "__call__", False): + if getattr(o, "__call__", False): # B004: 7 print("Ooh, callable! Or is it?") diff --git a/tests/b005.py b/tests/b005.py index 1fb63f5..798a088 100644 --- a/tests/b005.py +++ b/tests/b005.py @@ -1,33 +1,33 @@ s = "qwe" -s.strip(s) # no warning -s.strip("we") # no warning -s.strip(".facebook.com") # warning -s.strip("e") # no warning -s.strip("\n\t ") # no warning -s.strip(r"\n\t ") # warning -s.lstrip(s) # no warning -s.lstrip("we") # no warning -s.lstrip(".facebook.com") # warning -s.lstrip("e") # no warning -s.lstrip("\n\t ") # no warning -s.lstrip(r"\n\t ") # warning -s.rstrip(s) # no warning -s.rstrip("we") # warning -s.rstrip(".facebook.com") # warning -s.rstrip("e") # no warning -s.rstrip("\n\t ") # no warning -s.rstrip(r"\n\t ") # warning +s.strip(s) +s.strip("we") +s.strip(".facebook.com") # B005: 0 +s.strip("e") +s.strip("\n\t ") +s.strip(r"\n\t ") # B005: 0 +s.lstrip(s) +s.lstrip("we") +s.lstrip(".facebook.com") # B005: 0 +s.lstrip("e") +s.lstrip("\n\t ") +s.lstrip(r"\n\t ") # B005: 0 +s.rstrip(s) +s.rstrip("we") +s.rstrip(".facebook.com") # B005: 0 +s.rstrip("e") +s.rstrip("\n\t ") +s.rstrip(r"\n\t ") # B005: 0 from somewhere import other_type, strip -strip("we") # no warning -other_type().lstrip() # no warning -other_type().rstrip(["a", "b", "c"]) # no warning -other_type().strip("a", "b") # no warning +strip("we") +other_type().lstrip() +other_type().rstrip(["a", "b", "c"]) +other_type().strip("a", "b") import test, test2 # isort: skip import test_as as test3 -test.strip("test") # no warning -test2.strip("test") # no warning -test3.strip("test") # no warning +test.strip("test") +test2.strip("test") +test3.strip("test") diff --git a/tests/b006_b008.py b/tests/b006_b008.py index 4a4c302..71c673a 100644 --- a/tests/b006_b008.py +++ b/tests/b006_b008.py @@ -55,38 +55,38 @@ def kwonlyargs_immutable(*, value=()): ... # Flag mutable literals/comprehensions -def this_is_wrong(value=[1, 2, 3]): ... +def this_is_wrong(value=[1, 2, 3]): ... # B006: 24 -def this_is_also_wrong(value={}): ... +def this_is_also_wrong(value={}): ... # B006: 29 -def and_this(value=set()): ... +def and_this(value=set()): ... # B006: 19 -def this_too(value=collections.OrderedDict()): ... +def this_too(value=collections.OrderedDict()): ... # B006: 19 -async def async_this_too(value=collections.defaultdict()): ... +async def async_this_too(value=collections.defaultdict()): ... # B006: 31 -def dont_forget_me(value=collections.deque()): ... +def dont_forget_me(value=collections.deque()): ... # B006: 25 # N.B. we're also flagging the function call in the comprehension -def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): +def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): # B006: 45 # B008: 60 pass -def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}): +def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}): # B006: 45 # B008: 63 pass -def set_comprehension_also_not_okay(default={i**2 for i in range(3)}): +def set_comprehension_also_not_okay(default={i**2 for i in range(3)}): # B006: 44 # B008: 59 pass -def kwonlyargs_mutable(*, value=[]): ... +def kwonlyargs_mutable(*, value=[]): ... # B006: 32 # Recommended approach for mutable defaults @@ -97,14 +97,14 @@ def do_this_instead(value=None): # B008 # Flag function calls as default args (including if they are part of a sub-expression) -def in_fact_all_calls_are_wrong(value=time.time()): ... +def in_fact_all_calls_are_wrong(value=time.time()): ... # B008: 38 -def f(when=dt.datetime.now() + dt.timedelta(days=7)): +def f(when=dt.datetime.now() + dt.timedelta(days=7)): # B008: 11 # B008: 31 pass -def can_even_catch_lambdas(a=(lambda x: x)()): ... +def can_even_catch_lambdas(a=(lambda x: x)()): ... # B008: 29 # Recommended approach for function calls as default args @@ -146,28 +146,28 @@ def float_infinity_literal(value=float("1e999")): # But don't allow standard floats -def float_int_is_wrong(value=float(3)): +def float_int_is_wrong(value=float(3)): # B008: 29 pass -def float_str_not_inf_or_nan_is_wrong(value=float("3.14")): +def float_str_not_inf_or_nan_is_wrong(value=float("3.14")): # B008: 44 pass # B006 and B008 # We should handle arbitrary nesting of these B008. -def nested_combo(a=[float(3), dt.datetime.now()]): +def nested_combo(a=[float(3), dt.datetime.now()]): # B006: 19 # B008: 20 # B008: 30 pass # Don't flag nested B006 since we can't guarantee that # it isn't made mutable by the outer operation. -def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])): +def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])): # B008: 21 pass # B008-ception. -def nested_b008(a=random.randint(0, dt.datetime.now().year)): +def nested_b008(a=random.randint(0, dt.datetime.now().year)): # B008: 18 # B008: 36 pass diff --git a/tests/b007.py b/tests/b007.py index be623af..7c08b18 100644 --- a/tests/b007.py +++ b/tests/b007.py @@ -3,7 +3,7 @@ print(i) # name no longer defined on Python 3; no warning yet -for i in range(10): # name not used within the loop; B007 +for i in range(10): # name not used within the loop; B007 # B007: 4, "i" print(10) print(i) # name no longer defined on Python 3; no warning yet @@ -15,7 +15,7 @@ for i in range(10): for j in range(10): - for k in range(10): # k not used, i and j used transitively + for k in range(10): # k not used, i and j used transitively # B007: 12, "k" print(i + j) @@ -27,5 +27,5 @@ def strange_generator(): yield i, (j, (k, l)) -for i, (j, (k, l)) in strange_generator(): # i, k not used +for i, (j, (k, l)) in strange_generator(): # i, k not used # B007: 4, "i" # B007: 12, "k" print(j, l) diff --git a/tests/b008_extended.py b/tests/b008_extended.py index 484f68d..4f2c179 100644 --- a/tests/b008_extended.py +++ b/tests/b008_extended.py @@ -1,3 +1,4 @@ +# OPTIONS: extend_immutable_calls=["fastapi.Depends", "fastapi.Query"] from typing import List import fastapi @@ -10,4 +11,5 @@ def this_is_okay_extended(db=fastapi.Depends(get_db)): ... def this_is_okay_extended_second(data: List[str] = fastapi.Query(None)): ... -def this_is_not_okay_relative_import_not_listed(data: List[str] = Query(None)): ... +# not okay, relative import not listed +def not_okay(data: List[str] = Query(None)): ... # B008: 31 diff --git a/tests/b009_b010.py b/tests/b009_b010.py index e002b0f..1f18e41 100644 --- a/tests/b009_b010.py +++ b/tests/b009_b010.py @@ -14,9 +14,9 @@ getattr(foo, "except") # Invalid usage -getattr(foo, "bar") -getattr(foo, "_123abc") -getattr(foo, "abc123") +getattr(foo, "bar") # B009: 0 +getattr(foo, "_123abc") # B009: 0 +getattr(foo, "abc123") # B009: 0 # Valid setattr usage setattr(foo, bar, None) @@ -25,9 +25,9 @@ setattr(foo, "except", None) # Invalid usage -setattr(foo, "bar", None) -setattr(foo, "_123abc", None) -setattr(foo, "abc123", None) +setattr(foo, "bar", None) # B010: 0 +setattr(foo, "_123abc", None) # B010: 0 +setattr(foo, "abc123", None) # B010: 0 # Allow use of setattr within lambda expression # since assignment is not valid in this context. @@ -42,6 +42,6 @@ def __init__(self, has_setter): # getattr is still flagged within lambda though -c = lambda x: getattr(x, "some_attr") +c = lambda x: getattr(x, "some_attr") # B009: 14 # should be replaced with c = lambda x: x.some_attr diff --git a/tests/b011.py b/tests/b011.py index 3fa20eb..4d2703d 100644 --- a/tests/b011.py +++ b/tests/b011.py @@ -5,6 +5,6 @@ """ assert 1 != 2 -assert False +assert False # B011: 0, "i" assert 1 != 2, "message" -assert False, "message" +assert False, "message" # B011: 0, "k" diff --git a/tests/b012.py b/tests/b012.py index c03ff4e..a1971af 100644 --- a/tests/b012.py +++ b/tests/b012.py @@ -2,7 +2,7 @@ def a(): try: pass finally: - return # warning + return # warning # B012: 8, "" def b(): @@ -10,7 +10,7 @@ def b(): pass finally: if 1 + 0 == 2 - 1: - return # warning + return # warning # B012: 12, "" def c(): @@ -18,7 +18,7 @@ def c(): pass finally: try: - return # warning + return # warning # B012: 12, "" except Exception: pass @@ -28,7 +28,7 @@ def d(): try: pass finally: - return # warning + return # warning # B012: 12, "" finally: pass @@ -41,7 +41,7 @@ def f(): try: pass finally: - return # warning + return # warning # B012: 20, "" finally: pass @@ -63,7 +63,7 @@ def i(): try: pass finally: - break # warning + break # warning # B012: 12, "" def j(): while True: @@ -75,7 +75,7 @@ def h(): try: pass finally: - continue # warning + continue # warning # B012: 12, "" def j(): while True: @@ -91,17 +91,17 @@ def k(): while True: continue # no warning while True: - return # warning + return # warning # B012: 12, "" while True: try: pass finally: - continue # warning + continue # warning # B012: 8, "" while True: try: pass finally: - break # warning + break # warning # B012: 8, "" diff --git a/tests/b012_py311.py b/tests/b012_py311.py index 9e788fa..bb29f83 100644 --- a/tests/b012_py311.py +++ b/tests/b012_py311.py @@ -4,7 +4,7 @@ def a(): except* Exception: pass finally: - return # warning + return # warning # B012: 8, "*" def b(): @@ -14,7 +14,7 @@ def b(): pass finally: if 1 + 0 == 2 - 1: - return # warning + return # warning # B012: 12, "*" def c(): @@ -24,6 +24,6 @@ def c(): pass finally: try: - return # warning + return # warning # B012: 12, "*" except* Exception: pass diff --git a/tests/b013.py b/tests/b013.py index 17f314f..9a7fabc 100644 --- a/tests/b013.py +++ b/tests/b013.py @@ -7,7 +7,7 @@ try: pass -except (ValueError,): +except (ValueError,): # B013: 0, "ValueError", "" # pointless use of tuple pass @@ -29,7 +29,7 @@ try: pass -except (re.error,): +except (re.error,): # B013: 0, "re.error", "" # pointless use of tuple with dotted attribute pass diff --git a/tests/b013_py311.py b/tests/b013_py311.py index 27af470..52efaa4 100644 --- a/tests/b013_py311.py +++ b/tests/b013_py311.py @@ -7,7 +7,7 @@ try: pass -except* (ValueError,): +except* (ValueError,): # B013: 0, "ValueError", "*" # pointless use of tuple pass @@ -29,7 +29,7 @@ try: pass -except* (re.error,): +except* (re.error,): # B013: 0, "re.error", "*" # pointless use of tuple with dotted attribute pass diff --git a/tests/b014.py b/tests/b014.py index 3cb4570..9244f93 100644 --- a/tests/b014.py +++ b/tests/b014.py @@ -8,13 +8,13 @@ try: pass -except (Exception, TypeError): +except (Exception, TypeError): # B014: 0, "Exception, TypeError", "", "Exception", "" # TypeError is a subclass of Exception, so it doesn't add anything pass try: pass -except (OSError, OSError) as err: +except (OSError, OSError) as err: # B014: 0, "OSError, OSError", " as err", "OSError", "" # Duplicate exception types are useless pass @@ -25,7 +25,7 @@ class MyError(Exception): try: pass -except (MyError, MyError): +except (MyError, MyError): # B014: 0, "MyError, MyError", "", "MyError", "" # Detect duplicate non-builtin errors pass @@ -39,21 +39,21 @@ class MyError(Exception): try: pass -except (MyError, BaseException) as e: +except (MyError, BaseException) as e: # B014: 0, "MyError, BaseException", " as e", "BaseException", "" # But we *can* assume that everything is a subclass of BaseException raise e try: pass -except (re.error, re.error): +except (re.error, re.error): # B014: 0, "re.error, re.error", "", "re.error", "" # Duplicate exception types as attributes pass try: pass -except (IOError, EnvironmentError, OSError): +except (IOError, EnvironmentError, OSError): # B014: 0, "IOError, EnvironmentError, OSError", "", "OSError", "" # Detect if a primary exception and any its aliases are present. # # Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error, @@ -71,6 +71,6 @@ class MyError(Exception): try: pass -except (ValueError, binascii.Error): +except (ValueError, binascii.Error): # B014: 0, "ValueError, binascii.Error", "", "ValueError", "" # binascii.Error is a subclass of ValueError. pass diff --git a/tests/b014_py311.py b/tests/b014_py311.py index 2979a51..60792d9 100644 --- a/tests/b014_py311.py +++ b/tests/b014_py311.py @@ -8,13 +8,13 @@ try: pass -except* (Exception, TypeError): +except* (Exception, TypeError): # B014: 0, "Exception, TypeError", "", "Exception", "*" # TypeError is a subclass of Exception, so it doesn't add anything pass try: pass -except* (OSError, OSError) as err: +except* (OSError, OSError) as err: # B014: 0, "OSError, OSError", " as err", "OSError", "*" # Duplicate exception types are useless pass @@ -25,7 +25,7 @@ class MyError(Exception): try: pass -except* (MyError, MyError): +except* (MyError, MyError): # B014: 0, "MyError, MyError", "", "MyError", "*" # Detect duplicate non-builtin errors pass @@ -39,21 +39,21 @@ class MyError(Exception): try: pass -except* (MyError, BaseException) as e: +except* (MyError, BaseException) as e: # B014: 0, "MyError, BaseException", " as e", "BaseException", "*" # But we *can* assume that everything is a subclass of BaseException raise e try: pass -except* (re.error, re.error): +except* (re.error, re.error): # B014: 0, "re.error, re.error", "", "re.error", "*" # Duplicate exception types as attributes pass try: pass -except* (IOError, EnvironmentError, OSError): +except* (IOError, EnvironmentError, OSError): # B014: 0, "IOError, EnvironmentError, OSError", "", "OSError", "*" # Detect if a primary exception and any its aliases are present. # # Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error, @@ -71,6 +71,6 @@ class MyError(Exception): try: pass -except* (ValueError, binascii.Error): +except* (ValueError, binascii.Error): # B014: 0, "ValueError, binascii.Error", "", "ValueError", "*" # binascii.Error is a subclass of ValueError. pass diff --git a/tests/b015.py b/tests/b015.py index 0351c5c..b6d7d05 100644 --- a/tests/b015.py +++ b/tests/b015.py @@ -5,11 +5,11 @@ assert 1 == 1 -1 == 1 +1 == 1 # B015: 0 assert 1 in (1, 2) -1 in (1, 2) +1 in (1, 2) # B015: 0 if 1 == 2: @@ -19,11 +19,11 @@ def test(): assert 1 in (1, 2) - 1 in (1, 2) + 1 in (1, 2) # B015: 4 data = [x for x in [1, 2, 3] if x in (1, 2)] class TestClass: - 1 == 1 + 1 == 1 # B015: 4 diff --git a/tests/b016.py b/tests/b016.py index f12f468..d022cb9 100644 --- a/tests/b016.py +++ b/tests/b016.py @@ -3,11 +3,11 @@ B016 - on lines 6, 7, 8, and 10 """ -raise False -raise 1 -raise "string" +raise False # B016: 0 +raise 1 # B016: 0 +raise "string" # B016: 0 fstring = "fstring" -raise f"fstring {fstring}" +raise f"fstring {fstring}" # B016: 0 raise Exception(False) raise Exception(1) raise Exception("string") diff --git a/tests/b017.py b/tests/b017.py index 95f4e30..19b18b0 100644 --- a/tests/b017.py +++ b/tests/b017.py @@ -23,13 +23,13 @@ class Foo: class Foobar(unittest.TestCase): def evil_raises(self) -> None: - with self.assertRaises(Exception): + with self.assertRaises(Exception): # B017: 8 raise Exception("Evil I say!") - with self.assertRaises(Exception, msg="Generic exception"): + with self.assertRaises(Exception, msg="Generic exception"): # B017: 8 raise Exception("Evil I say!") - with pytest.raises(Exception): + with pytest.raises(Exception): # B017: 8 raise Exception("Evil I say!") - with raises(Exception): + with raises(Exception): # B017: 8 raise Exception("Evil I say!") # These are evil as well but we are only testing inside a with statement self.assertRaises(Exception, lambda x, y: x / y, 1, y=0) @@ -70,9 +70,9 @@ def raises_with_absolute_reference(self): Foo() def raises_base_exception(self): - with self.assertRaises(BaseException): + with self.assertRaises(BaseException): # B017: 8 Foo() - with pytest.raises(BaseException): + with pytest.raises(BaseException): # B017: 8 Foo() - with raises(BaseException): + with raises(BaseException): # B017: 8 Foo() diff --git a/tests/b018_classes.py b/tests/b018_classes.py index 8150a33..fcc41c6 100644 --- a/tests/b018_classes.py +++ b/tests/b018_classes.py @@ -13,23 +13,23 @@ class Foo2: a = 2 "str" # Str (no raise) f"{int}" # JoinedStr (no raise) - 1j # Number (complex) - 1 # Number (int) - 1.0 # Number (float) - b"foo" # Binary - True # NameConstant (True) - False # NameConstant (False) - None # NameConstant (None) - [1, 2] # list - {1, 2} # set - {"foo": "bar"} # dict + 1j # Number (complex) # B018: 4, "Constant" + 1 # Number (int) # B018: 4, "Constant" + 1.0 # Number (float) # B018: 4, "Constant" + b"foo" # Binary # B018: 4, "Constant" + True # NameConstant (True) # B018: 4, "Constant" + False # NameConstant (False) # B018: 4, "Constant" + None # NameConstant (None) # B018: 4, "Constant" + [1, 2] # list # B018: 4, "List" + {1, 2} # set # B018: 4, "Set" + {"foo": "bar"} # dict # B018: 4, "Dict" class Foo3: - 123 + 123 # B018: 4, "Constant" a = 2 "str" - 1 - (1,) # bad - (2, 3) # bad + 1 # B018: 4, "Constant" + (1,) # bad # B018: 4, "Tuple" + (2, 3) # bad # B018: 4, "Tuple" t = (4, 5) # good diff --git a/tests/b018_functions.py b/tests/b018_functions.py index c3e5440..5dc1c02 100644 --- a/tests/b018_functions.py +++ b/tests/b018_functions.py @@ -12,23 +12,23 @@ def foo2(): a = 2 "str" # Str (no raise) f"{int}" # JoinedStr (no raise) - 1j # Number (complex) - 1 # Number (int) - 1.0 # Number (float) - b"foo" # Binary - True # NameConstant (True) - False # NameConstant (False) - None # NameConstant (None) - [1, 2] # list - {1, 2} # set - {"foo": "bar"} # dict + 1j # Number (complex) # B018: 4, "Constant" + 1 # Number (int) # B018: 4, "Constant" + 1.0 # Number (float) # B018: 4, "Constant" + b"foo" # Binary # B018: 4, "Constant" + True # NameConstant (True) # B018: 4, "Constant" + False # NameConstant (False) # B018: 4, "Constant" + None # NameConstant (None) # B018: 4, "Constant" + [1, 2] # list # B018: 4, "List" + {1, 2} # set # B018: 4, "Set" + {"foo": "bar"} # dict # B018: 4, "Dict" def foo3(): - 123 + 123 # B018: 4, "Constant" a = 2 "str" - 3 - (1,) # bad - (2, 3) # bad + 3 # B018: 4, "Constant" + (1,) # bad # B018: 4, "Tuple" + (2, 3) # bad # B018: 4, "Tuple" t = (4, 5) # good diff --git a/tests/b018_modules.py b/tests/b018_modules.py index eb94008..d4d98e3 100644 --- a/tests/b018_modules.py +++ b/tests/b018_modules.py @@ -6,16 +6,16 @@ a = 2 "str" # Str (no raise) f"{int}" # JoinedStr (no raise) -1j # Number (complex) -1 # Number (int) -1.0 # Number (float) -b"foo" # Binary -True # NameConstant (True) -False # NameConstant (False) -None # NameConstant (None) -[1, 2] # list -{1, 2} # set -{"foo": "bar"} # dict -(1,) # bad -(2, 3) # bad +1j # Number (complex) # B018: 0, "Constant" +1 # Number (int) # B018: 0, "Constant" +1.0 # Number (float) # B018: 0, "Constant" +b"foo" # Binary # B018: 0, "Constant" +True # NameConstant (True) # B018: 0, "Constant" +False # NameConstant (False) # B018: 0, "Constant" +None # NameConstant (None) # B018: 0, "Constant" +[1, 2] # list # B018: 0, "List" +{1, 2} # set # B018: 0, "Set" +{"foo": "bar"} # dict # B018: 0, "Dict" +(1,) # bad # B018: 0, "Tuple" +(2, 3) # bad # B018: 0, "Tuple" t = (4, 5) # good diff --git a/tests/b018_nested.py b/tests/b018_nested.py index 3888d25..0d9171e 100644 --- a/tests/b018_nested.py +++ b/tests/b018_nested.py @@ -1,44 +1,44 @@ X = 1 -False # bad +False # bad # B018: 0, "Constant" def func(y): a = y + 1 - 5.5 # bad + 5.5 # bad # B018: 4, "Constant" return a class TestClass: GOOD = [1, 3] - [5, 6] # bad + [5, 6] # bad # B018: 4, "List" def method(self, xx, yy=5): t = (xx,) - (yy,) # bad + (yy,) # bad # B018: 8, "Tuple" while 1: i = 3 - 4 # bad + 4 # bad # B018: 12, "Constant" for n in range(i): j = 5 - 1.5 # bad + 1.5 # bad # B018: 16, "Constant" if j < n: u = {1, 2} - {4, 5} # bad + {4, 5} # bad # B018: 20, "Set" elif j == n: u = {1, 2, 3} - {4, 5, 6} # bad + {4, 5, 6} # bad # B018: 20, "Set" else: u = {2, 3} - {4, 6} # bad + {4, 6} # bad # B018: 20, "Set" try: - 1j # bad + 1j # bad # B018: 24, "Constant" r = 2j except Exception: r = 3j - 5 # bad + 5 # bad # B018: 24, "Constant" finally: - 4j # bad + 4j # bad # B018: 24, "Constant" r += 1 return u + t diff --git a/tests/b019.py b/tests/b019.py index 93042fc..445e445 100644 --- a/tests/b019.py +++ b/tests/b019.py @@ -58,26 +58,26 @@ def some_cached_property(self): ... def some_other_cached_property(self): ... # Remaining methods should emit B019 - @functools.cache + @functools.cache # B019: 5 def cached_method(self, y): ... - @cache + @cache # B019: 5 def another_cached_method(self, y): ... - @functools.cache() + @functools.cache() # B019: 5 def called_cached_method(self, y): ... - @cache() + @cache() # B019: 5 def another_called_cached_method(self, y): ... - @functools.lru_cache + @functools.lru_cache # B019: 5 def lru_cached_method(self, y): ... - @lru_cache + @lru_cache # B019: 5 def another_lru_cached_method(self, y): ... - @functools.lru_cache() + @functools.lru_cache() # B019: 5 def called_lru_cached_method(self, y): ... - @lru_cache() - def another_called_lru_cached_method(self, y): ... + @lru_cache() # B019: 5 + def another_called_lru_cached_method(self, y): ... \ No newline at end of file diff --git a/tests/b020.py b/tests/b020.py index 9bdc4b4..e6b39ea 100644 --- a/tests/b020.py +++ b/tests/b020.py @@ -5,7 +5,7 @@ items = [1, 2, 3] -for items in items: +for items in items: # B020: 4, "items" print(items) items = [1, 2, 3] @@ -18,7 +18,7 @@ for key, value in values.items(): print(f"{key}, {value}") -for key, values in values.items(): +for key, values in values.items(): # B020: 9, "values" print(f"{key}, {values}") # Variables defined in a comprehension are local in scope @@ -29,11 +29,11 @@ for var in (var for var in range(10)): print(var) -for k, v in {k: v for k, v in zip(range(10), range(10, 20))}.items(): +for k, v in {k: v for k, v in zip(range(10), range(10, 20))}.items(): # B905: 30 print(k, v) # However we still call out reassigning the iterable in the comprehension. -for vars in [i for i in vars]: +for vars in [i for i in vars]: # B020: 4, "vars" print(vars) for var in sorted(range(10), key=lambda var: var.real): diff --git a/tests/b021.py b/tests/b021.py index dd0bb63..dd9233a 100644 --- a/tests/b021.py +++ b/tests/b021.py @@ -11,7 +11,7 @@ def foo1(): def foo2(): - f"""hello {VARIABLE}!""" + f"""hello {VARIABLE}!""" # B021: 4 class bar1: @@ -19,7 +19,7 @@ class bar1: class bar2: - f"""hello {VARIABLE}!""" + f"""hello {VARIABLE}!""" # B021: 4 def foo1(): @@ -27,7 +27,7 @@ def foo1(): def foo2(): - f"""hello {VARIABLE}!""" + f"""hello {VARIABLE}!""" # B021: 4 class bar1: @@ -35,7 +35,7 @@ class bar1: class bar2: - f"""hello {VARIABLE}!""" + f"""hello {VARIABLE}!""" # B021: 4 def foo1(): @@ -43,7 +43,7 @@ def foo1(): def foo2(): - f"hello {VARIABLE}!" + f"hello {VARIABLE}!" # B021: 4 class bar1: @@ -51,7 +51,7 @@ class bar1: class bar2: - f"hello {VARIABLE}!" + f"hello {VARIABLE}!" # B021: 4 def foo1(): @@ -59,7 +59,7 @@ def foo1(): def foo2(): - f"hello {VARIABLE}!" + f"hello {VARIABLE}!" # B021: 4 class bar1: @@ -67,10 +67,10 @@ class bar1: class bar2: - f"hello {VARIABLE}!" + f"hello {VARIABLE}!" # B021: 4 def baz(): - f"""I'm probably a docstring: {VARIABLE}!""" + f"""I'm probably a docstring: {VARIABLE}!""" # B021: 4 print(f"""I'm a normal string""") - f"""Don't detect me!""" + f"""Don't detect me!""" \ No newline at end of file diff --git a/tests/b022.py b/tests/b022.py index 9d597ed..dbb45f5 100644 --- a/tests/b022.py +++ b/tests/b022.py @@ -5,7 +5,7 @@ import contextlib -with contextlib.suppress(): +with contextlib.suppress(): # B022: 0 raise ValueError with contextlib.suppress(ValueError): diff --git a/tests/b023.py b/tests/b023.py index bcbea9a..e4fff45 100644 --- a/tests/b023.py +++ b/tests/b023.py @@ -10,11 +10,11 @@ for x in range(3): y = x + 1 # Subject to late-binding problems - functions.append(lambda: x) - functions.append(lambda: y) # not just the loop var + functions.append(lambda: x) # B023: 29, "x" + functions.append(lambda: y) # not just the loop var # B023: 29, "y" def f_bad_1(): - return x + return x # B023: 15, "x" # Actually OK functions.append(lambda x: x * 2) @@ -26,10 +26,10 @@ def f_ok_1(x): def check_inside_functions_too(): - ls = [lambda: x for x in range(2)] # error - st = {lambda: x for x in range(2)} # error - gn = (lambda: x for x in range(2)) # error - dt = {x: lambda: x for x in range(2)} # error + ls = [lambda: x for x in range(2)] # error # B023: 18, "x" + st = {lambda: x for x in range(2)} # error # B023: 18, "x" + gn = (lambda: x for x in range(2)) # error # B023: 18, "x" + dt = {x: lambda: x for x in range(2)} # error # B023: 21, "x" async def pointless_async_iterable(): @@ -38,9 +38,9 @@ async def pointless_async_iterable(): async def container_for_problems(): async for x in pointless_async_iterable(): - functions.append(lambda: x) # error + functions.append(lambda: x) # error # B023: 33, "x" - [lambda: x async for x in pointless_async_iterable()] # error + [lambda: x async for x in pointless_async_iterable()] # error # B023: 13, "x" a = 10 @@ -48,10 +48,10 @@ async def container_for_problems(): while True: a = a_ = a - 1 b += 1 - functions.append(lambda: a) # error - functions.append(lambda: a_) # error - functions.append(lambda: b) # error - functions.append(lambda: c) # error, but not a name error due to late binding + functions.append(lambda: a) # error # B023: 29, "a" + functions.append(lambda: a_) # error # B023: 29, "a_" + functions.append(lambda: b) # error # B023: 29, "b" + functions.append(lambda: c) # error, but not a name error due to late binding # B023: 29, "c" c: bool = a > 3 if not c: break @@ -59,14 +59,14 @@ async def container_for_problems(): # Nested loops should not duplicate reports for j in range(2): for k in range(3): - lambda: j * k # error + lambda: j * k # error # B023: 16, "j" # B023: 20, "k" for j, k, l in [(1, 2, 3)]: def f(): j = None # OK because it's an assignment - [l for k in range(2)] # error for l, not for k + [l for k in range(2)] # error for l, not for k # B023: 9, "l" assert a and functions @@ -111,11 +111,11 @@ def myfunc(x): # argument or in a consumed `filter()` (even if a comprehension is better style) for x in range(2): # It's not a complete get-out-of-linting-free construct - these should fail: - min([None, lambda: x], key=repr) - sorted([None, lambda: x], key=repr) - any(filter(bool, [None, lambda: x])) - list(filter(bool, [None, lambda: x])) - all(reduce(bool, [None, lambda: x])) + min([None, lambda: x], key=repr) # B023: 23, "x" + sorted([None, lambda: x], key=repr) # B023: 26, "x" + any(filter(bool, [None, lambda: x])) # B023: 36, "x" + list(filter(bool, [None, lambda: x])) # B023: 37, "x" + all(reduce(bool, [None, lambda: x])) # B023: 36, "x" # But all these ones should be OK: min(range(3), key=lambda y: x * y) @@ -166,7 +166,7 @@ def iter_f(names): return lambda: name if exists(name) else None if foo(name): - return [lambda: name] # known false alarm + return [lambda: name] # known false alarm # B023: 28, "name" if False: - return [lambda: i for i in range(3)] # error + return [lambda: i for i in range(3)] # error # B023: 28, "i" \ No newline at end of file diff --git a/tests/b024.py b/tests/b024.py index 058de7a..4956bb5 100644 --- a/tests/b024.py +++ b/tests/b024.py @@ -14,7 +14,7 @@ """ -class Base_1(ABC): # error +class Base_1(ABC): # error # B024: 0, "Base_1" def method(self): foo() @@ -49,13 +49,13 @@ def method(self): foo() -class Base_7(ABC): # error +class Base_7(ABC): # error # B024: 0, "Base_7" @notabstract def method(self): foo() -class MetaBase_1(metaclass=ABCMeta): # error +class MetaBase_1(metaclass=ABCMeta): # error # B024: 0, "MetaBase_1" def method(self): foo() @@ -66,12 +66,12 @@ def method(self): foo() -class abc_Base_1(abc.ABC): # error +class abc_Base_1(abc.ABC): # error # B024: 0, "abc_Base_1" def method(self): foo() -class abc_Base_2(metaclass=abc.ABCMeta): # error +class abc_Base_2(metaclass=abc.ABCMeta): # error # B024: 0, "abc_Base_2" def method(self): foo() @@ -120,13 +120,13 @@ def method(self): # *not* safe, see https://github.com/PyCQA/flake8-bugbear/issues/471 -class abc_assign_class_variable(ABC): +class abc_assign_class_variable(ABC): # B024: 0, "abc_assign_class_variable" foo = 2 def method(self): foo() -class abc_annassign_class_variable(ABC): # *not* safe, see #471 +class abc_annassign_class_variable(ABC): # *not* safe, see #471 # B024: 0, "abc_annassign_class_variable" foo: int = 2 def method(self): foo() diff --git a/tests/b025.py b/tests/b025.py index 085f82f..034b56f 100644 --- a/tests/b025.py +++ b/tests/b025.py @@ -12,14 +12,14 @@ finally: a = 3 -try: +try: # B025: 0, "ValueError" a = 1 except ValueError: a = 2 except ValueError: a = 2 -try: +try: # B025: 0, "pickle.PickleError" a = 1 except pickle.PickleError: a = 2 @@ -28,7 +28,7 @@ except pickle.PickleError: a = 2 -try: +try: # B025: 0, "TypeError" # B025: 0, "ValueError" a = 1 except (ValueError, TypeError): a = 2 diff --git a/tests/b025_py311.py b/tests/b025_py311.py index d1d9914..e6713a3 100644 --- a/tests/b025_py311.py +++ b/tests/b025_py311.py @@ -12,14 +12,14 @@ finally: a = 3 -try: +try: # B025: 0, "ValueError" a = 1 except* ValueError: a = 2 except* ValueError: a = 2 -try: +try: # B025: 0, "pickle.PickleError" a = 1 except* pickle.PickleError: a = 2 @@ -28,7 +28,7 @@ except* pickle.PickleError: a = 2 -try: +try: # B025: 0, "TypeError" # B025: 0, "ValueError" a = 1 except* (ValueError, TypeError): a = 2 diff --git a/tests/b026.py b/tests/b026.py index a4b7f8f..c72fa97 100644 --- a/tests/b026.py +++ b/tests/b026.py @@ -12,10 +12,10 @@ def foo(bar, baz, bam): foo("bar", "baz", bam="bam") foo("bar", baz="baz", bam="bam") foo(bar="bar", baz="baz", bam="bam") -foo(bam="bam", *["bar", "baz"]) -foo(bam="bam", *bar_baz) -foo(baz="baz", bam="bam", *["bar"]) -foo(bar="bar", baz="baz", bam="bam", *[]) -foo(bam="bam", *["bar"], *["baz"]) -foo(*["bar"], bam="bam", *["baz"]) -foo.bar(bam="bam", *["bar"]) +foo(bam="bam", *["bar", "baz"]) # B026: 15 +foo(bam="bam", *bar_baz) # B026: 15 +foo(baz="baz", bam="bam", *["bar"]) # B026: 26 +foo(bar="bar", baz="baz", bam="bam", *[]) # B026: 37 +foo(bam="bam", *["bar"], *["baz"]) # B026: 15 # B026: 25 +foo(*["bar"], bam="bam", *["baz"]) # B026: 25 +foo.bar(bam="bam", *["bar"]) # B026: 19 diff --git a/tests/b027.py b/tests/b027.py index 218184b..83e8638 100644 --- a/tests/b027.py +++ b/tests/b027.py @@ -10,17 +10,17 @@ class AbstractClass(ABC): - def empty_1(self): # error + def empty_1(self): # error # B027: 4, "empty_1" ... - def empty_2(self): # error + def empty_2(self): # error # B027: 4, "empty_2" pass - def empty_3(self): # error + def empty_3(self): # error # B027: 4, "empty_3" """docstring""" ... - def empty_4(self): # error + def empty_4(self): # error # B027: 4, "empty_4" """multiple ellipsis/pass""" ... pass @@ -28,7 +28,7 @@ def empty_4(self): # error pass @notabstract - def empty_5(self): # error + def empty_5(self): # error # B027: 4, "empty_5" ... @abstractmethod diff --git a/tests/b028.py b/tests/b028.py index 1490e99..2c01919 100644 --- a/tests/b028.py +++ b/tests/b028.py @@ -5,8 +5,8 @@ B028 - on lines 8 and 9 """ -warnings.warn("test", DeprecationWarning) -warnings.warn("test", DeprecationWarning, source=None) +warnings.warn("test", DeprecationWarning) # B028: 0 +warnings.warn("test", DeprecationWarning, source=None) # B028: 0 warnings.warn("test", DeprecationWarning, source=None, stacklevel=2) warnings.warn("test", DeprecationWarning, stacklevel=1) warnings.warn("test", DeprecationWarning, 1) diff --git a/tests/b029.py b/tests/b029.py index 3df98cb..779b186 100644 --- a/tests/b029.py +++ b/tests/b029.py @@ -5,10 +5,10 @@ try: pass -except (): +except (): # B029: 0, "" pass try: pass -except () as e: +except () as e: # B029: 0, "" pass diff --git a/tests/b029_py311.py b/tests/b029_py311.py index b121436..081996f 100644 --- a/tests/b029_py311.py +++ b/tests/b029_py311.py @@ -5,10 +5,10 @@ try: pass -except* (): +except* (): # B029: 0, "*" pass try: pass -except* () as e: +except* () as e: # B029: 0, "*" pass diff --git a/tests/b030.py b/tests/b030.py index f90b2cd..01077af 100644 --- a/tests/b030.py +++ b/tests/b030.py @@ -1,6 +1,6 @@ try: pass -except (ValueError, (RuntimeError, (KeyError, TypeError))): # error +except (ValueError, (RuntimeError, (KeyError, TypeError))): # error # B030: 0 pass try: @@ -10,12 +10,12 @@ try: pass -except 1: # error +except 1: # error # B030: 0 pass try: pass -except (1, ValueError): # error +except (1, ValueError): # error # B030: 0 pass try: diff --git a/tests/b030_py311.py b/tests/b030_py311.py index beca578..9d78c4f 100644 --- a/tests/b030_py311.py +++ b/tests/b030_py311.py @@ -1,6 +1,6 @@ try: pass -except* (ValueError, (RuntimeError, (KeyError, TypeError))): # error +except* (ValueError, (RuntimeError, (KeyError, TypeError))): # error # B030: 0 pass try: @@ -10,12 +10,12 @@ try: pass -except* 1: # error +except* 1: # error # B030: 0 pass try: pass -except* (1, ValueError): # error +except* (1, ValueError): # error # B030: 0 pass try: diff --git a/tests/b031.py b/tests/b031.py index 3f3ef57..6580076 100644 --- a/tests/b031.py +++ b/tests/b031.py @@ -28,11 +28,11 @@ def collect_shop_items(shopper, items): # Group by shopping section for _section, section_items in groupby(items, key=lambda p: p[1]): for shopper in shoppers: - collect_shop_items(shopper, section_items) + collect_shop_items(shopper, section_items) # B031: 36, "section_items" for _section, section_items in groupby(items, key=lambda p: p[1]): collect_shop_items("Jane", section_items) - collect_shop_items("Joe", section_items) + collect_shop_items("Joe", section_items) # B031: 30, "section_items" for _section, section_items in groupby(items, key=lambda p: p[1]): @@ -41,7 +41,7 @@ def collect_shop_items(shopper, items): for _section, section_items in itertools.groupby(items, key=lambda p: p[1]): for shopper in shoppers: - collect_shop_items(shopper, section_items) + collect_shop_items(shopper, section_items) # B031: 36, "section_items" for group in groupby(items, key=lambda p: p[1]): # This is bad, but not detected currently diff --git a/tests/b032.py b/tests/b032.py index fc1fa12..3e39294 100644 --- a/tests/b032.py +++ b/tests/b032.py @@ -6,17 +6,17 @@ # Flag these dct = {"a": 1} -dct["b"]: 2 -dct.b: 2 +dct["b"]: 2 # B032: 0 +dct.b: 2 # B032: 0 -dct["b"]: "test" -dct.b: "test" +dct["b"]: "test" # B032: 0 +dct.b: "test" # B032: 0 test = "test" -dct["b"]: test -dct["b"]: test.lower() -dct.b: test -dct.b: test.lower() +dct["b"]: test # B032: 0 +dct["b"]: test.lower() # B032: 0 +dct.b: test # B032: 0 +dct.b: test.lower() # B032: 0 # Do not flag below typed_dct: dict[str, int] = {"a": 1} diff --git a/tests/b033.py b/tests/b033.py index 8ce42c9..efab06a 100644 --- a/tests/b033.py +++ b/tests/b033.py @@ -3,19 +3,19 @@ B033 - on lines 6-12, 16, 18 """ -test = {1, 2, 3, 3, 5} -test = {"a", "b", "c", "c", "e"} -test = {True, False, True} -test = {None, True, None} -test = {3, 3.0} -test = {1, True} -test = {0, False} +test = {1, 2, 3, 3, 5} # B033: 17, "3" +test = {"a", "b", "c", "c", "e"} # B033: 23, "'c'" +test = {True, False, True} # B033: 21, "True" +test = {None, True, None} # B033: 20, "None" +test = {3, 3.0} # B033: 11, "3.0" +test = {1, True} # B033: 11, "True" +test = {0, False} # B033: 11, "False" multi_line = { "alongvalueeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", 1, - True, + True, # B033: 4, "True" 0, - False, + False, # B033: 4, "False" } test = {1, 2, 3, 3.5, 5} diff --git a/tests/b034.py b/tests/b034.py index 8c20009..a2fc29e 100644 --- a/tests/b034.py +++ b/tests/b034.py @@ -2,15 +2,15 @@ from re import sub # error -re.sub("a", "b", "aaa", re.IGNORECASE) -re.sub("a", "b", "aaa", 5) -re.sub("a", "b", "aaa", 5, re.IGNORECASE) -re.subn("a", "b", "aaa", re.IGNORECASE) -re.subn("a", "b", "aaa", 5) -re.subn("a", "b", "aaa", 5, re.IGNORECASE) -re.split(" ", "a a a a", re.I) -re.split(" ", "a a a a", 2) -re.split(" ", "a a a a", 2, re.I) +re.sub("a", "b", "aaa", re.IGNORECASE) # B034: 24, "sub", "count" +re.sub("a", "b", "aaa", 5) # B034: 24, "sub", "count" +re.sub("a", "b", "aaa", 5, re.IGNORECASE) # B034: 24, "sub", "count" +re.subn("a", "b", "aaa", re.IGNORECASE) # B034: 25, "subn", "count" +re.subn("a", "b", "aaa", 5) # B034: 25, "subn", "count" +re.subn("a", "b", "aaa", 5, re.IGNORECASE) # B034: 25, "subn", "count" +re.split(" ", "a a a a", re.I) # B034: 25, "split", "maxsplit" +re.split(" ", "a a a a", 2) # B034: 25, "split", "maxsplit" +re.split(" ", "a a a a", 2, re.I) # B034: 25, "split", "maxsplit" # okay re.sub("a", "b", "aaa") diff --git a/tests/b035.py b/tests/b035.py index dd41d66..dee810b 100644 --- a/tests/b035.py +++ b/tests/b035.py @@ -3,8 +3,8 @@ regular_nested_dict = {"a": 1, "nested": {"b": 2, "c": "three"}} # bad - const key in dict comprehension -bad_const_key_str = {"a": i for i in range(3)} -bad_const_key_int = {1: i for i in range(3)} +bad_const_key_str = {"a": i for i in range(3)} # B035: 21, "a" +bad_const_key_int = {1: i for i in range(3)} # B035: 21, 1 # OK - const value in dict comp const_val = {i: "a" for i in range(3)} @@ -16,13 +16,13 @@ # nested nested_bad_and_good = { "good": {"a": 1, "b": 2}, - "bad": {"a": i for i in range(3)}, + "bad": {"a": i for i in range(3)}, # B035: 12, "a" } CONST_KEY_VAR = "KEY" # bad -bad_const_key_var = {CONST_KEY_VAR: i for i in range(3)} +bad_const_key_var = {CONST_KEY_VAR: i for i in range(3)} # B035: 21, "CONST_KEY_VAR" # OK - variable from tuple var_from_tuple = {k: v for k, v in {}.items()} @@ -32,7 +32,7 @@ # bad - variabe not from generator v3 = 1 -bad_var_not_from_nested_tuple = {v3: k for k, (v1, v2) in {"a": (1, 2)}.items()} +bad_var_not_from_nested_tuple = {v3: k for k, (v1, v2) in {"a": (1, 2)}.items()} # B035: 33, "v3" # OK - variable from named expression var_from_named_expr = { diff --git a/tests/b036.py b/tests/b036.py index 933ee8f..43905e3 100644 --- a/tests/b036.py +++ b/tests/b036.py @@ -1,14 +1,14 @@ try: pass -except BaseException: # bad +except BaseException: # bad # B036: 0 print("aaa") pass try: pass -except BaseException as ex: # bad +except BaseException as ex: # bad # B036: 0 print(ex) pass @@ -17,7 +17,7 @@ pass except ValueError: raise -except BaseException: # bad +except BaseException: # bad # B036: 0 pass @@ -30,7 +30,7 @@ try: pass -except BaseException as ex: # bad - raised something else +except BaseException as ex: # bad - raised something else # B036: 0 print("aaa") raise KeyError from ex @@ -47,7 +47,7 @@ try: pass -except BaseException: +except BaseException: # B036: 0 try: # nested try pass except ValueError: @@ -55,5 +55,5 @@ try: pass -except BaseException: +except BaseException: # B036: 0 raise a.b from None # bad (regression test for #449) diff --git a/tests/b036_py311.py b/tests/b036_py311.py index f9ca110..cd85dc0 100644 --- a/tests/b036_py311.py +++ b/tests/b036_py311.py @@ -1,14 +1,14 @@ try: pass -except* BaseException: # bad +except* BaseException: # bad # B036: 0 print("aaa") pass try: pass -except* BaseException as ex: # bad +except* BaseException as ex: # bad # B036: 0 print(ex) pass @@ -17,7 +17,7 @@ pass except* ValueError: raise -except* BaseException: # bad +except* BaseException: # bad # B036: 0 pass @@ -30,7 +30,7 @@ try: pass -except* BaseException as ex: # bad - raised something else +except* BaseException as ex: # bad - raised something else # B036: 0 print("aaa") raise KeyError from ex @@ -47,7 +47,7 @@ try: pass -except* BaseException: +except* BaseException: # B036: 0 try: # nested try pass except* ValueError: @@ -55,5 +55,5 @@ try: pass -except* BaseException: +except* BaseException: # B036: 0 raise a.b from None # bad (regression test for #449) diff --git a/tests/b037.py b/tests/b037.py index e580764..dc80d90 100644 --- a/tests/b037.py +++ b/tests/b037.py @@ -1,18 +1,18 @@ class A: def __init__(self) -> None: - return 1 # bad + return 1 # bad # B037: 8 class B: def __init__(self, x) -> None: if x: return # ok else: - return [] # bad + return [] # bad # B037: 12 class BNested: def __init__(self) -> None: - yield # bad + yield # bad # B037: 12 class C: @@ -20,14 +20,14 @@ def func(self): pass def __init__(self, k="") -> None: - yield from [] # bad + yield from [] # bad # B037: 8 class D(C): def __init__(self, k="") -> None: super().__init__(k) - return None # bad + return None # bad # B037: 8 class E: def __init__(self) -> None: - yield "a" + yield "a" # B037: 8 diff --git a/tests/b039.py b/tests/b039.py index 1ba7ce3..5acde64 100644 --- a/tests/b039.py +++ b/tests/b039.py @@ -2,11 +2,11 @@ import time from contextvars import ContextVar -ContextVar("cv", default=[]) # bad -ContextVar("cv", default=list()) # bad -ContextVar("cv", default=set()) # bad -ContextVar("cv", default=time.time()) # bad (B008-like) -contextvars.ContextVar("cv", default=[]) # bad +ContextVar("cv", default=[]) # bad # B039: 25 +ContextVar("cv", default=list()) # bad # B039: 25 +ContextVar("cv", default=set()) # bad # B039: 25 +ContextVar("cv", default=time.time()) # bad (B008-like) # B039: 25 +contextvars.ContextVar("cv", default=[]) # bad # B039: 37 # good diff --git a/tests/b040.py b/tests/b040.py index 1f5d6ec..cade900 100644 --- a/tests/b040.py +++ b/tests/b040.py @@ -4,7 +4,7 @@ def arbitrary_fun(*args, **kwargs): ... # classic case try: ... -except Exception as e: +except Exception as e: # B040: 0 e.add_note("...") # error try: @@ -21,7 +21,7 @@ def arbitrary_fun(*args, **kwargs): ... # other exception raised try: ... -except Exception as e: +except Exception as e: # B040: 0 f = ValueError() e.add_note("...") # error raise f @@ -80,7 +80,7 @@ def arbitrary_fun(*args, **kwargs): ... # multiple ExceptHandlers try: ... -except ValueError as e: +except ValueError as e: # B040: 0 e.add_note("") # error except TypeError as e: raise e @@ -104,14 +104,14 @@ def arbitrary_fun(*args, **kwargs): ... # special case: e is only used in the `add_note` call itself try: ... -except Exception as e: # error +except Exception as e: # error # B040: 0 e.add_note(str(e)) e.add_note(str(e)) # check nesting try: ... -except Exception as e: # error +except Exception as e: # error # B040: 0 e.add_note("") try: ... @@ -121,7 +121,7 @@ def arbitrary_fun(*args, **kwargs): ... # questionable if this should error try: ... -except Exception as e: +except Exception as e: # B040: 0 e.add_note("") e = ValueError() diff --git a/tests/b041.py b/tests/b041.py index ae819f4..4ccf6a1 100644 --- a/tests/b041.py +++ b/tests/b041.py @@ -1,11 +1,11 @@ a = 1 -test = {'yes': 1, 'yes': 1} -test = {'yes': 1, 'yes': 1, 'no': 2, 'no': 2} -test = {'yes': 1, 'yes': 1, 'yes': 1} -test = {1: 1, 1.0: 1} -test = {True: 1, True: 1} -test = {None: 1, None: 1} -test = {a: a, a: a} +test = {'yes': 1, 'yes': 1} # B041: 18 +test = {'yes': 1, 'yes': 1, 'no': 2, 'no': 2} # B041: 18 # B041: 37 +test = {'yes': 1, 'yes': 1, 'yes': 1} # B041: 18 # B041: 28 +test = {1: 1, 1.0: 1} # B041: 14 +test = {True: 1, True: 1} # B041: 17 +test = {None: 1, None: 1} # B041: 17 +test = {a: a, a: a} # B041: 14 # no error if either keys or values are different test = {'yes': 1, 'yes': 2} diff --git a/tests/b901.py b/tests/b901.py index 276eaca..c40f472 100644 --- a/tests/b901.py +++ b/tests/b901.py @@ -5,7 +5,7 @@ def broken(): if True: - return [1, 2, 3] # B901 + return [1, 2, 3] # B901 # B901: 8 yield 3 yield 2 @@ -32,7 +32,7 @@ def not_broken3(): def broken2(): - return [3, 2, 1] # B901 + return [3, 2, 1] # B901 # B901: 4 yield from not_broken() @@ -79,19 +79,19 @@ def __await__(self): def broken3(): if True: - return [1, 2, 3] # B901 + return [1, 2, 3] # B901 # B901: 8 else: yield 3 def broken4() -> Iterable[str]: yield "x" - return ["x"] # B901 + return ["x"] # B901 # B901: 4 def broken5() -> Generator[str]: yield "x" - return ["x"] # B901 + return ["x"] # B901 # B901: 4 def not_broken10() -> Generator[str, int, float]: diff --git a/tests/b902.py b/tests/b902.py index f48b09f..ebc7454 100644 --- a/tests/b902.py +++ b/tests/b902.py @@ -19,25 +19,25 @@ def not_a_problem(arg1): ... class Warnings: - def __init__(i_am_special): ... + def __init__(i_am_special): ... # B902: 17, "'i_am_special'", "instance", "self" - def almost_a_class_method(cls, arg1): ... + def almost_a_class_method(cls, arg1): ... # B902: 30, "'cls'", "instance", "self" - def almost_a_static_method(): ... + def almost_a_static_method(): ... # B902: 4, "(none)", "instance", "self" @classmethod - def wat(self, i_like_confusing_people): ... + def wat(self, i_like_confusing_people): ... # B902: 12, "'self'", "class", "cls" - def i_am_strange(*args, **kwargs): + def i_am_strange(*args, **kwargs): # B902: 22, "*args", "instance", "self" self = args[0] def defaults_anyone(self=None): ... - def invalid_kwargs_only(**kwargs): ... + def invalid_kwargs_only(**kwargs): ... # B902: 30, "**kwargs", "instance", "self" - def invalid_keyword_only(*, self): ... + def invalid_keyword_only(*, self): ... # B902: 32, "*, self", "instance", "self" - async def async_invalid_keyword_only(*, self): ... + async def async_invalid_keyword_only(*, self): ... # B902: 44, "*, self", "instance", "self" class Meta(type): @@ -49,10 +49,10 @@ def __prepare__(metacls, name, bases): class OtherMeta(type): - def __init__(self, name, bases, d): ... + def __init__(self, name, bases, d): ... # B902: 17, "'self'", "metaclass instance", "cls" @classmethod - def __prepare__(cls, name, bases): + def __prepare__(cls, name, bases): # B902: 20, "'cls'", "metaclass class", "metacls" return {} @classmethod diff --git a/tests/b902_extended.py b/tests/b902_extended.py index b458f1f..87fa4e6 100644 --- a/tests/b902_extended.py +++ b/tests/b902_extended.py @@ -1,33 +1,33 @@ -# parameters: --classmethod-decorators=["mylibrary.classmethod", "validator"] +# OPTIONS: classmethod_decorators=["mylibrary.makeclassmethod", "validator"], select=["B902"], class Errors: # correctly registered as classmethod @validator - def foo_validator(self) -> None: ... + def foo_validator(self) -> None: ... # B902: 22, "'self'", "class", "cls" @other.validator - def foo_other_validator(self) -> None: ... + def foo_other_validator(self) -> None: ... # B902: 28, "'self'", "class", "cls" @foo.bar.validator - def foo_foo_bar_validator(self) -> None: ... + def foo_foo_bar_validator(self) -> None: ... # B902: 30, "'self'", "class", "cls" @validator.blah - def foo_validator_blah(cls) -> None: ... + def foo_validator_blah(cls) -> None: ... # B902: 27, "'cls'", "instance", "self" # specifying attribute in options is not valid @mylibrary.makeclassmethod - def foo2(cls) -> None: ... + def foo2(cls) -> None: ... # B902: 13, "'cls'", "instance", "self" # specified attribute in options @makeclassmethod - def foo6(cls) -> None: ... + def foo6(cls) -> None: ... # B902: 13, "'cls'", "instance", "self" # classmethod is default, but if not specified it's ignored @classmethod - def foo3(cls) -> None: ... + def foo3(cls) -> None: ... # B902: 13, "'cls'", "instance", "self" # random unknown decorator @aoeuaoeu - def foo5(cls) -> None: ... + def foo5(cls) -> None: ... # B902: 13, "'cls'", "instance", "self" class NoErrors: @@ -56,32 +56,32 @@ def foo6(self) -> None: ... class ErrorsMeta(type): # correctly registered as classmethod @validator - def foo_validator(cls) -> None: ... + def foo_validator(cls) -> None: ... # B902: 22, "'cls'", "metaclass class", "metacls" @other.validator - def foo_other_validator(cls) -> None: ... + def foo_other_validator(cls) -> None: ... # B902: 28, "'cls'", "metaclass class", "metacls" @foo.bar.validator - def foo_foo_bar_validator(cls) -> None: ... + def foo_foo_bar_validator(cls) -> None: ... # B902: 30, "'cls'", "metaclass class", "metacls" @validator.blah - def foo_validator_blah(metacls) -> None: ... + def foo_validator_blah(metacls) -> None: ... # B902: 27, "'metacls'", "metaclass instance", "cls" # specifying attribute in options is not valid @mylibrary.makeclassmethod - def foo2(metacls) -> None: ... + def foo2(metacls) -> None: ... # B902: 13, "'metacls'", "metaclass instance", "cls" # specified attribute in options @makeclassmethod - def foo6(metacls) -> None: ... + def foo6(metacls) -> None: ... # B902: 13, "'metacls'", "metaclass instance", "cls" # classmethod is default, but if not specified it's ignored @classmethod - def foo3(metacls) -> None: ... + def foo3(metacls) -> None: ... # B902: 13, "'metacls'", "metaclass instance", "cls" # random unknown decorator @aoeuaoeu - def foo5(metacls) -> None: ... + def foo5(metacls) -> None: ... # B902: 13, "'metacls'", "metaclass instance", "cls" class NoErrorsMeta(type): diff --git a/tests/b902_py38.py b/tests/b902_py38.py index 9d81e15..7e128d6 100644 --- a/tests/b902_py38.py +++ b/tests/b902_py38.py @@ -19,25 +19,25 @@ def not_a_problem(arg1, /): ... class Warnings: - def __init__(i_am_special, /): ... + def __init__(i_am_special, /): ... # B902: 17, "'i_am_special'", "instance", "self" - def almost_a_class_method(cls, arg1, /): ... + def almost_a_class_method(cls, arg1, /): ... # B902: 30, "'cls'", "instance", "self" - def almost_a_static_method(): ... + def almost_a_static_method(): ... # B902: 4, "(none)", "instance", "self" @classmethod - def wat(self, i_like_confusing_people, /): ... + def wat(self, i_like_confusing_people, /): ... # B902: 12, "'self'", "class", "cls" - def i_am_strange(*args, **kwargs): + def i_am_strange(*args, **kwargs): # B902: 22, "*args", "instance", "self" self = args[0] def defaults_anyone(self=None, /): ... - def invalid_kwargs_only(**kwargs): ... + def invalid_kwargs_only(**kwargs): ... # B902: 30, "**kwargs", "instance", "self" - def invalid_keyword_only(*, self): ... + def invalid_keyword_only(*, self): ... # B902: 32, "*, self", "instance", "self" - async def async_invalid_keyword_only(*, self): ... + async def async_invalid_keyword_only(*, self): ... # B902: 44, "*, self", "instance", "self" class Meta(type): @@ -49,10 +49,10 @@ def __prepare__(metacls, name, bases, /): class OtherMeta(type): - def __init__(self, name, bases, d, /): ... + def __init__(self, name, bases, d, /): ... # B902: 17, "'self'", "metaclass instance", "cls" @classmethod - def __prepare__(cls, name, bases, /): + def __prepare__(cls, name, bases, /): # B902: 20, "'cls'", "metaclass class", "metacls" return {} @classmethod diff --git a/tests/b903.py b/tests/b903.py index 78da444..7c62cfb 100644 --- a/tests/b903.py +++ b/tests/b903.py @@ -28,13 +28,13 @@ def __init__(self, foo, bar): self.bar = bar -class Warnings: +class Warnings: # B903: 0 def __init__(self, foo, bar): self.foo = foo self.bar = bar -class WarningsWithDocstring: +class WarningsWithDocstring: # B903: 0 """A docstring should not be an impediment to a warning""" def __init__(self, foo, bar): diff --git a/tests/b904.py b/tests/b904.py index c410d10..5ef1f5d 100644 --- a/tests/b904.py +++ b/tests/b904.py @@ -7,13 +7,13 @@ raise ValueError except ValueError: if "abc": - raise TypeError - raise UserWarning + raise TypeError # B904: 8, "" + raise UserWarning # B904: 4, "" except AssertionError: raise # Bare `raise` should not be an error except Exception as err: assert err - raise Exception("No cause here...") + raise Exception("No cause here...") # B904: 4, "" except BaseException as base_err: # Might use this instead of bare raise with the `.with_traceback()` method raise base_err @@ -52,4 +52,4 @@ def context_switch(): try: raise ValueError except ValueError: - raise Exception + raise Exception # B904: 16, "" diff --git a/tests/b904_py311.py b/tests/b904_py311.py index 43cb4e9..ceda2c2 100644 --- a/tests/b904_py311.py +++ b/tests/b904_py311.py @@ -7,13 +7,13 @@ raise ValueError except* ValueError: if "abc": - raise TypeError - raise UserWarning + raise TypeError # B904: 8, "*" + raise UserWarning # B904: 4, "*" except* AssertionError: raise # Bare `raise` should not be an error except* Exception as err: assert err - raise Exception("No cause here...") + raise Exception("No cause here...") # B904: 4, "*" except* BaseException as base_err: # Might use this instead of bare raise with the `.with_traceback()` method raise base_err @@ -52,4 +52,4 @@ def context_switch(): try: raise ValueError except* ValueError: - raise Exception + raise Exception # B904: 16, "*" diff --git a/tests/b905_py310.py b/tests/b905_py310.py index 07f6d59..ae4694e 100644 --- a/tests/b905_py310.py +++ b/tests/b905_py310.py @@ -1,9 +1,9 @@ -zip() -zip(range(3)) -zip("a", "b") -zip("a", "b", *zip("c")) -zip(zip("a"), strict=False) -zip(zip("a", strict=True)) +zip() # B905: 0 +zip(range(3)) # B905: 0 +zip("a", "b") # B905: 0 +zip("a", "b", *zip("c")) # B905: 0 # B905: 15 +zip(zip("a"), strict=False) # B905: 4 +zip(zip("a", strict=True)) # B905: 0 zip(range(3), strict=True) zip("a", "b", strict=False) @@ -18,5 +18,5 @@ zip([1, 2, 3], itertools.repeat(1, None)) zip([1, 2, 3], itertools.repeat(1, times=None)) -zip([1, 2, 3], itertools.repeat(1, 1)) -zip([1, 2, 3], itertools.repeat(1, times=4)) +zip([1, 2, 3], itertools.repeat(1, 1)) # B905: 0 +zip([1, 2, 3], itertools.repeat(1, times=4)) # B905: 0 diff --git a/tests/b906.py b/tests/b906.py index b96a00e..f5478a3 100644 --- a/tests/b906.py +++ b/tests/b906.py @@ -6,7 +6,7 @@ # error -def visit_For(): ... +def visit_For(): ... # B906: 0 # has call to visit function diff --git a/tests/b907.py b/tests/b907.py deleted file mode 100644 index adfc629..0000000 --- a/tests/b907.py +++ /dev/null @@ -1,114 +0,0 @@ -def foo(): - return "hello" - - -var = var2 = "hello" - -# warnings -f"begin '{var}' end" -f"'{var}' end" -f"begin '{var}'" - -f'begin "{var}" end' -f'"{var}" end' -f'begin "{var}"' - -f'a "{"hello"}" b' -f'a "{foo()}" b' - -# fmt: off -k = (f'"' # Error emitted here on }"' -f'"{var:^}"' -f'"{var:5<}"' - -# explicit string specifier -f'"{var:s}"' - -# empty format string -f'"{var:}"' - -# These all currently give warnings, but could be considered false alarms -# multiple quote marks -f'"""{var}"""' -# str conversion specified -f'"{var!s}"' -# two variables fighting over the same quote mark -f'"{var}"{var2}"' # currently gives warning on the first one - - -# ***no warnings*** # - -# padding inside quotes -f'"{var:5}"' - -# quote mark not immediately adjacent -f'" {var} "' -f'"{var} "' -f'" {var}"' - -# mixed quote marks -f"'{var}\"" - -# repr specifier already given -f'"{var!r}"' - -# two variables in a row with no quote mark inbetween -f'"{var}{var}"' - -# don't crash on non-string constants -f'5{var}"' -f"\"{var}'" - -# sign option (only valid for number types) -f'"{var:+}"' - -# integer presentation type specified -f'"{var:b}"' -f'"{var:x}"' - -# float presentation type -f'"{var:e%}"' - -# alignment specifier invalid for strings -f'"{var:=}"' - -# other types and combinations are tested in test_b907_format_specifier_permutations - -# don't attempt to parse complex format specs -f'"{var:{var}}"' -f'"{var:5{var}}"' - -# even if explicit string type (not implemented) -f'"{var:{var}s}"' diff --git a/tests/b907_py312.py b/tests/b907_py312.py new file mode 100644 index 0000000..1c92627 --- /dev/null +++ b/tests/b907_py312.py @@ -0,0 +1,116 @@ +# column and lineno changes since 3.12 +# on <3.12 and columns are 0, and lineno is emitted from the first line if multiline +def foo(): + return "hello" + + +var = var2 = "hello" + +# warnings +f"begin '{var}' end" # B907: 9, "var" +f"'{var}' end" # B907: 3, "var" +f"begin '{var}'" # B907: 9, "var" + +f'begin "{var}" end' # B907: 9, "var" +f'"{var}" end' # B907: 3, "var" +f'begin "{var}"' # B907: 9, "var" + +f'a "{"hello"}" b' # B907: 5, "'hello'" +f'a "{foo()}" b' # B907: 5, "foo()" + +# fmt: off +k = (f'"' # Error emitted here on }"' # B907: 3, "var" +f'"{var:^}"' # B907: 3, "var" +f'"{var:5<}"' # B907: 3, "var" + +# explicit string specifier +f'"{var:s}"' # B907: 3, "var" + +# empty format string +f'"{var:}"' # B907: 3, "var" + +# These all currently give warnings, but could be considered false alarms +# multiple quote marks +f'"""{var}"""' # B907: 5, "var" +# str conversion specified +f'"{var!s}"' # B907: 3, "var" +# two variables fighting over the same quote mark +f'"{var}"{var2}"' # currently gives warning on the first one # B907: 3, "var" + + +# ***no warnings*** # + +# padding inside quotes +f'"{var:5}"' + +# quote mark not immediately adjacent +f'" {var} "' +f'"{var} "' +f'" {var}"' + +# mixed quote marks +f"'{var}\"" + +# repr specifier already given +f'"{var!r}"' + +# two variables in a row with no quote mark inbetween +f'"{var}{var}"' + +# don't crash on non-string constants +f'5{var}"' +f"\"{var}'" + +# sign option (only valid for number types) +f'"{var:+}"' + +# integer presentation type specified +f'"{var:b}"' +f'"{var:x}"' + +# float presentation type +f'"{var:e%}"' + +# alignment specifier invalid for strings +f'"{var:=}"' + +# other types and combinations are tested in test_b907_format_specifier_permutations + +# don't attempt to parse complex format specs +f'"{var:{var}}"' +f'"{var:5{var}}"' + +# even if explicit string type (not implemented) +f'"{var:{var}s}"' diff --git a/tests/b908.py b/tests/b908.py index d89f97e..8acb138 100644 --- a/tests/b908.py +++ b/tests/b908.py @@ -4,7 +4,7 @@ import pytest from pytest import raises, warns -with pytest.raises(TypeError): +with pytest.raises(TypeError): # B908: 0 a = 1 + "x" b = "x" + 1 print(a, b) @@ -12,19 +12,19 @@ class SomeTestCase(unittest.TestCase): def test_func_raises(self): - with self.assertRaises(TypeError): + with self.assertRaises(TypeError): # B908: 8 a = 1 + "x" b = "x" + 1 print(a, b) def test_func_raises_regex(self): - with self.assertRaisesRegex(TypeError): + with self.assertRaisesRegex(TypeError): # B908: 8 a = 1 + "x" b = "x" + 1 print(a, b) def test_func_raises_regexp(self): - with self.assertRaisesRegexp(TypeError): + with self.assertRaisesRegexp(TypeError): # B908: 8 a = 1 + "x" b = "x" + 1 print(a, b) @@ -34,15 +34,15 @@ def test_raises_correct(self): print("1" + 1) -with raises(Exception): +with raises(Exception): # B017: 0 # B908: 0 "1" + 1 "2" + 2 -with pytest.warns(Warning): +with pytest.warns(Warning): # B908: 0 print("print before warning") warnings.warn("some warning", stacklevel=1) -with warns(Warning): +with warns(Warning): # B908: 0 print("print before warning") warnings.warn("some warning", stacklevel=1) @@ -53,5 +53,5 @@ def test_raises_correct(self): with pytest.warns(Warning): warnings.warn("some warning", stacklevel=1) -with raises(Exception): +with raises(Exception): # B017: 0 raise Exception("some exception") diff --git a/tests/b909.py b/tests/b909.py index 1601ee9..18e6489 100644 --- a/tests/b909.py +++ b/tests/b909.py @@ -9,20 +9,20 @@ some_other_list = [1, 2, 3] for elem in some_list: # errors - some_list.remove(elem) - del some_list[2] - some_list.append(elem) - some_list.sort() - some_list.reverse() - some_list.clear() - some_list.extend([1, 2]) - some_list.insert(1, 1) - some_list.pop(1) - some_list.pop() + some_list.remove(elem) # B909: 4 + del some_list[2] # B909: 4 + some_list.append(elem) # B909: 4 + some_list.sort() # B909: 4 + some_list.reverse() # B909: 4 + some_list.clear() # B909: 4 + some_list.extend([1, 2]) # B909: 4 + some_list.insert(1, 1) # B909: 4 + some_list.pop(1) # B909: 4 + some_list.pop() # B909: 4 # conditional break should error if elem == 2: - some_list.remove(elem) + some_list.remove(elem) # B909: 8 if elem == 3: break @@ -44,9 +44,9 @@ for elem in mydicts: # errors - mydicts.popitem() - mydicts.setdefault('foo', 1) - mydicts.update({'foo': 'bar'}) + mydicts.popitem() # B909: 4 + mydicts.setdefault('foo', 1) # B909: 4 + mydicts.update({'foo': 'bar'}) # B909: 4 # no errors elem.popitem() @@ -59,12 +59,12 @@ for _ in myset: # errors - myset.update({4, 5}) - myset.intersection_update({4, 5}) - myset.difference_update({4, 5}) - myset.symmetric_difference_update({4, 5}) - myset.add(4) - myset.discard(3) + myset.update({4, 5}) # B909: 4 + myset.intersection_update({4, 5}) # B909: 4 + myset.difference_update({4, 5}) # B909: 4 + myset.symmetric_difference_update({4, 5}) # B909: 4 + myset.add(4) # B909: 4 + myset.discard(3) # B909: 4 # no errors del myset @@ -81,8 +81,8 @@ def __init__(self, ls): a = A((1, 2, 3)) # ensure member accesses are handled for elem in a.some_list: - a.some_list.remove(elem) - del a.some_list[2] + a.some_list.remove(elem) # B909: 4 + del a.some_list[2] # B909: 4 # Augassign @@ -90,19 +90,19 @@ def __init__(self, ls): foo = [1, 2, 3] bar = [4, 5, 6] for _ in foo: - foo *= 2 - foo += bar - foo[1] = 9 #todo - foo[1:2] = bar - foo[1:2:3] = bar + foo *= 2 # B909: 4 + foo += bar # B909: 4 + foo[1] = 9 #todo # B909: 4 + foo[1:2] = bar # B909: 4 + foo[1:2:3] = bar # B909: 4 foo = {1,2,3} bar = {4,5,6} for _ in foo: - foo |= bar - foo &= bar - foo -= bar - foo ^= bar + foo |= bar # B909: 4 + foo &= bar # B909: 4 + foo -= bar # B909: 4 + foo ^= bar # B909: 4 # more tests for unconditional breaks @@ -122,7 +122,7 @@ def __init__(self, ls): # should error (?) for _ in foo: - foo.remove(1) + foo.remove(1) # B909: 4 if bar: bar.remove(1) break diff --git a/tests/b910.py b/tests/b910.py index 0b98bc6..3ad0c51 100644 --- a/tests/b910.py +++ b/tests/b910.py @@ -1,8 +1,8 @@ from collections import defaultdict -a = defaultdict(int) +a = defaultdict(int) # B910: 4 b = defaultdict(float) c = defaultdict(bool) d = defaultdict(str) e = defaultdict() -f = defaultdict(int) +f = defaultdict(int) # B910: 4 diff --git a/tests/b911_py313.py b/tests/b911_py313.py index 61107c8..3287fa4 100644 --- a/tests/b911_py313.py +++ b/tests/b911_py313.py @@ -2,12 +2,12 @@ from itertools import batched # Expect B911 -batched(range(3), 2) -batched(range(3), n=2) -batched(iterable=range(3), n=2) -itertools.batched(range(3), 2) -itertools.batched(range(3), n=2) -itertools.batched(iterable=range(3), n=2) +batched(range(3), 2) # B911: 0 +batched(range(3), n=2) # B911: 0 +batched(iterable=range(3), n=2) # B911: 0 +itertools.batched(range(3), 2) # B911: 0 +itertools.batched(range(3), n=2) # B911: 0 +itertools.batched(iterable=range(3), n=2) # B911: 0 # OK batched(range(3), 2, strict=True) diff --git a/tests/b950.py b/tests/b950.py index d5b7e4e..50e54a5 100644 --- a/tests/b950.py +++ b/tests/b950.py @@ -4,21 +4,21 @@ "line is fine" " line is fine " " line is still fine " -" line is no longer fine by any measures, yup" +" line is no longer fine by any measures, yup" # B950: 113, 113, 79 "line is fine again" # Ensure URL/path on it's own line is fine "https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com" -"NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com" +"NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com" # B950: 125, 125, 79 # https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com -# NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com +# NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com # B950: 125, 125, 79 # #: Okay # This # almost_empty_line_too_long # This -# almost_empty_line_too_long +# almost_empty_line_too_long # B950: 118, 118, 79 # Long empty line """ @@ -33,5 +33,5 @@ "https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com # type: ignore[some-code]" "https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com # type: ignore[some-code] # noqa: F401" "https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com # noqa: F401 # type:ignore[some-code]" -"NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com # noqa" -"NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com # type: ignore" +"NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com # noqa" # B950: 132, 132, 79 +"NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com # type: ignore" # B950: 140, 140, 79 diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 44426bf..b67fc99 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -1,858 +1,85 @@ import ast import itertools import os +import re import site import subprocess -import sys import unittest from argparse import Namespace from pathlib import Path +import pytest + from bugbear import ( - B001, - B002, - B003, - B004, - B005, - B006, - B007, - B008, - B009, - B010, - B011, - B012, - B013, - B014, - B015, - B016, - B017, - B018, - B019, - B020, - B021, - B022, - B023, - B024, - B025, - B026, - B027, - B028, - B029, - B030, - B031, - B032, - B033, - B034, - B035, - B036, - B037, - B039, - B040, - B041, - B901, - B902, - B903, - B904, - B905, - B906, - B907, - B908, - B909, - B910, - B911, - B950, BugBearChecker, BugBearVisitor, + error, + error_codes, +) + +test_files: list[tuple[str, Path]] = sorted( + (f.stem.upper(), f) + for f in (Path(__file__).parent).iterdir() + if re.fullmatch(r"b\d\d\d.*\.py", f.name) ) +@pytest.mark.parametrize(("test", "path"), test_files, ids=[f[0] for f in test_files]) +def test_eval( + test: str, + path: Path, +): + print(test, path) + content = path.read_text() + expected, options = _parse_eval_file(test, content) + assert expected + tuple_expected = [ + (e.lineno, e.col, e.message.format(*e.vars), e.type) for e in expected + ] + + bbc = BugBearChecker(filename=str(path), options=options) + errors = [e for e in bbc.run() if (test == "B950" or not e[2].startswith("B950"))] + errors.sort() + assert errors == tuple_expected + + +def _parse_eval_file(test: str, content: str) -> tuple[list[error], Namespace | None]: + + # error_class: Any = eval(error_code) + expected: list[error] = [] + options: Namespace | None = None + + for lineno, line in enumerate(content.split("\n"), start=1): + if line.startswith("# OPTIONS:"): + options = eval(f"Namespace({line[10:]})") + + # skip commented out lines + if not line or (line[0] == "#" and test != "B950"): + continue + + # skip lines that *don't* have a comment + if "#" not in line: + continue + + # get text between `error:` and (end of line or another comment) + k = re.findall(r"(B\d\d\d):([^#]*)(?=#|$)", line) + for err_code, err_args in k: + # evaluate the arguments as if in a tuple + args = eval(f"({err_args},)") + assert args, "you must specify at least column" + col, *vars = args + assert isinstance(col, int), "column must be an int" + error_class = error_codes[err_code] + expected.append(error_class(lineno, col, vars=vars)) + return expected, options + + class BugbearTestCase(unittest.TestCase): maxDiff = None def errors(self, *errors): return [BugBearChecker.adapt_error(e) for e in errors] - def test_b001(self): - filename = Path(__file__).absolute().parent / "b001.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B001(8, 0), - B001(40, 4), - ) - self.assertEqual(errors, expected) - - def test_b002(self): - filename = Path(__file__).absolute().parent / "b002.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual(errors, self.errors(B002(14, 8), B002(19, 11))) - - def test_b003(self): - filename = Path(__file__).absolute().parent / "b003.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual(errors, self.errors(B003(9, 0))) - - def test_b004(self): - filename = Path(__file__).absolute().parent / "b004.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual(errors, self.errors(B004(3, 7), B004(5, 7))) - - def test_b005(self): - filename = Path(__file__).absolute().parent / "b005.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B005(4, 0), - B005(7, 0), - B005(10, 0), - B005(13, 0), - B005(16, 0), - B005(19, 0), - ), - ) - - def test_b006_b008(self): - filename = Path(__file__).absolute().parent / "b006_b008.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B006(58, 24), - B006(61, 29), - B006(64, 19), - B006(67, 19), - B006(70, 31), - B006(73, 25), - B006(77, 45), - B008(77, 60), - B006(81, 45), - B008(81, 63), - B006(85, 44), - B008(85, 59), - B006(89, 32), - B008(100, 38), - B008(103, 11), - B008(103, 31), - B008(107, 29), - B008(149, 29), - B008(153, 44), - B006(159, 19), - B008(159, 20), - B008(159, 30), - B008(165, 21), - B008(170, 18), - B008(170, 36), - ), - ) - - def test_b007(self): - filename = Path(__file__).absolute().parent / "b007.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B007(6, 4, vars=("i",)), - B007(18, 12, vars=("k",)), - B007(30, 4, vars=("i",)), - B007(30, 12, vars=("k",)), - ), - ) - - def test_b008_extended(self): - filename = Path(__file__).absolute().parent / "b008_extended.py" - - mock_options = Namespace( - extend_immutable_calls=["fastapi.Depends", "fastapi.Query"] - ) - - bbc = BugBearChecker(filename=str(filename), options=mock_options) - errors = list(bbc.run()) - - self.assertEqual( - errors, - self.errors(B008(13, 66)), - ) - - def test_b009_b010(self): - filename = Path(__file__).absolute().parent / "b009_b010.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - all_errors = [ - B009(17, 0), - B009(18, 0), - B009(19, 0), - B010(28, 0), - B010(29, 0), - B010(30, 0), - B009(45, 14), - ] - self.assertEqual(errors, self.errors(*all_errors)) - - def test_b011(self): - filename = Path(__file__).absolute().parent / "b011.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, self.errors(B011(8, 0, vars=("i",)), B011(10, 0, vars=("k",))) - ) - - def test_b012(self): - filename = Path(__file__).absolute().parent / "b012.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - all_errors = [ - B012(5, 8, vars=("",)), - B012(13, 12, vars=("",)), - B012(21, 12, vars=("",)), - B012(31, 12, vars=("",)), - B012(44, 20, vars=("",)), - B012(66, 12, vars=("",)), - B012(78, 12, vars=("",)), - B012(94, 12, vars=("",)), - B012(101, 8, vars=("",)), - B012(107, 8, vars=("",)), - ] - self.assertEqual(errors, self.errors(*all_errors)) - - @unittest.skipIf(sys.version_info < (3, 11), "requires 3.11+") - def test_b012_py311(self): - filename = Path(__file__).absolute().parent / "b012_py311.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - all_errors = [ - B012(7, 8, vars=("*",)), - B012(17, 12, vars=("*",)), - B012(27, 12, vars=("*",)), - ] - self.assertEqual(errors, self.errors(*all_errors)) - - def test_b013(self): - filename = Path(__file__).absolute().parent / "b013.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B013(10, 0, vars=("ValueError", "")), - B013(32, 0, vars=("re.error", "")), - ) - self.assertEqual(errors, expected) - - @unittest.skipIf(sys.version_info < (3, 11), "requires 3.11+") - def test_b013_py311(self): - filename = Path(__file__).absolute().parent / "b013_py311.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B013(10, 0, vars=("ValueError", "*")), - B013(32, 0, vars=("re.error", "*")), - ) - self.assertEqual(errors, expected) - - def test_b014(self): - filename = Path(__file__).absolute().parent / "b014.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B014(11, 0, vars=("Exception, TypeError", "", "Exception", "")), - B014(17, 0, vars=("OSError, OSError", " as err", "OSError", "")), - B014(28, 0, vars=("MyError, MyError", "", "MyError", "")), - B014(42, 0, vars=("MyError, BaseException", " as e", "BaseException", "")), - B014(49, 0, vars=("re.error, re.error", "", "re.error", "")), - B014( - 56, - 0, - vars=("IOError, EnvironmentError, OSError", "", "OSError", ""), - ), - B014(74, 0, vars=("ValueError, binascii.Error", "", "ValueError", "")), - ) - self.assertEqual(errors, expected) - - @unittest.skipIf(sys.version_info < (3, 11), "requires 3.11+") - def test_b014_py311(self): - filename = Path(__file__).absolute().parent / "b014_py311.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B014(11, 0, vars=("Exception, TypeError", "", "Exception", "*")), - B014(17, 0, vars=("OSError, OSError", " as err", "OSError", "*")), - B014(28, 0, vars=("MyError, MyError", "", "MyError", "*")), - B014(42, 0, vars=("MyError, BaseException", " as e", "BaseException", "*")), - B014(49, 0, vars=("re.error, re.error", "", "re.error", "*")), - B014( - 56, - 0, - vars=("IOError, EnvironmentError, OSError", "", "OSError", "*"), - ), - B014(74, 0, vars=("ValueError, binascii.Error", "", "ValueError", "*")), - ) - self.assertEqual(errors, expected) - - def test_b015(self): - filename = Path(__file__).absolute().parent / "b015.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors(B015(8, 0), B015(12, 0), B015(22, 4), B015(29, 4)) - self.assertEqual(errors, expected) - - def test_b016(self): - filename = Path(__file__).absolute().parent / "b016.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors(B016(6, 0), B016(7, 0), B016(8, 0), B016(10, 0)) - self.assertEqual(errors, expected) - - def test_b017(self): - filename = Path(__file__).absolute().parent / "b017.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B017(26, 8), - B017(28, 8), - B017(30, 8), - B017(32, 8), - B017(73, 8), - B017(75, 8), - B017(77, 8), - ) - self.assertEqual(errors, expected) - - def test_b018_functions(self): - filename = Path(__file__).absolute().parent / "b018_functions.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - - expected = [ - B018(15, 4, vars=("Constant",)), - B018(16, 4, vars=("Constant",)), - B018(17, 4, vars=("Constant",)), - B018(18, 4, vars=("Constant",)), - B018(19, 4, vars=("Constant",)), - B018(20, 4, vars=("Constant",)), - B018(21, 4, vars=("Constant",)), - B018(22, 4, vars=("List",)), - B018(23, 4, vars=("Set",)), - B018(24, 4, vars=("Dict",)), - B018(28, 4, vars=("Constant",)), - B018(31, 4, vars=("Constant",)), - B018(32, 4, vars=("Tuple",)), - B018(33, 4, vars=("Tuple",)), - ] - - self.assertEqual(errors, self.errors(*expected)) - - def test_b018_classes(self): - filename = Path(__file__).absolute().parent / "b018_classes.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - - expected = [ - B018(16, 4, vars=("Constant",)), - B018(17, 4, vars=("Constant",)), - B018(18, 4, vars=("Constant",)), - B018(19, 4, vars=("Constant",)), - B018(20, 4, vars=("Constant",)), - B018(21, 4, vars=("Constant",)), - B018(22, 4, vars=("Constant",)), - B018(23, 4, vars=("List",)), - B018(24, 4, vars=("Set",)), - B018(25, 4, vars=("Dict",)), - B018(29, 4, vars=("Constant",)), - B018(32, 4, vars=("Constant",)), - B018(33, 4, vars=("Tuple",)), - B018(34, 4, vars=("Tuple",)), - ] - - self.assertEqual(errors, self.errors(*expected)) - - def test_b018_modules(self): - filename = Path(__file__).absolute().parent / "b018_modules.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - - expected = [ - B018(9, 0, vars=("Constant",)), - B018(10, 0, vars=("Constant",)), - B018(11, 0, vars=("Constant",)), - B018(12, 0, vars=("Constant",)), - B018(13, 0, vars=("Constant",)), - B018(14, 0, vars=("Constant",)), - B018(15, 0, vars=("Constant",)), - B018(16, 0, vars=("List",)), - B018(17, 0, vars=("Set",)), - B018(18, 0, vars=("Dict",)), - B018(19, 0, vars=("Tuple",)), - B018(20, 0, vars=("Tuple",)), - ] - self.assertEqual(errors, self.errors(*expected)) - - def test_b018_nested(self): - filename = Path(__file__).absolute().parent / "b018_nested.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - - expected = [ - B018(3, 0, vars=("Constant",)), - B018(8, 4, vars=("Constant",)), - B018(14, 4, vars=("List",)), - B018(18, 8, vars=("Tuple",)), - B018(22, 12, vars=("Constant",)), - B018(25, 16, vars=("Constant",)), - B018(28, 20, vars=("Set",)), - B018(31, 20, vars=("Set",)), - B018(34, 20, vars=("Set",)), - B018(36, 24, vars=("Constant",)), - B018(40, 24, vars=("Constant",)), - B018(42, 24, vars=("Constant",)), - ] - self.assertEqual(errors, self.errors(*expected)) - - def test_b019(self): - filename = Path(__file__).absolute().parent / "b019.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B019(61, 5), - B019(64, 5), - B019(67, 5), - B019(70, 5), - B019(73, 5), - B019(76, 5), - B019(79, 5), - B019(82, 5), - ), - ) - - def test_b020(self): - filename = Path(__file__).absolute().parent / "b020.py" - bbc = BugBearChecker(filename=str(filename)) - errors = [e for e in bbc.run() if e[2][:4] == "B020"] - self.assertEqual( - errors, - self.errors( - B020(8, 4, vars=("items",)), - B020(21, 9, vars=("values",)), - B020(36, 4, vars=("vars",)), - ), - ) - - def test_b021_classes(self): - filename = Path(__file__).absolute().parent / "b021.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B021(14, 4), - B021(22, 4), - B021(30, 4), - B021(38, 4), - B021(46, 4), - B021(54, 4), - B021(62, 4), - B021(70, 4), - B021(74, 4), - ) - self.assertEqual(errors, expected) - - def test_b022(self): - filename = Path(__file__).absolute().parent / "b022.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual(errors, self.errors(B022(8, 0))) - - def test_b023(self): - filename = Path(__file__).absolute().parent / "b023.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B023(13, 29, vars=("x",)), - B023(14, 29, vars=("y",)), - B023(17, 15, vars=("x",)), - B023(29, 18, vars=("x",)), - B023(30, 18, vars=("x",)), - B023(31, 18, vars=("x",)), - B023(32, 21, vars=("x",)), - B023(41, 33, vars=("x",)), - B023(43, 13, vars=("x",)), - B023(51, 29, vars=("a",)), - B023(52, 29, vars=("a_",)), - B023(53, 29, vars=("b",)), - B023(54, 29, vars=("c",)), - B023(62, 16, vars=("j",)), - B023(62, 20, vars=("k",)), - B023(69, 9, vars=("l",)), - B023(114, 23, vars=("x",)), - B023(115, 26, vars=("x",)), - B023(116, 36, vars=("x",)), - B023(117, 37, vars=("x",)), - B023(118, 36, vars=("x",)), - B023(169, 28, vars=("name",)), # known false alarm - B023(172, 28, vars=("i",)), - ) - self.assertEqual(errors, expected) - - def test_b024(self): - filename = Path(__file__).absolute().parent / "b024.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B024(17, 0, vars=("Base_1",)), - B024(52, 0, vars=("Base_7",)), - B024(58, 0, vars=("MetaBase_1",)), - B024(69, 0, vars=("abc_Base_1",)), - B024(74, 0, vars=("abc_Base_2",)), - B024(123, 0, vars=("abc_assign_class_variable",)), - B024(129, 0, vars=("abc_annassign_class_variable",)), - ) - self.assertEqual(errors, expected) - - def test_b025(self): - filename = Path(__file__).absolute().parent / "b025.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B025(15, 0, vars=("ValueError",)), - B025(22, 0, vars=("pickle.PickleError",)), - B025(31, 0, vars=("TypeError",)), - B025(31, 0, vars=("ValueError",)), - ), - ) - - @unittest.skipIf(sys.version_info < (3, 11), "requires 3.11+") - def test_b025_py311(self): - filename = Path(__file__).absolute().parent / "b025_py311.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B025(15, 0, vars=("ValueError",)), - B025(22, 0, vars=("pickle.PickleError",)), - B025(31, 0, vars=("TypeError",)), - B025(31, 0, vars=("ValueError",)), - ), - ) - - def test_b026(self): - filename = Path(__file__).absolute().parent / "b026.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B026(15, 15), - B026(16, 15), - B026(17, 26), - B026(18, 37), - B026(19, 15), - B026(19, 25), - B026(20, 25), - B026(21, 19), - ), - ) - - def test_b027(self): - filename = Path(__file__).absolute().parent / "b027.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B027(13, 4, vars=("empty_1",)), - B027(16, 4, vars=("empty_2",)), - B027(19, 4, vars=("empty_3",)), - B027(23, 4, vars=("empty_4",)), - B027(31, 4, vars=("empty_5",)), - ) - self.assertEqual(errors, expected) - - def test_b028(self): - filename = Path(__file__).absolute().parent / "b028.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors(B028(8, 0), B028(9, 0)) - self.assertEqual(errors, expected) - - def test_b029(self): - filename = Path(__file__).absolute().parent / "b029.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B029(8, 0, vars=("",)), - B029(13, 0, vars=("",)), - ) - self.assertEqual(errors, expected) - - @unittest.skipIf(sys.version_info < (3, 11), "requires 3.11+") - def test_b029_py311(self): - filename = Path(__file__).absolute().parent / "b029_py311.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B029(8, 0, vars=("*",)), - B029(13, 0, vars=("*",)), - ) - self.assertEqual(errors, expected) - - def test_b030(self): - filename = Path(__file__).absolute().parent / "b030.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B030(3, 0), - B030(13, 0), - B030(18, 0), - ) - self.assertEqual(errors, expected) - - @unittest.skipIf(sys.version_info < (3, 11), "requires 3.11+") - def test_b030_py311(self): - filename = Path(__file__).absolute().parent / "b030_py311.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B030(3, 0), - B030(13, 0), - B030(18, 0), - ) - self.assertEqual(errors, expected) - - def test_b031(self): - filename = Path(__file__).absolute().parent / "b031.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B031(31, 36, vars=("section_items",)), - B031(35, 30, vars=("section_items",)), - B031(44, 36, vars=("section_items",)), - ) - self.assertEqual(errors, expected) - - def test_b032(self): - filename = Path(__file__).absolute().parent / "b032.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B032(9, 0), - B032(10, 0), - B032(12, 0), - B032(13, 0), - B032(16, 0), - B032(17, 0), - B032(18, 0), - B032(19, 0), - ) - self.assertEqual(errors, expected) - - def test_b033(self): - filename = Path(__file__).absolute().parent / "b033.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B033(6, 17, vars=("3",)), - B033(7, 23, vars=("'c'",)), - B033(8, 21, vars=("True",)), - B033(9, 20, vars=("None",)), - B033(10, 11, vars=("3.0",)), - B033(11, 11, vars=("True",)), - B033(12, 11, vars=("False",)), - B033(16, 4, vars=("True",)), - B033(18, 4, vars=("False",)), - ) - self.assertEqual(errors, expected) - - def test_b034(self): - filename = Path(__file__).absolute().parent / "b034.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B034(5, 24, vars=("sub", "count")), - B034(6, 24, vars=("sub", "count")), - B034(7, 24, vars=("sub", "count")), - B034(8, 25, vars=("subn", "count")), - B034(9, 25, vars=("subn", "count")), - B034(10, 25, vars=("subn", "count")), - B034(11, 25, vars=("split", "maxsplit")), - B034(12, 25, vars=("split", "maxsplit")), - B034(13, 25, vars=("split", "maxsplit")), - ) - self.assertEqual(errors, expected) - - def test_b035(self): - filename = Path(__file__).absolute().parent / "b035.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B035(6, 21, vars=("a",)), - B035(7, 21, vars=(1,)), - B035(19, 12, vars=("a",)), - B035(25, 21, vars=("CONST_KEY_VAR",)), - B035(35, 33, vars=("v3",)), - ) - self.assertEqual(errors, expected) - - def test_b036(self) -> None: - filename = Path(__file__).absolute().parent / "b036.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B036(4, 0), - B036(11, 0), - B036(20, 0), - B036(33, 0), - B036(50, 0), - B036(58, 0), - ) - self.assertEqual(errors, expected) - - @unittest.skipIf(sys.version_info < (3, 11), "requires 3.11+") - def test_b036_py311(self) -> None: - filename = Path(__file__).absolute().parent / "b036_py311.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B036(4, 0), - B036(11, 0), - B036(20, 0), - B036(33, 0), - B036(50, 0), - B036(58, 0), - ) - self.assertEqual(errors, expected) - - def test_b037(self) -> None: - filename = Path(__file__).absolute().parent / "b037.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B037(4, 8), - B037(11, 12), - B037(15, 12), - B037(23, 8), - B037(29, 8), - B037(33, 8), - ) - self.assertEqual(errors, expected) - - def test_b039(self) -> None: - filename = Path(__file__).absolute().parent / "b039.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B039(5, 25), - B039(6, 25), - B039(7, 25), - B039(8, 25), - B039(9, 37), - ) - self.assertEqual(errors, expected) - - def test_b040(self) -> None: - filename = Path(__file__).absolute().parent / "b040.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B040(7, 0), - B040(24, 0), - B040(83, 0), - B040(107, 0), - B040(114, 0), - B040(124, 0), - ) - self.assertEqual(errors, expected) - - def test_b041(self) -> None: - filename = Path(__file__).absolute().parent / "b041.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B041(2, 18), - B041(3, 18), - B041(3, 37), - B041(4, 18), - B041(4, 28), - B041(5, 14), - B041(6, 17), - B041(7, 17), - B041(8, 14), - ) - self.assertEqual(errors, expected) - - def test_b908(self): - filename = Path(__file__).absolute().parent / "b908.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = self.errors( - B908(7, 0), - B908(15, 8), - B908(21, 8), - B908(27, 8), - B017(37, 0), - B908(37, 0), - B908(41, 0), - B908(45, 0), - B017(56, 0), - ) - self.assertEqual(errors, expected) - - def test_b907(self): - filename = Path(__file__).absolute().parent / "b907.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - py39 = sys.version_info >= (3, 9) - py312 = sys.version_info >= (3, 12) - - def on_py312(number): - """F-string nodes have column numbers set to 0 on 60k permutations # see format spec at # https://docs.python.org/3/library/string.html#format-specification-mini-language @@ -907,177 +134,22 @@ def test_b907_format_specifier_permutations(self): visitor.errors == [] ), f"b907 raised for {format_spec} that would look different with !r" - def test_b901(self): - filename = Path(__file__).absolute().parent / "b901.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors(B901(8, 8), B901(35, 4), B901(82, 8), B901(89, 4), B901(94, 4)), - ) - - def test_b902(self): - filename = Path(__file__).absolute().parent / "b902.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B902(22, 17, vars=("'i_am_special'", "instance", "self")), - B902(24, 30, vars=("'cls'", "instance", "self")), - B902(26, 4, vars=("(none)", "instance", "self")), - B902(29, 12, vars=("'self'", "class", "cls")), - B902(31, 22, vars=("*args", "instance", "self")), - B902(36, 30, vars=("**kwargs", "instance", "self")), - B902(38, 32, vars=("*, self", "instance", "self")), - B902(40, 44, vars=("*, self", "instance", "self")), - B902(52, 17, vars=("'self'", "metaclass instance", "cls")), - B902(55, 20, vars=("'cls'", "metaclass class", "metacls")), - ), - ) - - def test_b902_extended(self): - filename = Path(__file__).absolute().parent / "b902_extended.py" - - mock_options = Namespace( - classmethod_decorators=["mylibrary.makeclassmethod", "validator"], - select=["B902"], - ) - bbc = BugBearChecker(filename=str(filename), options=mock_options) - errors = list(bbc.run()) - - self.assertEqual( - errors, - self.errors( - B902(5, 22, vars=("'self'", "class", "cls")), - B902(8, 28, vars=("'self'", "class", "cls")), - B902(11, 30, vars=("'self'", "class", "cls")), - B902(14, 27, vars=("'cls'", "instance", "self")), - B902(18, 13, vars=("'cls'", "instance", "self")), - B902(22, 13, vars=("'cls'", "instance", "self")), - B902(26, 13, vars=("'cls'", "instance", "self")), - B902(30, 13, vars=("'cls'", "instance", "self")), - # metaclass - B902(59, 22, vars=("'cls'", "metaclass class", "metacls")), - B902(62, 28, vars=("'cls'", "metaclass class", "metacls")), - B902(65, 30, vars=("'cls'", "metaclass class", "metacls")), - B902(68, 27, vars=("'metacls'", "metaclass instance", "cls")), - B902(72, 13, vars=("'metacls'", "metaclass instance", "cls")), - B902(76, 13, vars=("'metacls'", "metaclass instance", "cls")), - B902(80, 13, vars=("'metacls'", "metaclass instance", "cls")), - B902(84, 13, vars=("'metacls'", "metaclass instance", "cls")), - ), - ) - - def test_b902_py38(self): - filename = Path(__file__).absolute().parent / "b902_py38.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B902(22, 17, vars=("'i_am_special'", "instance", "self")), - B902(24, 30, vars=("'cls'", "instance", "self")), - B902(26, 4, vars=("(none)", "instance", "self")), - B902(29, 12, vars=("'self'", "class", "cls")), - B902(31, 22, vars=("*args", "instance", "self")), - B902(36, 30, vars=("**kwargs", "instance", "self")), - B902(38, 32, vars=("*, self", "instance", "self")), - B902(40, 44, vars=("*, self", "instance", "self")), - B902(52, 17, vars=("'self'", "metaclass instance", "cls")), - B902(55, 20, vars=("'cls'", "metaclass class", "metacls")), - ), - ) - - def test_b903(self): - filename = Path(__file__).absolute().parent / "b903.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual(errors, self.errors(B903(31, 0), B903(37, 0))) - - def test_b904(self): - filename = Path(__file__).absolute().parent / "b904.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = [ - B904(10, 8, vars=("",)), - B904(11, 4, vars=("",)), - B904(16, 4, vars=("",)), - B904(55, 16, vars=("",)), - ] - self.assertEqual(errors, self.errors(*expected)) - - @unittest.skipIf(sys.version_info < (3, 11), "requires 3.11+") - def test_b904_py311(self): - filename = Path(__file__).absolute().parent / "b904_py311.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = [ - B904(10, 8, vars=("*",)), - B904(11, 4, vars=("*",)), - B904(16, 4, vars=("*",)), - B904(55, 16, vars=("*",)), - ] - self.assertEqual(errors, self.errors(*expected)) - - @unittest.skipIf(sys.version_info < (3, 10), "requires 3.10+") - def test_b905(self): - filename = Path(__file__).absolute().parent / "b905_py310.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = [ - B905(1, 0), - B905(2, 0), - B905(3, 0), - B905(4, 0), - B905(4, 15), - B905(5, 4), - B905(6, 0), - B905(21, 0), - B905(22, 0), - ] - self.assertEqual(errors, self.errors(*expected)) - - def test_b906(self): - filename = Path(__file__).absolute().parent / "b906.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = [ - B906(9, 0), - ] - self.assertEqual(errors, self.errors(*expected)) - - def test_b950(self): - filename = Path(__file__).absolute().parent / "b950.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - self.assertEqual( - errors, - self.errors( - B950(7, 92, vars=(92, 79)), - B950(12, 103, vars=(103, 79)), - B950(14, 103, vars=(103, 79)), - B950(21, 97, vars=(97, 79)), - B950(36, 104, vars=(104, 79)), - B950(37, 104, vars=(104, 79)), - ), - ) - def test_b9_select(self): filename = Path(__file__).absolute().parent / "b950.py" mock_options = Namespace(select=["B950"]) bbc = BugBearChecker(filename=str(filename), options=mock_options) errors = list(bbc.run()) + B950 = error_codes["B950"] self.assertEqual( errors, self.errors( - B950(7, 92, vars=(92, 79)), - B950(12, 103, vars=(103, 79)), - B950(14, 103, vars=(103, 79)), - B950(21, 97, vars=(97, 79)), - B950(36, 104, vars=(104, 79)), - B950(37, 104, vars=(104, 79)), + B950(7, 113, vars=(113, 79)), + B950(12, 125, vars=(125, 79)), + B950(14, 125, vars=(125, 79)), + B950(21, 118, vars=(118, 79)), + B950(36, 132, vars=(132, 79)), + B950(37, 140, vars=(140, 79)), ), ) @@ -1089,15 +161,16 @@ def test_b9_extend_select(self): mock_options = Namespace(select=[], extend_select=["B950"]) bbc = BugBearChecker(filename=str(filename), options=mock_options) errors = list(bbc.run()) + B950 = error_codes["B950"] self.assertEqual( errors, self.errors( - B950(7, 92, vars=(92, 79)), - B950(12, 103, vars=(103, 79)), - B950(14, 103, vars=(103, 79)), - B950(21, 97, vars=(97, 79)), - B950(36, 104, vars=(104, 79)), - B950(37, 104, vars=(104, 79)), + B950(7, 113, vars=(113, 79)), + B950(12, 125, vars=(125, 79)), + B950(14, 125, vars=(125, 79)), + B950(21, 118, vars=(118, 79)), + B950(36, 132, vars=(132, 79)), + B950(37, 140, vars=(140, 79)), ), ) @@ -1133,73 +206,6 @@ def test_selfclean_test_bugbear(self): self.assertEqual(proc.stdout, b"") self.assertEqual(proc.stderr, b"") - def test_b909(self): - filename = Path(__file__).absolute().parent / "b909.py" - mock_options = Namespace(select=[], extend_select=["B909"]) - bbc = BugBearChecker(filename=str(filename), options=mock_options) - errors = list(bbc.run()) - expected = [ - B909(12, 4), - B909(13, 4), - B909(14, 4), - B909(15, 4), - B909(16, 4), - B909(17, 4), - B909(18, 4), - B909(19, 4), - B909(20, 4), - B909(21, 4), - B909(25, 8), - B909(47, 4), - B909(48, 4), - B909(49, 4), - B909(62, 4), - B909(63, 4), - B909(64, 4), - B909(65, 4), - B909(66, 4), - B909(67, 4), - B909(84, 4), - B909(85, 4), - B909(93, 4), - B909(94, 4), - B909(95, 4), - B909(96, 4), - B909(97, 4), - B909(102, 4), - B909(103, 4), - B909(104, 4), - B909(105, 4), - B909(125, 4), - ] - self.assertEqual(errors, self.errors(*expected)) - - def test_b910(self): - filename = Path(__file__).absolute().parent / "b910.py" - mock_options = Namespace(select=[], extend_select=["B910"]) - bbc = BugBearChecker(filename=str(filename), options=mock_options) - errors = list(bbc.run()) - expected = [ - B910(3, 4), - B910(8, 4), - ] - self.assertEqual(errors, self.errors(*expected)) - - @unittest.skipIf(sys.version_info < (3, 13), "requires 3.13+") - def test_b911(self): - filename = Path(__file__).absolute().parent / "b911_py313.py" - bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) - expected = [ - B911(5, 0), - B911(6, 0), - B911(7, 0), - B911(8, 0), - B911(9, 0), - B911(10, 0), - ] - self.assertEqual(errors, self.errors(*expected)) - class TestFuzz(unittest.TestCase): from hypothesis import HealthCheck, given, settings diff --git a/tox.ini b/tox.ini index 5a58747..7f0b7e7 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ python = 3.11: py311,pep8_naming 3.12: py312 3.13: py313,mypy - # blocked by https://github.com/PyO3/pyo3/issues/5107 + # blocked by https://github.com/ijl/orjson/issues/569 # 3.14: py314 [testenv] From 536c8b4f35e8dc9a09d433806800500e5228e578 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 22 May 2025 17:08:25 +0200 Subject: [PATCH 2/7] use pytest --- tests/test_bugbear.py | 5 +---- tox.ini | 6 ++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index b67fc99..4fac0fe 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -211,6 +211,7 @@ class TestFuzz(unittest.TestCase): from hypothesis import HealthCheck, given, settings from hypothesmith import from_grammar + @pytest.mark.filterwarnings("ignore::SyntaxWarning") @settings(suppress_health_check=[HealthCheck.too_slow]) @given(from_grammar().map(ast.parse)) def test_does_not_crash_on_any_valid_code(self, syntax_tree): @@ -250,7 +251,3 @@ def test_does_not_crash_on_call_in_except_statement(self): "foo = lambda: IOError\ntry:\n ...\nexcept (foo(),):\n ...\n" ) BugBearVisitor(filename="", lines=[]).visit(syntax_tree) - - -if __name__ == "__main__": - unittest.main() diff --git a/tox.ini b/tox.ini index 7f0b7e7..9e2d842 100644 --- a/tox.ini +++ b/tox.ini @@ -19,8 +19,9 @@ deps = coverage hypothesis hypothesmith + pytest commands = - coverage run tests/test_bugbear.py {posargs} + coverage run -m pytest tests/test_bugbear.py {posargs} coverage report -m [testenv:pep8_naming] @@ -28,9 +29,10 @@ deps = coverage hypothesis hypothesmith + pytest pep8-naming commands = - coverage run tests/test_bugbear.py -k b902 {posargs} + coverage run -m pytest tests/test_bugbear.py -k b902 {posargs} coverage report -m [testenv:{py39-,py310-,py311-,py312-,py313-}mypy] From 428d3b671f4c71c50c08e171e2f3a4058a4b9499 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 22 May 2025 17:15:59 +0200 Subject: [PATCH 3/7] fix lower python versions --- tests/test_bugbear.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 4fac0fe..1b31e9b 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -1,9 +1,12 @@ +from __future__ import annotations + import ast import itertools import os import re import site import subprocess +import sys import unittest from argparse import Namespace from pathlib import Path @@ -24,12 +27,23 @@ ) +def check_version(test: str) -> None: + python_version = re.search(r"(?<=_PY)\d*", test) + if python_version: + version_str = python_version.group() + major, minor = version_str[0], version_str[1:] + v_i = sys.version_info + if (v_i.major, v_i.minor) < (int(major), int(minor)): + pytest.skip(f"python version {v_i} smaller than {major}, {minor}") + + @pytest.mark.parametrize(("test", "path"), test_files, ids=[f[0] for f in test_files]) def test_eval( test: str, path: Path, ): - print(test, path) + check_version(test) + content = path.read_text() expected, options = _parse_eval_file(test, content) assert expected From ebf48f7a1fd1a0dc222b3fad0eb398dc002f2d43 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 23 May 2025 12:23:51 +0200 Subject: [PATCH 4/7] add instructions to DEVELOPMENT.md, move eval files to tests/eval_files --- .pre-commit-config.yaml | 2 +- DEVELOPMENT.md | 31 +++++++++++++++++++++--- pyproject.toml | 2 +- tests/{ => eval_files}/b001.py | 0 tests/{ => eval_files}/b002.py | 0 tests/{ => eval_files}/b003.py | 0 tests/{ => eval_files}/b004.py | 0 tests/{ => eval_files}/b005.py | 0 tests/{ => eval_files}/b006_b008.py | 0 tests/{ => eval_files}/b007.py | 0 tests/{ => eval_files}/b008_extended.py | 0 tests/{ => eval_files}/b009_b010.py | 0 tests/{ => eval_files}/b011.py | 0 tests/{ => eval_files}/b012.py | 0 tests/{ => eval_files}/b012_py311.py | 0 tests/{ => eval_files}/b013.py | 0 tests/{ => eval_files}/b013_py311.py | 0 tests/{ => eval_files}/b014.py | 0 tests/{ => eval_files}/b014_py311.py | 0 tests/{ => eval_files}/b015.py | 0 tests/{ => eval_files}/b016.py | 0 tests/{ => eval_files}/b017.py | 0 tests/{ => eval_files}/b018_classes.py | 0 tests/{ => eval_files}/b018_functions.py | 0 tests/{ => eval_files}/b018_modules.py | 0 tests/{ => eval_files}/b018_nested.py | 0 tests/{ => eval_files}/b019.py | 0 tests/{ => eval_files}/b020.py | 0 tests/{ => eval_files}/b021.py | 0 tests/{ => eval_files}/b022.py | 0 tests/{ => eval_files}/b023.py | 0 tests/{ => eval_files}/b024.py | 0 tests/{ => eval_files}/b025.py | 0 tests/{ => eval_files}/b025_py311.py | 0 tests/{ => eval_files}/b026.py | 0 tests/{ => eval_files}/b027.py | 0 tests/{ => eval_files}/b028.py | 0 tests/{ => eval_files}/b029.py | 0 tests/{ => eval_files}/b029_py311.py | 0 tests/{ => eval_files}/b030.py | 0 tests/{ => eval_files}/b030_py311.py | 0 tests/{ => eval_files}/b031.py | 0 tests/{ => eval_files}/b032.py | 0 tests/{ => eval_files}/b033.py | 0 tests/{ => eval_files}/b034.py | 0 tests/{ => eval_files}/b035.py | 0 tests/{ => eval_files}/b036.py | 0 tests/{ => eval_files}/b036_py311.py | 0 tests/{ => eval_files}/b037.py | 0 tests/{ => eval_files}/b039.py | 0 tests/{ => eval_files}/b040.py | 0 tests/{ => eval_files}/b041.py | 0 tests/{ => eval_files}/b901.py | 0 tests/{ => eval_files}/b902.py | 0 tests/{ => eval_files}/b902_extended.py | 2 +- tests/{ => eval_files}/b902_py38.py | 0 tests/{ => eval_files}/b903.py | 0 tests/{ => eval_files}/b904.py | 0 tests/{ => eval_files}/b904_py311.py | 0 tests/{ => eval_files}/b905_py310.py | 0 tests/{ => eval_files}/b906.py | 0 tests/{ => eval_files}/b907_py312.py | 0 tests/{ => eval_files}/b908.py | 0 tests/{ => eval_files}/b909.py | 0 tests/{ => eval_files}/b910.py | 0 tests/{ => eval_files}/b911_py313.py | 0 tests/{ => eval_files}/b950.py | 0 tests/test_bugbear.py | 17 +++++++++---- 68 files changed, 43 insertions(+), 11 deletions(-) rename tests/{ => eval_files}/b001.py (100%) rename tests/{ => eval_files}/b002.py (100%) rename tests/{ => eval_files}/b003.py (100%) rename tests/{ => eval_files}/b004.py (100%) rename tests/{ => eval_files}/b005.py (100%) rename tests/{ => eval_files}/b006_b008.py (100%) rename tests/{ => eval_files}/b007.py (100%) rename tests/{ => eval_files}/b008_extended.py (100%) rename tests/{ => eval_files}/b009_b010.py (100%) rename tests/{ => eval_files}/b011.py (100%) rename tests/{ => eval_files}/b012.py (100%) rename tests/{ => eval_files}/b012_py311.py (100%) rename tests/{ => eval_files}/b013.py (100%) rename tests/{ => eval_files}/b013_py311.py (100%) rename tests/{ => eval_files}/b014.py (100%) rename tests/{ => eval_files}/b014_py311.py (100%) rename tests/{ => eval_files}/b015.py (100%) rename tests/{ => eval_files}/b016.py (100%) rename tests/{ => eval_files}/b017.py (100%) rename tests/{ => eval_files}/b018_classes.py (100%) rename tests/{ => eval_files}/b018_functions.py (100%) rename tests/{ => eval_files}/b018_modules.py (100%) rename tests/{ => eval_files}/b018_nested.py (100%) rename tests/{ => eval_files}/b019.py (100%) rename tests/{ => eval_files}/b020.py (100%) rename tests/{ => eval_files}/b021.py (100%) rename tests/{ => eval_files}/b022.py (100%) rename tests/{ => eval_files}/b023.py (100%) rename tests/{ => eval_files}/b024.py (100%) rename tests/{ => eval_files}/b025.py (100%) rename tests/{ => eval_files}/b025_py311.py (100%) rename tests/{ => eval_files}/b026.py (100%) rename tests/{ => eval_files}/b027.py (100%) rename tests/{ => eval_files}/b028.py (100%) rename tests/{ => eval_files}/b029.py (100%) rename tests/{ => eval_files}/b029_py311.py (100%) rename tests/{ => eval_files}/b030.py (100%) rename tests/{ => eval_files}/b030_py311.py (100%) rename tests/{ => eval_files}/b031.py (100%) rename tests/{ => eval_files}/b032.py (100%) rename tests/{ => eval_files}/b033.py (100%) rename tests/{ => eval_files}/b034.py (100%) rename tests/{ => eval_files}/b035.py (100%) rename tests/{ => eval_files}/b036.py (100%) rename tests/{ => eval_files}/b036_py311.py (100%) rename tests/{ => eval_files}/b037.py (100%) rename tests/{ => eval_files}/b039.py (100%) rename tests/{ => eval_files}/b040.py (100%) rename tests/{ => eval_files}/b041.py (100%) rename tests/{ => eval_files}/b901.py (100%) rename tests/{ => eval_files}/b902.py (100%) rename tests/{ => eval_files}/b902_extended.py (98%) rename tests/{ => eval_files}/b902_py38.py (100%) rename tests/{ => eval_files}/b903.py (100%) rename tests/{ => eval_files}/b904.py (100%) rename tests/{ => eval_files}/b904_py311.py (100%) rename tests/{ => eval_files}/b905_py310.py (100%) rename tests/{ => eval_files}/b906.py (100%) rename tests/{ => eval_files}/b907_py312.py (100%) rename tests/{ => eval_files}/b908.py (100%) rename tests/{ => eval_files}/b909.py (100%) rename tests/{ => eval_files}/b910.py (100%) rename tests/{ => eval_files}/b911_py313.py (100%) rename tests/{ => eval_files}/b950.py (100%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be47cc6..db4703b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: hooks: - id: flake8 additional_dependencies: [flake8-bugbear] - exclude: ^tests/b.* + exclude: ^tests/eval_files/.* - repo: https://github.com/rstcheck/rstcheck rev: v6.2.4 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6ebde17..9d3c8a6 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -42,12 +42,37 @@ cd flake8-bugbear /path/to/venv/bin/pip install -e '.[dev]' ``` +## Writing Tests + +flake8-bugbear has a test runner that will go through all files in `tests/eval_files/`, run them through the linter, and check that they emit the appropriate error messages. + +The expected errors are specified by adding comments on the line where the error is expected, using the format `# : [, ][, ][...]`. E.g. +```python +x = ++n # B002: 4 +try: + ... +except* (ValueError,): # B013: 0, "ValueError", "*" + ... +``` +The error code should be in the `error_codes` dict, and the other values are passed to `eval` so should be valid python objects. + +You can also specify options to be passed to `BugBearChecker` with an `# OPTIONS` comments +```python +# OPTIONS: extend_immutable_calls=["fastapi.Depends", "fastapi.Query"] +# OPTIONS: classmethod_decorators=["mylibrary.makeclassmethod", "validator"], select=["B902"] +``` + +If you specify a python version somewhere in the file name with `_pyXX`, the file will be skipped on smaller versions. Otherwise the name has no impact on the test, and you can test multiple errors in the same file. + +The infrastructure is based on the test runner in https://github.com/python-trio/flake8-async which has some additional features that can be pulled into flake8-bugbear when desired. + + ## Running Tests flake8-bugbear uses coverage to run standard unittest tests. ```console -/path/to/venv/bin/coverage run tests/test_bugbear.py +/path/to/venv/bin/coverage run -m pytest tests/test_bugbear.py ``` You can also use [tox](https://tox.wiki/en/latest/index.html) to test with multiple different python versions, emulating what the CI does. @@ -55,10 +80,10 @@ You can also use [tox](https://tox.wiki/en/latest/index.html) to test with multi ```console /path/to/venv/bin/tox ``` -will by default run all tests on python versions 3.8 through 3.12. If you only want to test a specific version you can specify the environment with `-e` +will by default run all tests on python versions 3.9 through 3.13. If you only want to test a specific version you can specify the environment with `-e` ```console -/path/to/venv/bin/tox -e py38 +/path/to/venv/bin/tox -e py313 ``` ## Running linter diff --git a/pyproject.toml b/pyproject.toml index b9cacd1..01a0bc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,6 @@ profile = "black" [tool.black] force-exclude = ''' ( - ^/tests\/b.* + ^/tests\/eval_files\/.* ) ''' diff --git a/tests/b001.py b/tests/eval_files/b001.py similarity index 100% rename from tests/b001.py rename to tests/eval_files/b001.py diff --git a/tests/b002.py b/tests/eval_files/b002.py similarity index 100% rename from tests/b002.py rename to tests/eval_files/b002.py diff --git a/tests/b003.py b/tests/eval_files/b003.py similarity index 100% rename from tests/b003.py rename to tests/eval_files/b003.py diff --git a/tests/b004.py b/tests/eval_files/b004.py similarity index 100% rename from tests/b004.py rename to tests/eval_files/b004.py diff --git a/tests/b005.py b/tests/eval_files/b005.py similarity index 100% rename from tests/b005.py rename to tests/eval_files/b005.py diff --git a/tests/b006_b008.py b/tests/eval_files/b006_b008.py similarity index 100% rename from tests/b006_b008.py rename to tests/eval_files/b006_b008.py diff --git a/tests/b007.py b/tests/eval_files/b007.py similarity index 100% rename from tests/b007.py rename to tests/eval_files/b007.py diff --git a/tests/b008_extended.py b/tests/eval_files/b008_extended.py similarity index 100% rename from tests/b008_extended.py rename to tests/eval_files/b008_extended.py diff --git a/tests/b009_b010.py b/tests/eval_files/b009_b010.py similarity index 100% rename from tests/b009_b010.py rename to tests/eval_files/b009_b010.py diff --git a/tests/b011.py b/tests/eval_files/b011.py similarity index 100% rename from tests/b011.py rename to tests/eval_files/b011.py diff --git a/tests/b012.py b/tests/eval_files/b012.py similarity index 100% rename from tests/b012.py rename to tests/eval_files/b012.py diff --git a/tests/b012_py311.py b/tests/eval_files/b012_py311.py similarity index 100% rename from tests/b012_py311.py rename to tests/eval_files/b012_py311.py diff --git a/tests/b013.py b/tests/eval_files/b013.py similarity index 100% rename from tests/b013.py rename to tests/eval_files/b013.py diff --git a/tests/b013_py311.py b/tests/eval_files/b013_py311.py similarity index 100% rename from tests/b013_py311.py rename to tests/eval_files/b013_py311.py diff --git a/tests/b014.py b/tests/eval_files/b014.py similarity index 100% rename from tests/b014.py rename to tests/eval_files/b014.py diff --git a/tests/b014_py311.py b/tests/eval_files/b014_py311.py similarity index 100% rename from tests/b014_py311.py rename to tests/eval_files/b014_py311.py diff --git a/tests/b015.py b/tests/eval_files/b015.py similarity index 100% rename from tests/b015.py rename to tests/eval_files/b015.py diff --git a/tests/b016.py b/tests/eval_files/b016.py similarity index 100% rename from tests/b016.py rename to tests/eval_files/b016.py diff --git a/tests/b017.py b/tests/eval_files/b017.py similarity index 100% rename from tests/b017.py rename to tests/eval_files/b017.py diff --git a/tests/b018_classes.py b/tests/eval_files/b018_classes.py similarity index 100% rename from tests/b018_classes.py rename to tests/eval_files/b018_classes.py diff --git a/tests/b018_functions.py b/tests/eval_files/b018_functions.py similarity index 100% rename from tests/b018_functions.py rename to tests/eval_files/b018_functions.py diff --git a/tests/b018_modules.py b/tests/eval_files/b018_modules.py similarity index 100% rename from tests/b018_modules.py rename to tests/eval_files/b018_modules.py diff --git a/tests/b018_nested.py b/tests/eval_files/b018_nested.py similarity index 100% rename from tests/b018_nested.py rename to tests/eval_files/b018_nested.py diff --git a/tests/b019.py b/tests/eval_files/b019.py similarity index 100% rename from tests/b019.py rename to tests/eval_files/b019.py diff --git a/tests/b020.py b/tests/eval_files/b020.py similarity index 100% rename from tests/b020.py rename to tests/eval_files/b020.py diff --git a/tests/b021.py b/tests/eval_files/b021.py similarity index 100% rename from tests/b021.py rename to tests/eval_files/b021.py diff --git a/tests/b022.py b/tests/eval_files/b022.py similarity index 100% rename from tests/b022.py rename to tests/eval_files/b022.py diff --git a/tests/b023.py b/tests/eval_files/b023.py similarity index 100% rename from tests/b023.py rename to tests/eval_files/b023.py diff --git a/tests/b024.py b/tests/eval_files/b024.py similarity index 100% rename from tests/b024.py rename to tests/eval_files/b024.py diff --git a/tests/b025.py b/tests/eval_files/b025.py similarity index 100% rename from tests/b025.py rename to tests/eval_files/b025.py diff --git a/tests/b025_py311.py b/tests/eval_files/b025_py311.py similarity index 100% rename from tests/b025_py311.py rename to tests/eval_files/b025_py311.py diff --git a/tests/b026.py b/tests/eval_files/b026.py similarity index 100% rename from tests/b026.py rename to tests/eval_files/b026.py diff --git a/tests/b027.py b/tests/eval_files/b027.py similarity index 100% rename from tests/b027.py rename to tests/eval_files/b027.py diff --git a/tests/b028.py b/tests/eval_files/b028.py similarity index 100% rename from tests/b028.py rename to tests/eval_files/b028.py diff --git a/tests/b029.py b/tests/eval_files/b029.py similarity index 100% rename from tests/b029.py rename to tests/eval_files/b029.py diff --git a/tests/b029_py311.py b/tests/eval_files/b029_py311.py similarity index 100% rename from tests/b029_py311.py rename to tests/eval_files/b029_py311.py diff --git a/tests/b030.py b/tests/eval_files/b030.py similarity index 100% rename from tests/b030.py rename to tests/eval_files/b030.py diff --git a/tests/b030_py311.py b/tests/eval_files/b030_py311.py similarity index 100% rename from tests/b030_py311.py rename to tests/eval_files/b030_py311.py diff --git a/tests/b031.py b/tests/eval_files/b031.py similarity index 100% rename from tests/b031.py rename to tests/eval_files/b031.py diff --git a/tests/b032.py b/tests/eval_files/b032.py similarity index 100% rename from tests/b032.py rename to tests/eval_files/b032.py diff --git a/tests/b033.py b/tests/eval_files/b033.py similarity index 100% rename from tests/b033.py rename to tests/eval_files/b033.py diff --git a/tests/b034.py b/tests/eval_files/b034.py similarity index 100% rename from tests/b034.py rename to tests/eval_files/b034.py diff --git a/tests/b035.py b/tests/eval_files/b035.py similarity index 100% rename from tests/b035.py rename to tests/eval_files/b035.py diff --git a/tests/b036.py b/tests/eval_files/b036.py similarity index 100% rename from tests/b036.py rename to tests/eval_files/b036.py diff --git a/tests/b036_py311.py b/tests/eval_files/b036_py311.py similarity index 100% rename from tests/b036_py311.py rename to tests/eval_files/b036_py311.py diff --git a/tests/b037.py b/tests/eval_files/b037.py similarity index 100% rename from tests/b037.py rename to tests/eval_files/b037.py diff --git a/tests/b039.py b/tests/eval_files/b039.py similarity index 100% rename from tests/b039.py rename to tests/eval_files/b039.py diff --git a/tests/b040.py b/tests/eval_files/b040.py similarity index 100% rename from tests/b040.py rename to tests/eval_files/b040.py diff --git a/tests/b041.py b/tests/eval_files/b041.py similarity index 100% rename from tests/b041.py rename to tests/eval_files/b041.py diff --git a/tests/b901.py b/tests/eval_files/b901.py similarity index 100% rename from tests/b901.py rename to tests/eval_files/b901.py diff --git a/tests/b902.py b/tests/eval_files/b902.py similarity index 100% rename from tests/b902.py rename to tests/eval_files/b902.py diff --git a/tests/b902_extended.py b/tests/eval_files/b902_extended.py similarity index 98% rename from tests/b902_extended.py rename to tests/eval_files/b902_extended.py index 87fa4e6..9e1e9e4 100644 --- a/tests/b902_extended.py +++ b/tests/eval_files/b902_extended.py @@ -1,4 +1,4 @@ -# OPTIONS: classmethod_decorators=["mylibrary.makeclassmethod", "validator"], select=["B902"], +# OPTIONS: classmethod_decorators=["mylibrary.makeclassmethod", "validator"], select=["B902"] class Errors: # correctly registered as classmethod @validator diff --git a/tests/b902_py38.py b/tests/eval_files/b902_py38.py similarity index 100% rename from tests/b902_py38.py rename to tests/eval_files/b902_py38.py diff --git a/tests/b903.py b/tests/eval_files/b903.py similarity index 100% rename from tests/b903.py rename to tests/eval_files/b903.py diff --git a/tests/b904.py b/tests/eval_files/b904.py similarity index 100% rename from tests/b904.py rename to tests/eval_files/b904.py diff --git a/tests/b904_py311.py b/tests/eval_files/b904_py311.py similarity index 100% rename from tests/b904_py311.py rename to tests/eval_files/b904_py311.py diff --git a/tests/b905_py310.py b/tests/eval_files/b905_py310.py similarity index 100% rename from tests/b905_py310.py rename to tests/eval_files/b905_py310.py diff --git a/tests/b906.py b/tests/eval_files/b906.py similarity index 100% rename from tests/b906.py rename to tests/eval_files/b906.py diff --git a/tests/b907_py312.py b/tests/eval_files/b907_py312.py similarity index 100% rename from tests/b907_py312.py rename to tests/eval_files/b907_py312.py diff --git a/tests/b908.py b/tests/eval_files/b908.py similarity index 100% rename from tests/b908.py rename to tests/eval_files/b908.py diff --git a/tests/b909.py b/tests/eval_files/b909.py similarity index 100% rename from tests/b909.py rename to tests/eval_files/b909.py diff --git a/tests/b910.py b/tests/eval_files/b910.py similarity index 100% rename from tests/b910.py rename to tests/eval_files/b910.py diff --git a/tests/b911_py313.py b/tests/eval_files/b911_py313.py similarity index 100% rename from tests/b911_py313.py rename to tests/eval_files/b911_py313.py diff --git a/tests/b950.py b/tests/eval_files/b950.py similarity index 100% rename from tests/b950.py rename to tests/eval_files/b950.py diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 1b31e9b..80b6921 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -20,10 +20,12 @@ error_codes, ) +EVAL_FILES_DIR = Path(__file__).parent / "eval_files" + test_files: list[tuple[str, Path]] = sorted( (f.stem.upper(), f) - for f in (Path(__file__).parent).iterdir() - if re.fullmatch(r"b\d\d\d.*\.py", f.name) + for f in EVAL_FILES_DIR.iterdir() + if re.fullmatch(r".*.py", f.name) ) @@ -65,6 +67,11 @@ def _parse_eval_file(test: str, content: str) -> tuple[list[error], Namespace | for lineno, line in enumerate(content.split("\n"), start=1): if line.startswith("# OPTIONS:"): + assert options is None, ( + "Multiple '# OPTIONS' found in file. You can specify multiple options" + " in the same line, but if you want something more readable you may" + " want to upgrade the logic in this function." + ) options = eval(f"Namespace({line[10:]})") # skip commented out lines @@ -149,7 +156,7 @@ def test_b907_format_specifier_permutations(self): ), f"b907 raised for {format_spec} that would look different with !r" def test_b9_select(self): - filename = Path(__file__).absolute().parent / "b950.py" + filename = EVAL_FILES_DIR / "b950.py" mock_options = Namespace(select=["B950"]) bbc = BugBearChecker(filename=str(filename), options=mock_options) @@ -168,7 +175,7 @@ def test_b9_select(self): ) def test_b9_extend_select(self): - filename = Path(__file__).absolute().parent / "b950.py" + filename = EVAL_FILES_DIR / "b950.py" # select is always going to have a value, usually the default codes, but can # also be empty @@ -189,7 +196,7 @@ def test_b9_extend_select(self): ) def test_b9_flake8_next_default_options(self): - filename = Path(__file__).absolute().parent / "b950.py" + filename = EVAL_FILES_DIR / "b950.py" # in flake8 next, unset select / extend_select will be `None` to # signify the default values From e30efda052ce5337c8134b0cb41ebd9df157a034 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 23 May 2025 12:24:39 +0200 Subject: [PATCH 5/7] remove old commented-out code --- tests/test_bugbear.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 80b6921..b01f5bd 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -60,8 +60,6 @@ def test_eval( def _parse_eval_file(test: str, content: str) -> tuple[list[error], Namespace | None]: - - # error_class: Any = eval(error_code) expected: list[error] = [] options: Namespace | None = None From 8aa22ab299271a3509e61211ad0cb1ec3a868a8c Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 23 May 2025 18:04:56 +0200 Subject: [PATCH 6/7] clarified error message --- DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9d3c8a6..4e05c6f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -46,7 +46,7 @@ cd flake8-bugbear flake8-bugbear has a test runner that will go through all files in `tests/eval_files/`, run them through the linter, and check that they emit the appropriate error messages. -The expected errors are specified by adding comments on the line where the error is expected, using the format `# : [, ][, ][...]`. E.g. +The expected errors are specified by adding comments on the line where the error is expected. The format consists of the error code, followed by a comma-separated list of the `col_offset` as well as `vars` that are used when `str.format`ing the full error message. ```python x = ++n # B002: 4 try: @@ -54,7 +54,7 @@ try: except* (ValueError,): # B013: 0, "ValueError", "*" ... ``` -The error code should be in the `error_codes` dict, and the other values are passed to `eval` so should be valid python objects. +The error code should be in the `error_codes` dict, and the other values are `eval`'d as if in a `tuple` and should be valid python objects. (I.e. remember to quote strings) You can also specify options to be passed to `BugBearChecker` with an `# OPTIONS` comments ```python From f7d5123f7aaefe27b9a1e8bbdf76b26157dfc336 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 23 May 2025 18:09:59 +0200 Subject: [PATCH 7/7] fix comment (it applied to flake8-async, but this implementation does not allow # error: ...) --- tests/test_bugbear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index b01f5bd..04835cf 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -80,7 +80,7 @@ def _parse_eval_file(test: str, content: str) -> tuple[list[error], Namespace | if "#" not in line: continue - # get text between `error:` and (end of line or another comment) + # get text between `B\d\d\d:` and (end of line or another comment) k = re.findall(r"(B\d\d\d):([^#]*)(?=#|$)", line) for err_code, err_args in k: # evaluate the arguments as if in a tuple