From 7a0b1a1108732d71de6a21c299d72a82bc06b41e Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Thu, 23 Jan 2025 17:18:40 +0100
Subject: [PATCH 1/4] stuff
---
docs/usage.rst | 2 +-
flake8_async/__init__.py | 35 ++++++++++++++++++++++++++++++++---
setup.py | 3 ++-
tests/eval_files/async119.py | 2 +-
tests/test_config_and_args.py | 24 ++++++++++++++++--------
tests/test_decorator.py | 14 +++++++++-----
6 files changed, 61 insertions(+), 19 deletions(-)
diff --git a/docs/usage.rst b/docs/usage.rst
index 3a33e7a3..4898618f 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -36,7 +36,7 @@ adding the following to your ``.pre-commit-config.yaml``:
rev: 23.2.5
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 83c5d570..4551b3e1 100644
--- a/flake8_async/__init__.py
+++ b/flake8_async/__init__.py
@@ -127,8 +127,10 @@ 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):
super().__init__()
+ assert isinstance(filename, str)
+ self.filename: str | None = filename
self._tree = tree
source = "".join(lines)
@@ -139,14 +141,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 +236,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 +453,20 @@ def parse_async200_dict(raw_value: str) -> dict[str, str]:
)
res[split_values[0]] = split_values[1]
return res
+
+
+def parse_per_file_disable(raw_value: str) -> dict[str, tuple[str, ...]]:
+ res = {}
+ 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 it's 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 0f19b457..6770c992 100755
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,8 @@ def local_file(name: str) -> Path:
license="MIT",
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"],
+ # install_requires=["flake8>=6", "libcst>=1.0.1"],
python_requires=">=3.9",
classifiers=[
"Development Status :: 3 - Alpha",
diff --git a/tests/eval_files/async119.py b/tests/eval_files/async119.py
index 242db2c5..223d0e80 100644
--- a/tests/eval_files/async119.py
+++ b/tests/eval_files/async119.py
@@ -13,7 +13,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 d96b83f9..c41dd19b 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
+
EXAMPLE_PY_TEXT = """import trio
with trio.move_on_after(10):
...
@@ -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(
"""
@@ -187,9 +193,8 @@ def test_anyio_from_config(tmp_path: Path, capsys: pytest.CaptureFixture[str]):
# construct the full error message
expected = f"{err_file}:{lineno}:5: ASYNC220 {err_msg}\n"
- from flake8.main.cli import main
- returnvalue = main(
+ returnvalue = flake8.main.cli.main(
argv=[
str(err_file),
"--config",
@@ -228,6 +233,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]
):
@@ -239,9 +245,7 @@ def test_200_from_config_flake8_internals(
# replace ./ with tmp_path/
err_msg = str(tmp_path) + EXAMPLE_PY_TEXT[1:]
- from flake8.main.cli import main
-
- returnvalue = main(
+ returnvalue = flake8.main.cli.main(
argv=[
str(tmp_path / "example.py"),
"--append-config",
@@ -254,6 +258,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 +267,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_trio200_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,10 +279,9 @@ def test_trio200_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_900_default_off(capsys: pytest.CaptureFixture[str]):
- from flake8.main.cli import main
-
- returnvalue = main(
+ returnvalue = flake8.main.cli.main(
argv=[
"tests/trio900.py",
]
@@ -349,6 +354,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(
@@ -418,6 +424,7 @@ def test_disable_noqa_ast(
)
+@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
@pytest.mark.xfail(reason="flake8>=6 enforces three-letter error codes in config")
def test_config_ignore_error_code(tmp_path: Path) -> None:
assert tmp_path.joinpath(".flake8").write_text(
@@ -433,6 +440,7 @@ def test_config_ignore_error_code(tmp_path: Path) -> None:
assert res.returncode == 0
+@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..fae82941 100644
--- a/tests/test_decorator.py
+++ b/tests/test_decorator.py
@@ -4,17 +4,18 @@
import ast
from pathlib import Path
-from typing import TYPE_CHECKING
-from flake8.main.application import Application
+try:
+ from flake8.main.application import Application
+except ImportError:
+ Application = None
+
+import pytest
from flake8_async.base import Statement
from flake8_async.visitors.helpers import fnmatch_qualified_name
from flake8_async.visitors.visitor91x import Visitor91X
-if TYPE_CHECKING:
- import pytest
-
def dec_list(*decorators: str) -> ast.Module:
source = ""
@@ -93,6 +94,7 @@ def test_pep614():
common_flags = ["--select=ASYNC", file_path]
+@pytest.mark.skipif(Application is None, reason="flake8 not installed")
def test_command_line_1(capfd: pytest.CaptureFixture[str]):
Application().run([*common_flags, "--no-checkpoint-warning-decorators=app.route"])
assert capfd.readouterr() == ("", "")
@@ -114,11 +116,13 @@ def test_command_line_1(capfd: pytest.CaptureFixture[str]):
)
+@pytest.mark.skipif(Application is None, reason="flake8 not installed")
def test_command_line_2(capfd: pytest.CaptureFixture[str]):
Application().run([*common_flags, "--no-checkpoint-warning-decorators=app"])
assert capfd.readouterr() == (expected_out, "")
+@pytest.mark.skipif(Application is None, reason="flake8 not installed")
def test_command_line_3(capfd: pytest.CaptureFixture[str]):
Application().run(common_flags)
assert capfd.readouterr() == (expected_out, "")
From c50681f253efc8a4494b8ce4bfb7459892121328 Mon Sep 17 00:00:00 2001
From: jakkdl <11260241+jakkdl@users.noreply.github.com>
Date: Wed, 5 Feb 2025 13:35:35 +0100
Subject: [PATCH 2/4] add to ci, changelog, tox, coverage
---
.github/workflows/ci.yml | 2 ++
docs/changelog.rst | 4 ++++
docs/usage.rst | 4 ++--
flake8_async/__init__.py | 7 +++++--
setup.py | 2 +-
tests/test_config_and_args.py | 29 ++++++++++++++++++++++++-----
tox.ini | 2 +-
7 files changed, 39 insertions(+), 11 deletions(-)
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 d3b47c4d..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,7 +33,7 @@ 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=ASYNC100,ASYNC112", "--disable=", "--autofix=ASYNC"]
diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py
index 17c57143..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
@@ -456,7 +456,10 @@ def parse_async200_dict(raw_value: str) -> dict[str, str]:
return res
-def parse_per_file_disable(raw_value: str) -> dict[str, tuple[str, ...]]:
+# 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()]
diff --git a/setup.py b/setup.py
index e8a2c4cf..04861d07 100755
--- a/setup.py
+++ b/setup.py
@@ -35,7 +35,7 @@ def local_file(name: str) -> Path:
description="A highly opinionated flake8 plugin for Trio-related problems.",
zip_safe=False,
install_requires=["libcst>=1.0.1"],
- extras_requires={"flake8": ["flake8>=6"]},
+ extras_require={"flake8": ["flake8>=6"]},
python_requires=">=3.9",
classifiers=[
"Development Status :: 3 - Alpha",
diff --git a/tests/test_config_and_args.py b/tests/test_config_and_args.py
index ecf2313d..04be50ba 100644
--- a/tests/test_config_and_args.py
+++ b/tests/test_config_and_args.py
@@ -145,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}"])
@@ -297,8 +297,21 @@ def test_async200_from_config_subprocess_cli_ignore(tmp_path: Path):
assert res.returncode == 0
-@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
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(
@@ -313,13 +326,19 @@ def test_900_default_off(capsys: pytest.CaptureFixture[str]):
assert "ASYNC900" not in out
-@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
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,
@@ -467,8 +486,8 @@ def test_disable_noqa_ast(
@pytest.mark.skipif(flake8 is None, reason="flake8 is not installed")
-@pytest.mark.xfail(reason="flake8>=6 enforces three-letter error codes in config")
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]
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
From 8ea0acb1de95cca82b423982e2e68988e2669ee4 Mon Sep 17 00:00:00 2001
From: jakkdl <11260241+jakkdl@users.noreply.github.com>
Date: Wed, 5 Feb 2025 16:15:30 +0100
Subject: [PATCH 3/4] change test_decorator to no longer use flake8
---
tests/test_decorator.py | 45 +++++++++++++++++++++++++----------------
1 file changed, 28 insertions(+), 17 deletions(-)
diff --git a/tests/test_decorator.py b/tests/test_decorator.py
index fae82941..1da27970 100644
--- a/tests/test_decorator.py
+++ b/tests/test_decorator.py
@@ -3,19 +3,18 @@
from __future__ import annotations
import ast
+import sys
from pathlib import Path
+from typing import TYPE_CHECKING
-try:
- from flake8.main.application import Application
-except ImportError:
- Application = None
-
-import pytest
-
+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
+if TYPE_CHECKING:
+ import pytest
+
def dec_list(*decorators: str) -> ast.Module:
source = ""
@@ -91,12 +90,20 @@ def test_pep614():
file_path = str(Path(__file__).parent / "trio_options.py")
-common_flags = ["--select=ASYNC", file_path]
-@pytest.mark.skipif(Application is None, reason="flake8 not installed")
-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() == ("", "")
@@ -116,13 +123,17 @@ def test_command_line_1(capfd: pytest.CaptureFixture[str]):
)
-@pytest.mark.skipif(Application is None, reason="flake8 not installed")
-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, "")
-@pytest.mark.skipif(Application is None, reason="flake8 not installed")
-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, "")
From 7afdf6e2e61b3124ccfa4603221d4f4be0fb5265 Mon Sep 17 00:00:00 2001
From: jakkdl <11260241+jakkdl@users.noreply.github.com>
Date: Fri, 7 Feb 2025 11:43:34 +0100
Subject: [PATCH 4/4] fix type error
---
tests/test_config_and_args.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_config_and_args.py b/tests/test_config_and_args.py
index 04be50ba..a999a095 100644
--- a/tests/test_config_and_args.py
+++ b/tests/test_config_and_args.py
@@ -16,7 +16,7 @@
try:
import flake8
except ImportError:
- flake8 = None
+ flake8 = None # type: ignore[assignment]
EXAMPLE_PY_TEXT = """import trio
with trio.move_on_after(10):