Skip to content

Commit 159a03f

Browse files
authored
Make flake8 an optional dependency (#348)
1 parent 7c82d24 commit 159a03f

File tree

9 files changed

+113
-21
lines changed

9 files changed

+113
-21
lines changed

.github/workflows/ci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ jobs:
4444
run: python -m tox -e flake8_6
4545
- name: Run tests with flake8_7+
4646
run: python -m tox -e flake8_7
47+
- name: Run tests without flake8
48+
run: python -m tox -e noflake8 -- --no-cov
4749

4850
slow_tests:
4951
runs-on: ubuntu-latest

docs/changelog.rst

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changelog
44

55
`CalVer, YY.month.patch <https://calver.org/>`_
66

7+
25.2.3
8+
=======
9+
- No longer require ``flake8`` for installation... so if you require support for config files you must install ``flake8-async[flake8]``
10+
711
25.2.2
812
=======
913
- :ref:`ASYNC113 <async113>` now only triggers on ``trio.[serve_tcp, serve_ssl_over_tcp, serve_listeners, run_process]``, instead of accepting anything as the attribute base. (e.g. :func:`anyio.run_process` is not startable).

docs/usage.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ install and run through flake8
1717

1818
.. code-block:: sh
1919
20-
pip install flake8 flake8-async
20+
pip install flake8-async[flake8]
2121
flake8 .
2222
2323
.. _install-run-pre-commit:
@@ -33,10 +33,10 @@ adding the following to your ``.pre-commit-config.yaml``:
3333
minimum_pre_commit_version: '2.9.0'
3434
repos:
3535
- repo: https://github.com/python-trio/flake8-async
36-
rev: 25.2.2
36+
rev: 25.2.3
3737
hooks:
3838
- id: flake8-async
39-
# args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC]
39+
# args: ["--enable=ASYNC100,ASYNC112", "--disable=", "--autofix=ASYNC"]
4040
4141
This is often considerably faster for large projects, because ``pre-commit``
4242
can avoid running ``flake8-async`` on unchanged files.

flake8_async/__init__.py

+37-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939

4040
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
41-
__version__ = "25.2.2"
41+
__version__ = "25.2.3"
4242

4343

4444
# taken from https://github.com/Zac-HD/shed
@@ -127,8 +127,11 @@ def options(self) -> Options:
127127
assert self._options is not None
128128
return self._options
129129

130-
def __init__(self, tree: ast.AST, lines: Sequence[str]):
130+
def __init__(
131+
self, tree: ast.AST, lines: Sequence[str], filename: str | None = None
132+
):
131133
super().__init__()
134+
self.filename: str | None = filename
132135
self._tree = tree
133136
source = "".join(lines)
134137

@@ -139,14 +142,17 @@ def from_filename(cls, filename: str | PathLike[str]) -> Plugin: # pragma: no c
139142
# only used with --runslow
140143
with tokenize.open(filename) as f:
141144
source = f.read()
142-
return cls.from_source(source)
145+
return cls.from_source(source, filename=filename)
143146

144147
# alternative `__init__` to avoid re-splitting and/or re-joining lines
145148
@classmethod
146-
def from_source(cls, source: str) -> Plugin:
149+
def from_source(
150+
cls, source: str, filename: str | PathLike[str] | None = None
151+
) -> Plugin:
147152
plugin = Plugin.__new__(cls)
148153
super(Plugin, plugin).__init__()
149154
plugin._tree = ast.parse(source)
155+
plugin.filename = str(filename) if filename else None
150156
plugin.module = cst_parse_module_native(source)
151157
return plugin
152158

@@ -231,6 +237,13 @@ def add_options(option_manager: OptionManager | ArgumentParser):
231237
" errors."
232238
),
233239
)
240+
add_argument(
241+
"--per-file-disable",
242+
type=parse_per_file_disable,
243+
default={},
244+
required=False,
245+
help=("..."),
246+
)
234247
add_argument(
235248
"--autofix",
236249
type=comma_separated_list,
@@ -441,3 +454,23 @@ def parse_async200_dict(raw_value: str) -> dict[str, str]:
441454
)
442455
res[split_values[0]] = split_values[1]
443456
return res
457+
458+
459+
# not run if flake8 is installed
460+
def parse_per_file_disable( # pragma: no cover
461+
raw_value: str,
462+
) -> dict[str, tuple[str, ...]]:
463+
res: dict[str, tuple[str, ...]] = {}
464+
splitter = "->"
465+
values = [s.strip() for s in raw_value.split(" \t\n") if s.strip()]
466+
for value in values:
467+
split_values = list(map(str.strip, value.split(splitter)))
468+
if len(split_values) != 2:
469+
# argparse will eat this error message and spit out its own
470+
# if we raise it as ValueError
471+
raise ArgumentTypeError(
472+
f"Invalid number ({len(split_values)-1}) of splitter "
473+
f"tokens {splitter!r} in {value!r}"
474+
)
475+
res[split_values[0]] = tuple(split_values[1].split(","))
476+
return res

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def local_file(name: str) -> Path:
3434
license_files=[], # https://github.com/pypa/twine/issues/1216
3535
description="A highly opinionated flake8 plugin for Trio-related problems.",
3636
zip_safe=False,
37-
install_requires=["flake8>=6", "libcst>=1.0.1"],
37+
install_requires=["libcst>=1.0.1"],
38+
extras_require={"flake8": ["flake8>=6"]},
3839
python_requires=">=3.9",
3940
classifiers=[
4041
"Development Status :: 3 - Alpha",

tests/eval_files/async119.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async def async_with():
1515
yield # error: 8
1616

1717

18-
async def warn_on_yeach_yield():
18+
async def warn_on_each_yield():
1919
with open(""):
2020
yield # error: 8
2121
yield # error: 8

tests/test_config_and_args.py

+39-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313

1414
from .test_flake8_async import initialize_options
1515

16+
try:
17+
import flake8
18+
except ImportError:
19+
flake8 = None # type: ignore[assignment]
20+
1621
EXAMPLE_PY_TEXT = """import trio
1722
with trio.move_on_after(10):
1823
...
@@ -140,7 +145,7 @@ def test_run_100_autofix(
140145

141146
def test_114_raises_on_invalid_parameter(capsys: pytest.CaptureFixture[str]):
142147
plugin = Plugin(ast.AST(), [])
143-
# flake8 will reraise ArgumentError as SystemExit
148+
# argparse will reraise ArgumentTypeError as SystemExit
144149
for arg in "blah.foo", "foo*", "*":
145150
with pytest.raises(SystemExit):
146151
initialize_options(plugin, args=[f"--startable-in-context-manager={arg}"])
@@ -159,6 +164,7 @@ def test_200_options(capsys: pytest.CaptureFixture[str]):
159164
assert all(word in err for word in (str(i), arg, "->"))
160165

161166

167+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
162168
def test_anyio_from_config(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
163169
assert tmp_path.joinpath(".flake8").write_text(
164170
"""
@@ -228,6 +234,7 @@ async def foo():
228234
)
229235

