diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 595242ed..1759910b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -44,6 +44,8 @@ jobs:
run: python -m tox -e flake8_6
- name: Run tests with flake8_7+
run: python -m tox -e flake8_7
+ - name: Run tests without flake8
+ run: python -m tox -e noflake8 -- --no-cov
slow_tests:
runs-on: ubuntu-latest
diff --git a/docs/changelog.rst b/docs/changelog.rst
index c8f50839..86ea4ed7 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,6 +4,10 @@ Changelog
`CalVer, YY.month.patch `_
+25.2.3
+=======
+- No longer require ``flake8`` for installation... so if you require support for config files you must install ``flake8-async[flake8]``
+
25.2.2
=======
- :ref:`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).
diff --git a/docs/usage.rst b/docs/usage.rst
index df6440fd..e5b56eb3 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -17,7 +17,7 @@ install and run through flake8
.. code-block:: sh
- pip install flake8 flake8-async
+ pip install flake8-async[flake8]
flake8 .
.. _install-run-pre-commit:
@@ -33,10 +33,10 @@ adding the following to your ``.pre-commit-config.yaml``:
minimum_pre_commit_version: '2.9.0'
repos:
- repo: https://github.com/python-trio/flake8-async
- rev: 25.2.2
+ rev: 25.2.3
hooks:
- id: flake8-async
- # args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC]
+ # args: ["--enable=ASYNC100,ASYNC112", "--disable=", "--autofix=ASYNC"]
This is often considerably faster for large projects, because ``pre-commit``
can avoid running ``flake8-async`` on unchanged files.
diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py
index 8dcb1be5..d5e42ff8 100644
--- a/flake8_async/__init__.py
+++ b/flake8_async/__init__.py
@@ -38,7 +38,7 @@
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
-__version__ = "25.2.2"
+__version__ = "25.2.3"
# taken from https://github.com/Zac-HD/shed
@@ -127,8 +127,11 @@ def options(self) -> Options:
assert self._options is not None
return self._options
- def __init__(self, tree: ast.AST, lines: Sequence[str]):
+ def __init__(
+ self, tree: ast.AST, lines: Sequence[str], filename: str | None = None
+ ):
super().__init__()
+ self.filename: str | None = filename
self._tree = tree
source = "".join(lines)
@@ -139,14 +142,17 @@ def from_filename(cls, filename: str | PathLike[str]) -> Plugin: # pragma: no c
# only used with --runslow
with tokenize.open(filename) as f:
source = f.read()
- return cls.from_source(source)
+ return cls.from_source(source, filename=filename)
# alternative `__init__` to avoid re-splitting and/or re-joining lines
@classmethod
- def from_source(cls, source: str) -> Plugin:
+ def from_source(
+ cls, source: str, filename: str | PathLike[str] | None = None
+ ) -> Plugin:
plugin = Plugin.__new__(cls)
super(Plugin, plugin).__init__()
plugin._tree = ast.parse(source)
+ plugin.filename = str(filename) if filename else None
plugin.module = cst_parse_module_native(source)
return plugin
@@ -231,6 +237,13 @@ def add_options(option_manager: OptionManager | ArgumentParser):
" errors."
),
)
+ add_argument(
+ "--per-file-disable",
+ type=parse_per_file_disable,
+ default={},
+ required=False,
+ help=("..."),
+ )
add_argument(
"--autofix",
type=comma_separated_list,
@@ -441,3 +454,23 @@ def parse_async200_dict(raw_value: str) -> dict[str, str]:
)
res[split_values[0]] = split_values[1]
return res
+
+
+# not run if flake8 is installed
+def parse_per_file_disable( # pragma: no cover
+ raw_value: str,
+) -> dict[str, tuple[str, ...]]:
+ res: dict[str, tuple[str, ...]] = {}
+ splitter = "->"
+ values = [s.strip() for s in raw_value.split(" \t\n") if s.strip()]
+ for value in values:
+ split_values = list(map(str.strip, value.split(splitter)))
+ if len(split_values) != 2:
+ # argparse will eat this error message and spit out its own
+ # if we raise it as ValueError
+ raise ArgumentTypeError(
+ f"Invalid number ({len(split_values)-1}) of splitter "
+ f"tokens {splitter!r} in {value!r}"
+ )
+ res[split_values[0]] = tuple(split_values[1].split(","))
+ return res
diff --git a/setup.py b/setup.py
index 11af367b..04861d07 100755
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,8 @@ def local_file(name: str) -> Path:
license_files=[], # https://github.com/pypa/twine/issues/1216
description="A highly opinionated flake8 plugin for Trio-related problems.",
zip_safe=False,
- install_requires=["flake8>=6", "libcst>=1.0.1"],
+ install_requires=["libcst>=1.0.1"],
+ extras_require={"flake8": ["flake8>=6"]},
python_requires=">=3.9",
classifiers=[
"Development Status :: 3 - Alpha",
diff --git a/tests/eval_files/async119.py b/tests/eval_files/async119.py
index 63f2ca66..0fc3af26 100644
--- a/tests/eval_files/async119.py
+++ b/tests/eval_files/async119.py
@@ -15,7 +15,7 @@ async def async_with():
yield # error: 8
-async def warn_on_yeach_yield():
+async def warn_on_each_yield():
with open(""):
yield # error: 8
yield # error: 8
diff --git a/tests/test_config_and_args.py b/tests/test_config_and_args.py
index 766d7049..a999a095 100644
--- a/tests/test_config_and_args.py
+++ b/tests/test_config_and_args.py
@@ -13,6 +13,11 @@
from .test_flake8_async import initialize_options
+try:
+ import flake8
+except ImportError:
+ flake8 = None # type: ignore[assignment]
+
EXAMPLE_PY_TEXT = """import trio
with trio.move_on_after(10):
...
@@ -140,7 +145,7 @@ def test_run_100_autofix(
def test_114_raises_on_invalid_parameter(capsys: pytest.CaptureFixture[str]):
plugin = Plugin(ast.AST(), [])
- # flake8 will reraise ArgumentError as SystemExit
+ # argparse will reraise ArgumentTypeError as SystemExit
for arg in "blah.foo", "foo*", "*":
with pytest.raises(SystemExit):
initialize_options(plugin, args=[f"--startable-in-context-manager={arg}"])
@@ -159,6 +164,7 @@ def test_200_options(capsys: pytest.CaptureFixture[str]):
assert all(word in err for word in (str(i), arg, "->"))
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_anyio_from_config(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
assert tmp_path.joinpath(".flake8").write_text(
"""
@@ -228,6 +234,7 @@ async def foo():
)
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_200_from_config_flake8_internals(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
):
@@ -254,6 +261,7 @@ def test_200_from_config_flake8_internals(
assert err_msg == out
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_200_from_config_subprocess(tmp_path: Path):
err_msg = _test_async200_from_config_common(tmp_path)
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):
assert res.stdout == err_msg.encode("ascii")
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_async200_from_config_subprocess(tmp_path: Path):
err_msg = _test_async200_from_config_common(tmp_path, code="trio200")
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):
assert res.stdout == err_msg.encode("ascii")
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_async200_from_config_subprocess_cli_ignore(tmp_path: Path):
_ = _test_async200_from_config_common(tmp_path)
res = subprocess.run(
@@ -288,6 +298,20 @@ def test_async200_from_config_subprocess_cli_ignore(tmp_path: Path):
def test_900_default_off(capsys: pytest.CaptureFixture[str]):
+ res = subprocess.run(
+ ["flake8-async", "tests/eval_files/async900.py"],
+ capture_output=True,
+ check=False,
+ encoding="utf8",
+ )
+ assert res.returncode == 1
+ assert not res.stderr
+ assert "ASYNC124" in res.stdout
+ assert "ASYNC900" not in res.stdout
+
+
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
+def test_900_default_off_flake8(capsys: pytest.CaptureFixture[str]):
from flake8.main.cli import main
returnvalue = main(
@@ -303,11 +327,18 @@ def test_900_default_off(capsys: pytest.CaptureFixture[str]):
def test_910_can_be_selected(tmp_path: Path):
+ """Check if flake8 allows us to --select our 5-letter code.
+
+ But we can run with --enable regardless.
+ """
myfile = tmp_path.joinpath("foo.py")
myfile.write_text("""async def foo():\n print()""")
+ binary = "flake8-async" if flake8 is None else "flake8"
+ select_enable = "enable" if flake8 is None else "select"
+
res = subprocess.run(
- ["flake8", "--select=ASYNC910", "foo.py"],
+ [binary, f"--{select_enable}=ASYNC910", "foo.py"],
cwd=tmp_path,
capture_output=True,
check=False,
@@ -384,6 +415,7 @@ def _helper(*args: str, error: bool = False, autofix: bool = False) -> None:
)
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_flake8_plugin_with_autofix_fails(tmp_path: Path):
write_examplepy(tmp_path)
res = subprocess.run(
@@ -453,7 +485,9 @@ def test_disable_noqa_ast(
)
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_config_select_error_code(tmp_path: Path) -> None:
+ # this ... seems to work? I'm confused
assert tmp_path.joinpath(".flake8").write_text(
"""
[flake8]
@@ -469,6 +503,7 @@ def test_config_select_error_code(tmp_path: Path) -> None:
# flake8>=6 enforces three-letter error codes in config
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_config_ignore_error_code(tmp_path: Path) -> None:
assert tmp_path.joinpath(".flake8").write_text(
"""
@@ -490,6 +525,7 @@ def test_config_ignore_error_code(tmp_path: Path) -> None:
# flake8>=6 enforces three-letter error codes in config
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
def test_config_extend_ignore_error_code(tmp_path: Path) -> None:
assert tmp_path.joinpath(".flake8").write_text(
"""
@@ -511,6 +547,7 @@ def test_config_extend_ignore_error_code(tmp_path: Path) -> None:
assert res.returncode == 1
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
# but make sure we can disable selected codes
def test_config_disable_error_code(tmp_path: Path) -> None:
# select ASYNC200 and create file that induces ASYNC200
diff --git a/tests/test_decorator.py b/tests/test_decorator.py
index b3828cea..1da27970 100644
--- a/tests/test_decorator.py
+++ b/tests/test_decorator.py
@@ -3,11 +3,11 @@
from __future__ import annotations
import ast
+import sys
from pathlib import Path
from typing import TYPE_CHECKING
-from flake8.main.application import Application
-
+from flake8_async import main
from flake8_async.base import Statement
from flake8_async.visitors.helpers import fnmatch_qualified_name
from flake8_async.visitors.visitor91x import Visitor91X
@@ -90,11 +90,20 @@ def test_pep614():
file_path = str(Path(__file__).parent / "trio_options.py")
-common_flags = ["--select=ASYNC", file_path]
-def test_command_line_1(capfd: pytest.CaptureFixture[str]):
- Application().run([*common_flags, "--no-checkpoint-warning-decorators=app.route"])
+def _set_flags(monkeypatch: pytest.MonkeyPatch, *flags: str):
+ monkeypatch.setattr(
+ sys, "argv", ["./flake8-async", "--enable=ASYNC910", file_path, *flags]
+ )
+
+
+def test_command_line_1(
+ capfd: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
+):
+ _set_flags(monkeypatch, "--no-checkpoint-warning-decorators=app.route")
+ assert main() == 0
+
assert capfd.readouterr() == ("", "")
@@ -114,11 +123,17 @@ def test_command_line_1(capfd: pytest.CaptureFixture[str]):
)
-def test_command_line_2(capfd: pytest.CaptureFixture[str]):
- Application().run([*common_flags, "--no-checkpoint-warning-decorators=app"])
+def test_command_line_2(
+ capfd: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
+):
+ _set_flags(monkeypatch, "--no-checkpoint-warning-decorators=app")
+ assert main() == 1
assert capfd.readouterr() == (expected_out, "")
-def test_command_line_3(capfd: pytest.CaptureFixture[str]):
- Application().run(common_flags)
+def test_command_line_3(
+ capfd: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
+):
+ _set_flags(monkeypatch)
+ assert main() == 1
assert capfd.readouterr() == (expected_out, "")
diff --git a/tox.ini b/tox.ini
index 2bcca72d..ab012d9c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
# The test environment and commands
[tox]
# default environments to run without `-e`
-envlist = py{39,310,311,312,313}-{flake8_6,flake8_7}
+envlist = py{39,310,311,312,313}-{flake8_6,flake8_7},noflake8
# create a default testenv, whose behaviour will depend on the name it's called with.
# for CI you can call with `-e flake8_6,flake8_7` and let the CI handle python version