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):