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