From bffc30cb5694b2eb4188a858d2d1d5935b58d7b0 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 29 May 2024 15:20:15 +0200 Subject: [PATCH 1/5] doc updates. don't trigger on open_nursery in 102 (it didn't work anyway). async112 error message now specifies if its nursery or taskgroup. --- docs/rules.rst | 2 ++ docs/usage.rst | 5 +++-- flake8_async/visitors/visitor102.py | 4 +--- flake8_async/visitors/visitors.py | 28 +++++++++++++++------------- tests/eval_files/async102.py | 4 +++- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/docs/rules.rst b/docs/rules.rst index 14e0610..0eb8aa9 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -55,6 +55,8 @@ ASYNC112 : useless-nursery _`ASYNC113` : start-soon-in-aenter Using :meth:`~trio.Nursery.start_soon`/:meth:`~anyio.abc.TaskGroup.start_soon` in ``__aenter__`` doesn't wait for the task to begin. + This will only warn about built-in functions and those listed in _`ASYNC114`. + If you're starting a function that does not define `task_status`, then neither will trigger. Consider replacing with :meth:`~trio.Nursery.start`/:meth:`~anyio.abc.TaskGroup.start`. _`ASYNC114` : startable-not-in-config diff --git a/docs/usage.rst b/docs/usage.rst index 67a695b..be091a8 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -265,8 +265,9 @@ Example ``startable-in-context-manager`` -------------------------------- -Comma-separated list of methods which should be used with :meth:`trio.Nursery.start`/:meth:`anyio.abc.TaskGroup.start` when opening a context manager, -in addition to the default :func:`trio.run_process`, :func:`trio.serve_tcp`, :func:`trio.serve_ssl_over_tcp`, and :func:`trio.serve_listeners`. +Comma-separated list of methods which should be used with :meth:`trio.Nursery.start`/:meth:`anyio.abc.TaskGroup.start` when opening a context manager. +This includes startable functions in the trio and anyio standard library by default, namely :func:`trio.run_process`, :func:`trio.serve_tcp`, :func:`trio.serve_ssl_over_tcp`, :func:`trio.serve_listeners`, :func:`trio.serve`, :func:`anyio.run_process`, :func:`anyio.serve_tcp`, :func:`anyio.serve_ssl_over_tcp`, :func:`anyio.serve_listeners`, and :func:`anyio.serve`. + Names must be valid identifiers as per :meth:`str.isidentifier`. Used by :ref:`ASYNC113 `, and :ref:`ASYNC114 ` will warn when encountering methods not in the list. diff --git a/flake8_async/visitors/visitor102.py b/flake8_async/visitors/visitor102.py index 52177dd..e8abbe4 100644 --- a/flake8_async/visitors/visitor102.py +++ b/flake8_async/visitors/visitor102.py @@ -87,9 +87,7 @@ def visit_With(self, node: ast.With | ast.AsyncWith): # Check for a `with trio.` for item in node.items: - call = get_matching_call( - item.context_expr, "open_nursery", *cancel_scope_names - ) + call = get_matching_call(item.context_expr, *cancel_scope_names) if call is None: continue diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 200bb07..0091c2b 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -95,7 +95,7 @@ def visit_While(self, node: ast.While): class Visitor112(Flake8AsyncVisitor): error_codes: Mapping[str, str] = { "ASYNC112": ( - "Redundant nursery {}, consider replacing with directly awaiting " + "Redundant {1} {0}, consider replacing with directly awaiting " "the function call." ), } @@ -113,19 +113,21 @@ def visit_With(self, node: ast.With | ast.AsyncWith): continue var_name = item.optional_vars.id - # check for trio.open_nursery and anyio.create_task_group - nursery = get_matching_call( - item.context_expr, "open_nursery", base="trio" - ) or get_matching_call(item.context_expr, "create_task_group", base="anyio") start_methods: tuple[str, ...] = ("start", "start_soon") - if nursery is None: - # check for asyncio.TaskGroup - nursery = get_matching_call( - item.context_expr, "TaskGroup", base="asyncio" - ) - if nursery is None: - continue + # check for trio.open_nursery and anyio.create_task_group + if get_matching_call(item.context_expr, "open_nursery", base="trio"): + nursery_type = "nursery" + + elif get_matching_call( + item.context_expr, "create_task_group", base="anyio" + ): + nursery_type = "taskgroup" + # check for asyncio.TaskGroup + elif get_matching_call(item.context_expr, "TaskGroup", base="asyncio"): + nursery_type = "taskgroup" start_methods = ("create_task",) + else: + continue body_call = node.body[0].value if isinstance(body_call, ast.Await): @@ -142,7 +144,7 @@ def visit_With(self, node: ast.With | ast.AsyncWith): for n in self.walk(*body_call.args, *body_call.keywords) ) ): - self.error(item.context_expr, var_name) + self.error(item.context_expr, var_name, nursery_type) visit_AsyncWith = visit_With diff --git a/tests/eval_files/async102.py b/tests/eval_files/async102.py index b6b7278..1b932fd 100644 --- a/tests/eval_files/async102.py +++ b/tests/eval_files/async102.py @@ -111,7 +111,9 @@ async def foo(): myvar = True with trio.open_nursery(10) as s: s.shield = myvar - await foo() # safe in theory, error: 12, Statement("try/finally", lineno-6) + # this is not safe in theory - because `trio.open_nursery` is an async cm, + # so it's not possible to open a nursery at all. + await foo() # error: 12, Statement("try/finally", lineno-8) try: pass finally: From f383fe696129526ff339060460535c826c51d7fd Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 29 May 2024 15:32:04 +0200 Subject: [PATCH 2/5] update tests --- tests/eval_files/async112.py | 18 +++++++++--------- tests/eval_files/async112_anyio.py | 4 ++-- tests/eval_files/async112_asyncio.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/eval_files/async112.py b/tests/eval_files/async112.py index 2837f70..687443d 100644 --- a/tests/eval_files/async112.py +++ b/tests/eval_files/async112.py @@ -9,34 +9,34 @@ import trio as noterror # error -with trio.open_nursery() as n: # error: 5, "n" +with trio.open_nursery() as n: # error: 5, "n", "nursery" n.start(...) -with trio.open_nursery(...) as nurse: # error: 5, "nurse" +with trio.open_nursery(...) as nurse: # error: 5, "nurse", "nursery" nurse.start_soon(...) -with trio.open_nursery() as n: # error: 5, "n" +with trio.open_nursery() as n: # error: 5, "n", "nursery" n.start_soon(n=7) async def foo(): - async with trio.open_nursery() as n: # error: 15, "n" + async with trio.open_nursery() as n: # error: 15, "n", "nursery" n.start(...) # weird ones with multiple `withitem`s # but if split among several `with` they'd all be treated as error (or ASYNC111), so # treating as error for now. -with trio.open_nursery() as n, trio.open("") as n: # error: 5, "n" +with trio.open_nursery() as n, trio.open("") as n: # error: 5, "n", "nursery" n.start(...) -with open("") as o, trio.open_nursery() as n: # error: 20, "n" +with open("") as o, trio.open_nursery() as n: # error: 20, "n", "nursery" n.start(o) -with trio.open_nursery() as n, trio.open_nursery() as nurse: # error: 31, "nurse" +with trio.open_nursery() as n, trio.open_nursery() as nurse: # error: 31, "nurse", "nursery" nurse.start(n.start(...)) -with trio.open_nursery() as n, trio.open_nursery() as n: # error: 5, "n" # error: 31, "n" +with trio.open_nursery() as n, trio.open_nursery() as n: # error: 5, "n", "nursery" # error: 31, "n", "nursery" n.start(...) # safe if passing variable as parameter @@ -83,7 +83,7 @@ async def foo(): # body is a call to await n.start async def foo_1(): - with trio.open_nursery(...) as n: # error: 9, "n" + with trio.open_nursery(...) as n: # error: 9, "n", "nursery" await n.start(...) diff --git a/tests/eval_files/async112_anyio.py b/tests/eval_files/async112_anyio.py index ec5eb7c..52cf887 100644 --- a/tests/eval_files/async112_anyio.py +++ b/tests/eval_files/async112_anyio.py @@ -11,13 +11,13 @@ async def bar(*args): ... async def foo(): - async with anyio.create_task_group() as tg: # error: 15, "tg" + async with anyio.create_task_group() as tg: # error: 15, "tg", "taskgroup" await tg.start_soon(bar()) async with anyio.create_task_group() as tg: await tg.start(bar(tg)) - async with anyio.create_task_group() as tg: # error: 15, "tg" + async with anyio.create_task_group() as tg: # error: 15, "tg", "taskgroup" tg.start_soon(bar()) async with anyio.create_task_group() as tg: diff --git a/tests/eval_files/async112_asyncio.py b/tests/eval_files/async112_asyncio.py index 5d620a9..d29550c 100644 --- a/tests/eval_files/async112_asyncio.py +++ b/tests/eval_files/async112_asyncio.py @@ -13,7 +13,7 @@ async def bar(*args): ... async def foo(): - async with asyncio.TaskGroup() as tg: # error: 15, "tg" + async with asyncio.TaskGroup() as tg: # error: 15, "tg", "taskgroup" tg.create_task(bar()) async with asyncio.TaskGroup() as tg: From 2cf2519198db2adbd52581e481acf9a054bbe294 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 29 May 2024 17:33:39 +0200 Subject: [PATCH 3/5] help repro coverage bug --- flake8_async/visitors/visitors.py | 1 + tox.ini | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 0091c2b..fd79e20 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -127,6 +127,7 @@ def visit_With(self, node: ast.With | ast.AsyncWith): nursery_type = "taskgroup" start_methods = ("create_task",) else: + assert True continue body_call = node.body[0].value diff --git a/tox.ini b/tox.ini index 761ce97..bc9becd 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,9 @@ deps = hypothesmith >= 0.3.3 trio commands = - pytest {posargs:-n auto} + coverage erase + coverage run -m pytest {posargs:-n auto} + coverage html [testenv:docs] description = Generate docs locally @@ -34,12 +36,6 @@ commands = # Settings for other tools [pytest] -addopts = - --tb=native - --cov=flake8_async - --cov-branch - --cov-report=term-missing:skip-covered - --cov-fail-under=100 filterwarnings = error From 2cc96609aea54d1de0be64ba8fe7335b63a36cc0 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 30 May 2024 11:11:21 +0200 Subject: [PATCH 4/5] Revert "help repro coverage bug" This reverts commit 2cf2519198db2adbd52581e481acf9a054bbe294. --- flake8_async/visitors/visitors.py | 1 - tox.ini | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index fd79e20..0091c2b 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -127,7 +127,6 @@ def visit_With(self, node: ast.With | ast.AsyncWith): nursery_type = "taskgroup" start_methods = ("create_task",) else: - assert True continue body_call = node.body[0].value diff --git a/tox.ini b/tox.ini index bc9becd..761ce97 100644 --- a/tox.ini +++ b/tox.ini @@ -18,9 +18,7 @@ deps = hypothesmith >= 0.3.3 trio commands = - coverage erase - coverage run -m pytest {posargs:-n auto} - coverage html + pytest {posargs:-n auto} [testenv:docs] description = Generate docs locally @@ -36,6 +34,12 @@ commands = # Settings for other tools [pytest] +addopts = + --tb=native + --cov=flake8_async + --cov-branch + --cov-report=term-missing:skip-covered + --cov-fail-under=100 filterwarnings = error From 4539d9c6262d88366a220bcdafedb16ac2de2893 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 30 May 2024 13:18:46 +0200 Subject: [PATCH 5/5] updates after review. add test cases. type tracker can now handle attribute targets. --- docs/rules.rst | 4 +- docs/usage.rst | 6 +- flake8_async/visitors/visitor_utility.py | 9 ++- flake8_async/visitors/visitors.py | 11 +++- tests/eval_files/async102.py | 16 +++-- tests/eval_files/async113.py | 80 ++++++++++++++++++++++++ tests/eval_files/async113_anyio.py | 12 +++- tests/eval_files/async113_asyncio.py | 17 +++++ tests/test_flake8_async.py | 4 +- 9 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 tests/eval_files/async113_asyncio.py diff --git a/docs/rules.rst b/docs/rules.rst index 0eb8aa9..3aa6355 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -55,9 +55,9 @@ ASYNC112 : useless-nursery _`ASYNC113` : start-soon-in-aenter Using :meth:`~trio.Nursery.start_soon`/:meth:`~anyio.abc.TaskGroup.start_soon` in ``__aenter__`` doesn't wait for the task to begin. - This will only warn about built-in functions and those listed in _`ASYNC114`. - If you're starting a function that does not define `task_status`, then neither will trigger. Consider replacing with :meth:`~trio.Nursery.start`/:meth:`~anyio.abc.TaskGroup.start`. + This will only warn about functions listed in :ref:`ASYNC114 ` or known from Trio. + If you're starting a function that does not define `task_status`, then neither will trigger. _`ASYNC114` : startable-not-in-config Startable function (i.e. has a ``task_status`` keyword parameter) not in :ref:`--startable-in-context-manager <--startable-in-context-manager>` parameter list, please add it so ASYNC113 can catch errors when using it. diff --git a/docs/usage.rst b/docs/usage.rst index be091a8..efa32c8 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -265,8 +265,10 @@ Example ``startable-in-context-manager`` -------------------------------- -Comma-separated list of methods which should be used with :meth:`trio.Nursery.start`/:meth:`anyio.abc.TaskGroup.start` when opening a context manager. -This includes startable functions in the trio and anyio standard library by default, namely :func:`trio.run_process`, :func:`trio.serve_tcp`, :func:`trio.serve_ssl_over_tcp`, :func:`trio.serve_listeners`, :func:`trio.serve`, :func:`anyio.run_process`, :func:`anyio.serve_tcp`, :func:`anyio.serve_ssl_over_tcp`, :func:`anyio.serve_listeners`, and :func:`anyio.serve`. +Comma-separated list of functions which should be used with :meth:`trio.Nursery.start`/:meth:`anyio.abc.TaskGroup.start` when opening a context manager. +We then add known functions from Trio to this list, namely :func:`trio.run_process`, :func:`trio.serve_tcp`, :func:`trio.serve_ssl_over_tcp`, :func:`trio.serve_listeners`, and :meth:`trio.DTLSEndpoint.serve`. +AnyIO does not have any functions in its API that defines ``task_status``. +asyncio does not have an equivalent of :meth:`~trio.Nursery.start`, nor ``task_status``, but you could still add functions to this list that you want to be extra careful about when opening in an `asyncio.TaskGroup` in an ``__aenter__`` Names must be valid identifiers as per :meth:`str.isidentifier`. Used by :ref:`ASYNC113 `, and :ref:`ASYNC114 ` will warn when encountering methods not in the list. diff --git a/flake8_async/visitors/visitor_utility.py b/flake8_async/visitors/visitor_utility.py index bf84354..4474f21 100644 --- a/flake8_async/visitors/visitor_utility.py +++ b/flake8_async/visitors/visitor_utility.py @@ -64,9 +64,10 @@ def visit_ClassDef(self, node: ast.ClassDef): self.save_state(node, "variables", copy=True) def visit_AnnAssign(self, node: ast.AnnAssign): - if not isinstance(node.target, ast.Name): - return - target = node.target.id + if not isinstance(node.target, (ast.Name, ast.Attribute)): + # target can technically be a subscript + return # pragma: no cover + target = ast.unparse(node.target) typename = ast.unparse(node.annotation) self.variables[target] = typename @@ -87,6 +88,8 @@ def visit_Assign(self, node: ast.Assign): self.variables[node.targets[0].id] = value def visit_With(self, node: ast.With | ast.AsyncWith): + # TODO: it's actually the return type of + # `ast.unparse(item.context_expr.func).__[a]enter__()` that should be used if len(node.items) != 1: return item = node.items[0] diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 0091c2b..1ea540a 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -127,7 +127,8 @@ def visit_With(self, node: ast.With | ast.AsyncWith): nursery_type = "taskgroup" start_methods = ("create_task",) else: - continue + # incorrectly marked as not covered on py39 + continue # pragma: no cover # https://github.com/nedbat/coveragepy/issues/198 body_call = node.body[0].value if isinstance(body_call, ast.Await): @@ -170,8 +171,11 @@ class Visitor113(Flake8AsyncVisitor): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) + # this is not entirely correct, it's trio.open_nursery.__aenter__()->trio.Nursery + # but VisitorTypeTracker currently does not work like that. self.typed_calls["trio.open_nursery"] = "trio.Nursery" self.typed_calls["anyio.create_task_group"] = "anyio.TaskGroup" + self.typed_calls["asyncio.TaskGroup"] = "asyncio.TaskGroup" self.async_function = False self.asynccontextmanager = False @@ -198,7 +202,10 @@ def is_startable(n: ast.expr, *startable_list: str) -> bool: return False def is_nursery_call(node: ast.expr): - if not isinstance(node, ast.Attribute) or node.attr != "start_soon": + if not isinstance(node, ast.Attribute) or node.attr not in ( + "start_soon", + "create_task", + ): return False var = ast.unparse(node.value) return ("trio" in self.library and var.endswith("nursery")) or ( diff --git a/tests/eval_files/async102.py b/tests/eval_files/async102.py index 1b932fd..b6c912b 100644 --- a/tests/eval_files/async102.py +++ b/tests/eval_files/async102.py @@ -109,10 +109,18 @@ async def foo(): pass finally: myvar = True - with trio.open_nursery(10) as s: - s.shield = myvar - # this is not safe in theory - because `trio.open_nursery` is an async cm, - # so it's not possible to open a nursery at all. + with trio.CancelScope(deadline=10) as cs: + cs.shield = myvar + # safe in theory, but we don't track variable values + await foo() # error: 12, Statement("try/finally", lineno-7) + try: + pass + finally: + # false alarm, open_nursery does not block/checkpoint on entry. + async with trio.open_nursery() as nursery: # error: 8, Statement("try/finally", lineno-4) + nursery.cancel_scope.deadline = trio.current_time() + 10 + nursery.cancel_scope.shield = True + # false alarm, we currently don't handle nursery.cancel_scope.[deadline/shield] await foo() # error: 12, Statement("try/finally", lineno-8) try: pass diff --git a/tests/eval_files/async113.py b/tests/eval_files/async113.py index 63d1c41..ebddc5b 100644 --- a/tests/eval_files/async113.py +++ b/tests/eval_files/async113.py @@ -1,4 +1,6 @@ # mypy: disable-error-code="arg-type,attr-defined" +# ARG --startable-in-context-manager=my_startable +from __future__ import annotations from contextlib import asynccontextmanager import anyio @@ -22,6 +24,84 @@ async def foo(): boo.start_soon(trio.run_process) # ASYNC113: 4 boo_anyio: anyio.TaskGroup = ... # type: ignore + # false alarm - anyio.run_process is not startable + # (nor is asyncio.run_process) boo_anyio.start_soon(anyio.run_process) # ASYNC113: 4 yield + + +async def my_startable(task_status: trio.TaskStatus[object] = trio.TASK_STATUS_IGNORED): + task_status.started() + await trio.lowlevel.checkpoint() + + +# name of variable being [xxx.]nursery triggers it +class MyCm_named_variable: + def __init__(self): + self.nursery_manager = trio.open_nursery() + self.nursery = None + + async def __aenter__(self): + self.nursery = await self.nursery_manager.__aenter__() + self.nursery.start_soon(my_startable) # ASYNC113: 8 + + async def __aexit__(self, *args): + assert self.nursery is not None + await self.nursery_manager.__aexit__(*args) + + +# call chain is not tracked +# trio.open_nursery -> NurseryManager +# NurseryManager.__aenter__ -> nursery +class MyCm_calls: + async def __aenter__(self): + self.nursery_manager = trio.open_nursery() + self.moo = None + self.moo = await self.nursery_manager.__aenter__() + self.moo.start_soon(my_startable) + + async def __aexit__(self, *args): + assert self.moo is not None + await self.nursery_manager.__aexit__(*args) + + +# types of class variables are not tracked across functions +class MyCm_typehint_class_variable: + def __init__(self): + self.nursery_manager = trio.open_nursery() + self.moo: trio.Nursery = None # type: ignore + + async def __aenter__(self): + self.moo = await self.nursery_manager.__aenter__() + self.moo.start_soon(my_startable) + + async def __aexit__(self, *args): + assert self.moo is not None + await self.nursery_manager.__aexit__(*args) + + +# type hint with __or__ is not picked up +class MyCm_typehint: + async def __aenter__(self): + self.nursery_manager = trio.open_nursery() + self.moo: trio.Nursery | None = None + self.moo = await self.nursery_manager.__aenter__() + self.moo.start_soon(my_startable) + + async def __aexit__(self, *args): + assert self.moo is not None + await self.nursery_manager.__aexit__(*args) + + +# only if the type hint is exactly trio.Nursery +class MyCm_typehint_explicit: + async def __aenter__(self): + self.nursery_manager = trio.open_nursery() + self.moo: trio.Nursery = None # type: ignore + self.moo = await self.nursery_manager.__aenter__() + self.moo.start_soon(my_startable) # ASYNC113: 8 + + async def __aexit__(self, *args): + assert self.moo is not None + await self.nursery_manager.__aexit__(*args) diff --git a/tests/eval_files/async113_anyio.py b/tests/eval_files/async113_anyio.py index 7fd1a95..acbe7dd 100644 --- a/tests/eval_files/async113_anyio.py +++ b/tests/eval_files/async113_anyio.py @@ -1,12 +1,22 @@ # mypy: disable-error-code="arg-type" +# ARG --startable-in-context-manager=my_startable from contextlib import asynccontextmanager import anyio +async def my_startable( + task_status: anyio.abc.TaskStatus[object] = anyio.TASK_STATUS_IGNORED, +): + task_status.started() + await anyio.lowlevel.checkpoint() + + @asynccontextmanager async def foo(): # create_task_group only exists in anyio async with anyio.create_task_group() as bar_tg: + bar_tg.start_soon(my_startable) # ASYNC113: 8 + # false alarm - anyio.run_process is not startable bar_tg.start_soon(anyio.run_process) # ASYNC113: 8 - yield + yield diff --git a/tests/eval_files/async113_asyncio.py b/tests/eval_files/async113_asyncio.py new file mode 100644 index 0000000..008ee5c --- /dev/null +++ b/tests/eval_files/async113_asyncio.py @@ -0,0 +1,17 @@ +# BASE_LIBRARY asyncio +# ARG --startable-in-context-manager=bar +# TRIO_NO_ERROR +# ANYIO_NO_ERROR + +from contextlib import asynccontextmanager +import asyncio + + +async def bar(): ... + + +@asynccontextmanager +async def my_cm(): + async with asyncio.TaskGroup() as tg: # type: ignore[attr-defined] + tg.create_task(bar) # ASYNC113: 8 + yield diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py index c79dc4f..910df8c 100644 --- a/tests/test_flake8_async.py +++ b/tests/test_flake8_async.py @@ -308,7 +308,9 @@ def test_eval( ) if only_check_not_crash: - return + # mark it as skipped to indicate we didn't actually test anything in particular + # (it confused me once when I didn't notice a file was marked with NOTRIO) + pytest.skip() # Check that error messages refer to current library, or to no library. if test not in (