Skip to content

Commit 928eeb8

Browse files
committed
Run compile() against tests/eval_files to catch syntax mistakes
ast.parse() accepts some code the bytecode compiler rejects (e.g. `x = lambda: await foo()`, `return` outside a function), so a bad test fixture could silently slip past the plugin's ast-based checks. Add a new parametrized test that runs compile() on every tests/eval_files/*.py. A handful of existing fixtures intentionally contain such constructs to exercise edge cases; mark those with a new `# NOCOMPILE` magic marker so the new test skips them while still leaving the rest of the suite guarding against accidents. Fixes #268
1 parent 35dc640 commit 928eeb8

6 files changed

Lines changed: 31 additions & 0 deletions

File tree

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Unreleased
1010
- :ref:`ASYNC910 <async910>` and :ref:`ASYNC911 <async911>` now accept ``__aenter__`` / ``__aexit__`` methods when the partner method provides the checkpoint, or when only one of the two is defined on a class that inherits from another class (charitably assuming the partner is inherited and contains a checkpoint). `(issue #441) <https://github.com/python-trio/flake8-async/issues/441>`_
1111
- :ref:`ASYNC300 <async300>` no longer triggers when the result of ``asyncio.create_task()`` is returned from a function. `(issue #398) <https://github.com/python-trio/flake8-async/issues/398>`_
1212
- Document using ruff's ``lint.external`` setting to preserve ``# noqa: ASYNC...`` comments when running ruff alongside flake8-async. `(issue #310) <https://github.com/python-trio/flake8-async/issues/310>`_
13+
- Tests: each ``tests/eval_files/*.py`` fixture is now checked with ``compile()`` so that accidental semantic errors (e.g. ``lambda: await foo()``) in fixtures are caught. Files that intentionally contain such constructs are marked with ``# NOCOMPILE``. `(issue #268) <https://github.com/python-trio/flake8-async/issues/268>`_
1314

1415
25.7.1
1516
======

tests/autofix_files/async124.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
# ARG --enable=ASYNC124,ASYNC910,ASYNC911
55
# ARG --no-checkpoint-warning-decorator=custom_disabled_decorator
6+
# NOCOMPILE: foo_nested_sync contains `await` in a sync nested function, which is
7+
# a SyntaxError the bytecode compiler catches but ast.parse accepts. It's only here
8+
# to make sure the plugin doesn't crash on such code.
69

710
# 910/911 will also autofix async124, in the sense of adding a checkpoint. This is perhaps
811
# not what the user wants though, so this would be a case in favor of making 910/911 not

tests/eval_files/async103_104_py311.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
ASYNC104: cancelled-not-raised
55
"""
66

7+
# NOCOMPILE: `return` inside `except*` is a SyntaxError in 3.11+ but we still want
8+
# to exercise the plugin against it via ast.parse.
9+
710
# ARG --enable=ASYNC103,ASYNC104
811

912
try:

tests/eval_files/async104.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# ARG --enable=ASYNC103,ASYNC104
2+
# NOCOMPILE: contains `return` outside a function so the bytecode compiler rejects
3+
# the file, but ast.parse accepts it and the plugin flags it as ASYNC104.
24
try:
35
...
46
# raise different exception

tests/eval_files/async124.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
# ARG --enable=ASYNC124,ASYNC910,ASYNC911
55
# ARG --no-checkpoint-warning-decorator=custom_disabled_decorator
6+
# NOCOMPILE: foo_nested_sync contains `await` in a sync nested function, which is
7+
# a SyntaxError the bytecode compiler catches but ast.parse accepts. It's only here
8+
# to make sure the plugin doesn't crash on such code.
69

710
# 910/911 will also autofix async124, in the sense of adding a checkpoint. This is perhaps
811
# not what the user wants though, so this would be a case in favor of making 910/911 not

tests/test_flake8_async.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,12 @@ class MagicMarkers:
244244
# eval file is written using this library, so no substitution is required
245245
BASE_LIBRARY: str = "trio"
246246

247+
# File intentionally contains constructs that are syntactically valid for
248+
# `ast.parse` but rejected by the bytecode compiler (e.g. `return` outside a
249+
# function, `await` in a sync nested function). Used to skip the compile()
250+
# sanity check in test_eval_files_compile.
251+
NOCOMPILE: bool = False
252+
247253
def library_no_error(self, library: str) -> bool:
248254
return {
249255
"anyio": self.ANYIO_NO_ERROR,
@@ -545,6 +551,19 @@ def visit_AsyncFor(self, node: ast.AsyncFor):
545551
return self.replace_async(node, ast.For, node.target, node.iter)
546552

547553

554+
# ast.parse() is lenient and accepts some code that the bytecode compiler will
555+
# reject (e.g. `x = lambda: await foo()` or `return` outside a function). Running
556+
# compile() on each eval file catches accidental syntax errors in test fixtures
557+
# that would otherwise silently slip past the plugin's ast-based checks.
558+
@pytest.mark.parametrize(("test", "path"), test_files, ids=[f[0] for f in test_files])
559+
def test_eval_files_compile(test: str, path: Path):
560+
check_version(test)
561+
content = path.read_text()
562+
if find_magic_markers(content).NOCOMPILE:
563+
pytest.skip("file intentionally does not compile (has # NOCOMPILE marker)")
564+
compile(content, str(path), "exec")
565+
566+
548567
@pytest.mark.parametrize(("test", "path"), test_files, ids=[f[0] for f in test_files])
549568
def test_noerror_on_sync_code(test: str, path: Path):
550569
if any(e in test for e in error_codes_ignored_when_checking_transformed_sync_code):

0 commit comments

Comments
 (0)