Skip to content

Commit 115eb5e

Browse files
authored
Merge pull request #93 from jakkdl/config_parsing_type
fix #84
2 parents 375f70d + 5bebe41 commit 115eb5e

File tree

4 files changed

+94
-67
lines changed

4 files changed

+94
-67
lines changed

flake8_trio.py

+31-56
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111

1212
from __future__ import annotations
1313

14-
import argparse
1514
import ast
1615
import keyword
1716
import tokenize
18-
from argparse import Namespace
19-
from collections.abc import Iterable, Sequence
17+
from argparse import ArgumentTypeError, Namespace
18+
from collections.abc import Iterable
2019
from fnmatch import fnmatch
2120
from typing import Any, NamedTuple, Union, cast
2221

@@ -1427,46 +1426,33 @@ def visit_Call(self, node: ast.Call):
14271426
self.error(node, key, blocking_calls[key])
14281427

14291428

1430-
class ListOfIdentifiers(argparse.Action):
1431-
def __call__(
1432-
self,
1433-
parser: argparse.ArgumentParser,
1434-
namespace: argparse.Namespace,
1435-
values: Sequence[str] | None,
1436-
option_string: str | None = None,
1437-
):
1438-
assert values is not None
1439-
assert option_string is not None
1440-
for value in values:
1441-
if keyword.iskeyword(value) or not value.isidentifier():
1442-
raise argparse.ArgumentError(
1443-
self, f"{value!r} is not a valid method identifier"
1444-
)
1445-
setattr(namespace, self.dest, values)
1446-
1447-
1448-
class ParseDict(argparse.Action):
1449-
def __call__(
1450-
self,
1451-
parser: argparse.ArgumentParser,
1452-
namespace: argparse.Namespace,
1453-
values: Sequence[str] | None,
1454-
option_string: str | None = None,
1455-
):
1456-
res: dict[str, str] = {}
1457-
splitter = "->" # avoid ":" because it's part of .ini file syntax
1458-
assert values is not None
1459-
for value in values:
1460-
split_values = list(map(str.strip, value.split(splitter)))
1461-
if len(split_values) != 2:
1462-
raise argparse.ArgumentError(
1463-
self,
1464-
f"Invalid number ({len(split_values)-1}) of splitter "
1465-
f"tokens {splitter!r} in {value!r}",
1466-
)
1467-
res[split_values[0]] = split_values[1]
1468-
1469-
setattr(namespace, self.dest, res)
1429+
# flake8 ignores type parameters if using comma_separated_list
1430+
# so we need to reimplement that ourselves if we want to use "type"
1431+
# to check values
1432+
def parse_trio114_identifiers(raw_value: str) -> list[str]:
1433+
values = [s.strip() for s in raw_value.split(",") if s.strip()]
1434+
for value in values:
1435+
if keyword.iskeyword(value) or not value.isidentifier():
1436+
raise ArgumentTypeError(f"{value!r} is not a valid method identifier")
1437+
return values
1438+
1439+
1440+
def parse_trio200_dict(raw_value: str) -> dict[str, str]:
1441+
res: dict[str, str] = {}
1442+
splitter = "->" # avoid ":" because it's part of .ini file syntax
1443+
values = [s.strip() for s in raw_value.split(",") if s.strip()]
1444+
1445+
for value in values:
1446+
split_values = list(map(str.strip, value.split(splitter)))
1447+
if len(split_values) != 2:
1448+
# argparse will eat this error message and spit out it's own
1449+
# if we raise it as ValueError
1450+
raise ArgumentTypeError(
1451+
f"Invalid number ({len(split_values)-1}) of splitter "
1452+
f"tokens {splitter!r} in {value!r}",
1453+
)
1454+
res[split_values[0]] = split_values[1]
1455+
return res
14701456

14711457

14721458
class Plugin:
@@ -1484,15 +1470,6 @@ def from_filename(cls, filename: str) -> Plugin:
14841470
return cls(ast.parse(source))
14851471

14861472
def run(self) -> Iterable[Error]:
1487-
# temporary workaround, since the Action does not seem to be called properly
1488-
# by flake8 when parsing from config
1489-
if isinstance(self.options.trio200_blocking_calls, list):
1490-
ParseDict([""], dest="trio200_blocking_calls")(
1491-
None, # type: ignore
1492-
self.options,
1493-
self.options.trio200_blocking_calls, # type: ignore
1494-
None,
1495-
)
14961473
yield from Flake8TrioRunner.run(self._tree, self.options)
14971474

