Skip to content

Commit

Permalink
Set up mypy (#482)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Jul 2, 2024
1 parent 3157b89 commit b15feed
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 43 deletions.
76 changes: 35 additions & 41 deletions bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from typing import Dict, Iterable, Iterator, List, Set, Union

import attr
import pycodestyle
import pycodestyle # type: ignore[import-untyped]

__version__ = "24.4.26"

Expand Down Expand Up @@ -259,7 +259,7 @@ def _check_redundant_excepthandlers(names, node):
# Remove redundant exceptions that the automatic system either handles
# poorly (usually aliases) or can't be checked (e.g. it's not an
# built-in exception).
for primary, equivalents in B014.redundant_exceptions.items():
for primary, equivalents in B014_REDUNDANT_EXCEPTIONS.items():
if primary in good:
good = [g for g in good if g not in equivalents]

Expand Down Expand Up @@ -366,17 +366,16 @@ class B040CaughtException:
class BugBearVisitor(ast.NodeVisitor):
filename = attr.ib()
lines = attr.ib()
b008_b039_extend_immutable_calls = attr.ib(factory=set)
b902_classmethod_decorators = attr.ib(factory=set)
node_window = attr.ib(factory=list)
errors = attr.ib(factory=list)
futures = attr.ib(factory=set)
contexts = attr.ib(factory=list)
b008_b039_extend_immutable_calls: set[str] = attr.ib(factory=set)
b902_classmethod_decorators: set[str] = attr.ib(factory=set)
node_window: list[ast.AST] = attr.ib(factory=list)
errors: list[error] = attr.ib(factory=list)
contexts: list[Context] = attr.ib(factory=list)
b040_caught_exception: B040CaughtException | None = attr.ib(default=None)

NODE_WINDOW_SIZE = 4
_b023_seen = attr.ib(factory=set, init=False)
_b005_imports = attr.ib(factory=set, init=False)
_b023_seen: set[error] = attr.ib(factory=set, init=False)
_b005_imports: set[str] = attr.ib(factory=set, init=False)

if False:
# Useful for tracing what the hell is going on.
Expand Down Expand Up @@ -641,7 +640,7 @@ def check_for_b005(self, node):
for name in node.names:
self._b005_imports.add(f"{node.module}.{name.name or name.asname}")
elif isinstance(node, ast.Call):
if node.func.attr not in B005.methods:
if node.func.attr not in B005_METHODS:
return # method name doesn't match

if (
Expand All @@ -657,10 +656,6 @@ def check_for_b005(self, node):
):
return # used arguments don't match the builtin strip

call_path = ".".join(compose_call_path(node.func.value))
if call_path in B005.valid_paths:
return # path is exempt

value = node.args[0].value
if len(value) == 1:
return # stripping just one character
Expand Down Expand Up @@ -843,7 +838,7 @@ def check_for_b019(self, node):
if decorator in {"classmethod", "staticmethod"}:
return

if decorator in B019.caches:
if decorator in B019_CACHES:
self.errors.append(
B019(
node.decorator_list[idx].lineno,
Expand Down Expand Up @@ -1229,7 +1224,7 @@ def check_for_b902( # noqa: C901 (too complex)
def is_classmethod(decorators: Set[str]) -> bool:
return (
any(name in decorators for name in self.b902_classmethod_decorators)
or node.name in B902.implicit_classmethods
or node.name in B902_IMPLICIT_CLASSMETHODS
)

if len(self.contexts) < 2 or not isinstance(
Expand All @@ -1251,17 +1246,17 @@ def is_classmethod(decorators: Set[str]) -> bool:
bases = {b.id for b in cls.bases if isinstance(b, ast.Name)}
if any(basetype in bases for basetype in ("type", "ABCMeta", "EnumMeta")):
if is_classmethod(decorators):
expected_first_args = B902.metacls
expected_first_args = B902_METACLS
kind = "metaclass class"
else:
expected_first_args = B902.cls
expected_first_args = B902_CLS
kind = "metaclass instance"
else:
if is_classmethod(decorators):
expected_first_args = B902.cls
expected_first_args = B902_CLS
kind = "class"
else:
expected_first_args = B902.self
expected_first_args = B902_SELF
kind = "instance"

args = getattr(node.args, "posonlyargs", []) + node.args.args
Expand Down Expand Up @@ -1708,7 +1703,7 @@ def compose_call_path(node):
yield node.id


def _tansform_slice_to_py39(slice: ast.Slice) -> ast.Slice | ast.Name:
def _transform_slice_to_py39(slice: ast.expr | ast.Slice) -> ast.Slice | ast.expr:
"""Transform a py38 style slice to a py39 style slice.
In py39 the slice was changed to have simple names directly assigned:
Expand Down Expand Up @@ -1769,18 +1764,18 @@ class B909Checker(ast.NodeVisitor):
"discard",
)

def __init__(self, name: str, key: str):
def __init__(self, name: str, key: str) -> None:
self.name = name
self.key = key
self.mutations = defaultdict(list)
self.mutations: dict[int, list[ast.AST]] = defaultdict(list)
self._conditional_block = 0

def visit_Assign(self, node: ast.Assign):
def visit_Assign(self, node: ast.Assign) -> None:
for target in node.targets:
if (
isinstance(target, ast.Subscript)
and _to_name_str(target.value) == self.name
and _to_name_str(_tansform_slice_to_py39(target.slice)) != self.key
and _to_name_str(_transform_slice_to_py39(target.slice)) != self.key
):
self.mutations[self._conditional_block].append(node)
self.generic_visit(node)
Expand Down Expand Up @@ -1897,7 +1892,7 @@ def __init__(
)
self.error_code_calls = error_code_calls
self.error_code_literals = error_code_literals
for node in B006.mutable_literals + B006.mutable_comprehensions:
for node in B006_MUTABLE_LITERALS + B006_MUTABLE_COMPREHENSIONS:
setattr(self, f"visit_{node}", self.visit_mutable_literal_or_comprehension)
self.errors = []
self.arg_depth = 0
Expand All @@ -1921,12 +1916,12 @@ def visit_mutable_literal_or_comprehension(self, node):

def visit_Call(self, node):
call_path = ".".join(compose_call_path(node.func))
if call_path in B006.mutable_calls:
if call_path in B006_MUTABLE_CALLS:
self.errors.append(self.error_code_calls(node.lineno, node.col_offset))
self.generic_visit(node)
return

if call_path in B008.immutable_calls | self.b008_b039_extend_immutable_calls:
if call_path in B008_IMMUTABLE_CALLS | self.b008_b039_extend_immutable_calls:
self.generic_visit(node)
return

Expand Down Expand Up @@ -2031,8 +2026,7 @@ def visit_Lambda(self, node):
"expressions to remove string fragments."
)
)
B005.methods = {"lstrip", "rstrip", "strip"}
B005.valid_paths = {}
B005_METHODS = {"lstrip", "rstrip", "strip"}

B006 = Error(
message=(
Expand All @@ -2044,9 +2038,9 @@ def visit_Lambda(self, node):
)

# Note: these are also used by B039
B006.mutable_literals = ("Dict", "List", "Set")
B006.mutable_comprehensions = ("ListComp", "DictComp", "SetComp")
B006.mutable_calls = {
B006_MUTABLE_LITERALS = ("Dict", "List", "Set")
B006_MUTABLE_COMPREHENSIONS = ("ListComp", "DictComp", "SetComp")
B006_MUTABLE_CALLS = {
"Counter",
"OrderedDict",
"collections.Counter",
Expand Down Expand Up @@ -2076,7 +2070,7 @@ def visit_Lambda(self, node):
)

# Note: these are also used by B039
B008.immutable_calls = {
B008_IMMUTABLE_CALLS = {
"tuple",
"frozenset",
"types.MappingProxyType",
Expand Down Expand Up @@ -2126,7 +2120,7 @@ def visit_Lambda(self, node):
"Write `except {2}{1}:`, which catches exactly the same exceptions."
)
)
B014.redundant_exceptions = {
B014_REDUNDANT_EXCEPTIONS = {
"OSError": {
# All of these are actually aliases of OSError since Python 3.3
"IOError",
Expand Down Expand Up @@ -2175,7 +2169,7 @@ def visit_Lambda(self, node):
"preventing garbage collection."
)
)
B019.caches = {
B019_CACHES = {
"functools.cache",
"functools.lru_cache",
"cache",
Expand Down Expand Up @@ -2312,10 +2306,10 @@ def visit_Lambda(self, node):
"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.
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=(
Expand Down
10 changes: 8 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# The test environment and commands
[tox]
# default environments to run without `-e`
envlist = py38, py39, py310, py311, py312, py313, pep8_naming
envlist = py38, py39, py310, py311, py312, py313, pep8_naming, mypy

[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311,pep8_naming
3.12: py312
3.12: py312,mypy
3.13-dev: py313

[testenv]
Expand All @@ -31,3 +31,9 @@ deps =
commands =
coverage run tests/test_bugbear.py -k b902 {posargs}
coverage report -m

[testenv:mypy]
deps =
mypy==1.10.1
commands =
mypy bugbear.py

0 comments on commit b15feed

Please sign in to comment.