230236

237+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
231238
def test_200_from_config_flake8_internals(
232239
tmp_path: Path, capsys: pytest.CaptureFixture[str]
233240
):
@@ -254,6 +261,7 @@ def test_200_from_config_flake8_internals(
254261
assert err_msg == out
255262

256263

264+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
257265
def test_200_from_config_subprocess(tmp_path: Path):
258266
err_msg = _test_async200_from_config_common(tmp_path)
259267
res = subprocess.run(["flake8"], cwd=tmp_path, capture_output=True, check=False)
@@ -262,6 +270,7 @@ def test_200_from_config_subprocess(tmp_path: Path):
262270
assert res.stdout == err_msg.encode("ascii")
263271

264272

273+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
265274
def test_async200_from_config_subprocess(tmp_path: Path):
266275
err_msg = _test_async200_from_config_common(tmp_path, code="trio200")
267276
res = subprocess.run(["flake8"], cwd=tmp_path, capture_output=True, check=False)
@@ -273,6 +282,7 @@ def test_async200_from_config_subprocess(tmp_path: Path):
273282
assert res.stdout == err_msg.encode("ascii")
274283

275284

285+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
276286
def test_async200_from_config_subprocess_cli_ignore(tmp_path: Path):
277287
_ = _test_async200_from_config_common(tmp_path)
278288
res = subprocess.run(
@@ -288,6 +298,20 @@ def test_async200_from_config_subprocess_cli_ignore(tmp_path: Path):
288298

289299

290300
def test_900_default_off(capsys: pytest.CaptureFixture[str]):
301+
res = subprocess.run(
302+
["flake8-async", "tests/eval_files/async900.py"],
303+
capture_output=True,
304+
check=False,
305+
encoding="utf8",
306+
)
307+
assert res.returncode == 1
308+
assert not res.stderr
309+
assert "ASYNC124" in res.stdout
310+
assert "ASYNC900" not in res.stdout
311+
312+
313+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
314+
def test_900_default_off_flake8(capsys: pytest.CaptureFixture[str]):
291315
from flake8.main.cli import main
292316

293317
returnvalue = main(
@@ -303,11 +327,18 @@ def test_900_default_off(capsys: pytest.CaptureFixture[str]):
303327

304328

305329
def test_910_can_be_selected(tmp_path: Path):
330+
"""Check if flake8 allows us to --select our 5-letter code.
331+
332+
But we can run with --enable regardless.
333+
"""
306334
myfile = tmp_path.joinpath("foo.py")
307335
myfile.write_text("""async def foo():\n print()""")
308336

337+
binary = "flake8-async" if flake8 is None else "flake8"
338+
select_enable = "enable" if flake8 is None else "select"
339+
309340
res = subprocess.run(
310-
["flake8", "--select=ASYNC910", "foo.py"],
341+
[binary, f"--{select_enable}=ASYNC910", "foo.py"],
311342
cwd=tmp_path,
312343
capture_output=True,
313344
check=False,
@@ -384,6 +415,7 @@ def _helper(*args: str, error: bool = False, autofix: bool = False) -> None:
384415
)
385416

386417

418+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
387419
def test_flake8_plugin_with_autofix_fails(tmp_path: Path):
388420
write_examplepy(tmp_path)
389421
res = subprocess.run(
@@ -453,7 +485,9 @@ def test_disable_noqa_ast(
453485
)
454486

455487

488+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
456489
def test_config_select_error_code(tmp_path: Path) -> None:
490+
# this ... seems to work? I'm confused
457491
assert tmp_path.joinpath(".flake8").write_text(
458492
"""
459493
[flake8]
@@ -469,6 +503,7 @@ def test_config_select_error_code(tmp_path: Path) -> None:
469503

470504

471505
# flake8>=6 enforces three-letter error codes in config
506+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
472507
def test_config_ignore_error_code(tmp_path: Path) -> None:
473508
assert tmp_path.joinpath(".flake8").write_text(
474509
"""
@@ -490,6 +525,7 @@ def test_config_ignore_error_code(tmp_path: Path) -> None:
490525

491526

492527
# flake8>=6 enforces three-letter error codes in config
528+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
493529
def test_config_extend_ignore_error_code(tmp_path: Path) -> None:
494530
assert tmp_path.joinpath(".flake8").write_text(
495531
"""
@@ -511,6 +547,7 @@ def test_config_extend_ignore_error_code(tmp_path: Path) -> None:
511547
assert res.returncode == 1
512548

513549

550+
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
514551
# but make sure we can disable selected codes
515552
def test_config_disable_error_code(tmp_path: Path) -> None:
516553
# select ASYNC200 and create file that induces ASYNC200

tests/test_decorator.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from __future__ import annotations
44

55
import ast
6+
import sys
67
from pathlib import Path
78
from typing import TYPE_CHECKING
89

9-
from flake8.main.application import Application
10-
10+
from flake8_async import main
1111
from flake8_async.base import Statement
1212
from flake8_async.visitors.helpers import fnmatch_qualified_name
1313
from flake8_async.visitors.visitor91x import Visitor91X
@@ -90,11 +90,20 @@ def test_pep614():
9090

9191

9292
file_path = str(Path(__file__).parent / "trio_options.py")
93-
common_flags = ["--select=ASYNC", file_path]
9493

9594

96-
def test_command_line_1(capfd: pytest.CaptureFixture[str]):
97-
Application().run([*common_flags, "--no-checkpoint-warning-decorators=app.route"])
95+
def _set_flags(monkeypatch: pytest.MonkeyPatch, *flags: str):
96+
monkeypatch.setattr(
97+
sys, "argv", ["./flake8-async", "--enable=ASYNC910", file_path, *flags]
98+
)
99+
100+
101+
def test_command_line_1(
102+
capfd: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
103+
):
104+
_set_flags(monkeypatch, "--no-checkpoint-warning-decorators=app.route")
105+
assert main() == 0
106+
98107
assert capfd.readouterr() == ("", "")
99108

100109

@@ -114,11 +123,17 @@ def test_command_line_1(capfd: pytest.CaptureFixture[str]):
114123
)
115124

116125

117-
def test_command_line_2(capfd: pytest.CaptureFixture[str]):
118-
Application().run([*common_flags, "--no-checkpoint-warning-decorators=app"])
126+
def test_command_line_2(
127+
capfd: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
128+
):
129+
_set_flags(monkeypatch, "--no-checkpoint-warning-decorators=app")
130+
assert main() == 1
119131
assert capfd.readouterr() == (expected_out, "")
120132

121133

122-
def test_command_line_3(capfd: pytest.CaptureFixture[str]):
123-
Application().run(common_flags)
134+
def test_command_line_3(
135+
capfd: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
136+
):
137+
_set_flags(monkeypatch)
138+
assert main() == 1
124139
assert capfd.readouterr() == (expected_out, "")

tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# The test environment and commands
22
[tox]
33
# default environments to run without `-e`
4-
envlist = py{39,310,311,312,313}-{flake8_6,flake8_7}
4+
envlist = py{39,310,311,312,313}-{flake8_6,flake8_7},noflake8
55

66
# create a default testenv, whose behaviour will depend on the name it's called with.
77
# for CI you can call with `-e flake8_6,flake8_7` and let the CI handle python version

0 commit comments

Comments
 (0)