Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog

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

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 <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).
Expand Down
6 changes: 3 additions & 3 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
41 changes: 37 additions & 4 deletions flake8_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion tests/eval_files/async119.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 39 additions & 2 deletions tests/test_config_and_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
...
Expand Down Expand Up @@ -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}"])
Expand All @@ -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(
"""
Expand Down Expand Up @@ -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]
):
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]
Expand All @@ -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(
"""
Expand All @@ -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(
"""
Expand All @@ -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
Expand Down
33 changes: 24 additions & 9 deletions tests/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() == ("", "")


Expand All @@ -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, "")
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading