Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make flake8 an optional dependency #348

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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