14981475
@staticmethod
@@ -1513,11 +1490,10 @@ def add_options(option_manager: OptionManager):
15131490
)
15141491
option_manager.add_option(
15151492
"--startable-in-context-manager",
1493+
type=parse_trio114_identifiers,
15161494
default="",
15171495
parse_from_config=True,
15181496
required=False,
1519-
comma_separated_list=True,
1520-
action=ListOfIdentifiers,
15211497
help=(
15221498
"Comma-separated list of method calls to additionally enable TRIO113 "
15231499
"warnings for. Will also check for the pattern inside function calls. "
@@ -1529,11 +1505,10 @@ def add_options(option_manager: OptionManager):
15291505
)
15301506
option_manager.add_option(
15311507
"--trio200-blocking-calls",
1508+
type=parse_trio200_dict,
15321509
default={},
15331510
parse_from_config=True,
15341511
required=False,
1535-
comma_separated_list=True,
1536-
action=ParseDict,
15371512
help=(
15381513
"Comma-separated list of key:value pairs, where key is a [dotted] "
15391514
"function that if found inside an async function will raise TRIO200, "

tests/test_flake8_trio.py

+42-10
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def test_eval(test: str, path: str):
8080
lines = file.readlines()
8181

8282
for lineno, line in enumerate(lines, start=1):
83+
# interpret '\n' in comments as actual newlines
84+
line = line.replace("\\n", "\n")
85+
8386
line = line.strip()
8487

8588
# add other error codes to check if #INCLUDE is specified
@@ -90,9 +93,7 @@ def test_eval(test: str, path: str):
9093

9194
# add command-line args if specified with #ARGS
9295
elif reg_match := re.search(r"(?<=ARGS).*", line):
93-
for arg in reg_match.group().split(" "):
94-
if arg.strip():
95-
parsed_args.append(arg.strip())
96+
parsed_args.append(reg_match.group().strip())
9697

9798
# skip commented out lines
9899
if not line or line[0] == "#":
@@ -444,16 +445,17 @@ def test_200_options(capsys: pytest.CaptureFixture[str]):
444445
om.parse_args(args=[f"--trio200-blocking-calls={arg}"])
445446
)
446447
out, err = capsys.readouterr()
447-
assert not out
448+
assert not out, out
448449
assert all(word in err for word in (str(i), arg, "->"))
449450

450451

451-
def test_from_config_file(tmp_path: Path):
452+
def _test_trio200_from_config_common(tmp_path: Path) -> str:
452453
tmp_path.joinpath(".flake8").write_text(
453454
"""
454455
[flake8]
455456
trio200-blocking-calls =
456-
sync_fns.*->the_async_equivalent,
457+
other -> async,
458+
sync_fns.* -> the_async_equivalent,
457459
select = TRIO200
458460
"""
459461
)
@@ -465,12 +467,42 @@ async def foo():
465467
sync_fns.takes_a_long_time()
466468
"""
467469
)
470+
return (
471+
"./example.py:5:5: TRIO200 User-configured blocking sync call sync_fns.* "
472+
"in async function, consider replacing with the_async_equivalent.\n"
473+
)
474+
475+
476+
def test_200_from_config_flake8_internals(
477+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
478+
):
479+
# abuse flake8 internals to avoid having to use subprocess
480+
# which breaks breakpoints and hinders debugging
481+
# TODO: fixture (?) to change working directory
482+
483+
err_msg = _test_trio200_from_config_common(tmp_path)
484+
# replace ./ with tmp_path/
485+
err_msg = str(tmp_path) + err_msg[1:]
486+
487+
from flake8.main.cli import main
488+
489+
main(
490+
argv=[
491+
str(tmp_path / "example.py"),
492+
"--append-config",
493+
str(tmp_path / ".flake8"),
494+
]
495+
)
496+
out, err = capsys.readouterr()
497+
assert not err
498+
assert err_msg == out
499+
500+
501+
def test_200_from_config_subprocess(tmp_path: Path):
502+
err_msg = _test_trio200_from_config_common(tmp_path)
468503
res = subprocess.run(["flake8"], cwd=tmp_path, capture_output=True)
469504
assert not res.stderr
470-
assert res.stdout == (
471-
b"./example.py:5:5: TRIO200 User-configured blocking sync call sync_fns.* "
472-
b"in async function, consider replacing with the_async_equivalent.\n"
473-
)
505+
assert res.stdout == err_msg.encode("ascii")
474506

475507

476508
@pytest.mark.fuzz

tests/trio200.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# specify command-line arguments to be used when testing this file.
2-
# ARGS --trio200-blocking-calls=bar->BAR,bee->SHOULD_NOT_BE_PRINTED,bonnet->SHOULD_NOT_BE_PRINTED,bee.bonnet->BEEBONNET,*.postwild->POSTWILD,prewild.*->PREWILD,*.*.*->TRIPLEDOT
2+
# Test spaces in options, and trailing comma
3+
# Cannot test newlines, since argparse splits on those if passed on the CLI
4+
# ARGS --trio200-blocking-calls=bar -> BAR, bee-> SHOULD_NOT_BE_PRINTED,bonnet ->SHOULD_NOT_BE_PRINTED,bee.bonnet->BEEBONNET,*.postwild->POSTWILD,prewild.*->PREWILD,*.*.*->TRIPLEDOT,
35

46
# don't error in sync function
57
def foo():

typings/flake8/main/cli.pyi

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
This type stub file was generated by pyright.
3+
"""
4+
5+
from collections.abc import Sequence
6+
7+
"""Command-line implementation of flake8."""
8+
9+
def main(argv: Sequence[str] | None = ...) -> int:
10+
"""Execute the main bit of the application.
11+
12+
This handles the creation of an instance of :class:`Application`, runs it,
13+
and then exits the application.
14+
15+
:param argv:
16+
The arguments to be passed to the application for parsing.
17+
"""
18+
...

0 commit comments

Comments
 (0)