From fa6bdbbc942913fb6ce72deab02043e34299f52d Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 31 Jan 2025 17:57:19 +0100 Subject: [PATCH 01/10] make ASYNC912 and 913 care about cancel points, and ignore schedule points --- docs/changelog.rst | 4 ++++ docs/rules.rst | 5 +++-- docs/usage.rst | 2 +- flake8_async/__init__.py | 2 +- flake8_async/visitors/visitor91x.py | 14 ++++++++++---- tests/autofix_files/async913_trio_anyio.py | 12 ++++++++++++ tests/autofix_files/async913_trio_anyio.py.diff | 9 +++++++++ tests/eval_files/async912.py | 9 ++++++++- tests/eval_files/async913_trio_anyio.py | 11 +++++++++++ tests/test_flake8_async.py | 9 ++++++++- tox.ini | 1 - 11 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 tests/autofix_files/async913_trio_anyio.py create mode 100644 tests/autofix_files/async913_trio_anyio.py.diff create mode 100644 tests/eval_files/async913_trio_anyio.py diff --git a/docs/changelog.rst b/docs/changelog.rst index f1658de..9bd7e36 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ Changelog `CalVer, YY.month.patch `_ +25.2.1 +======= +- :ref:`ASYNC912 ` and :ref:`ASYNC913 ` will now trigger if there's no *cancel* points. This means that :func:`trio.open_nursery`/`anyio.create_task_group` will not silence them on their own, unless they're guaranteed to start tasks. + 25.1.1 ======= - Add :ref:`ASYNC124 ` async-function-could-be-sync diff --git a/docs/rules.rst b/docs/rules.rst index b2bef0b..9d32e14 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -197,12 +197,13 @@ _`ASYNC911` : async-generator-no-checkpoint Exit, ``yield`` or ``return`` from async iterable with no guaranteed :ref:`checkpoint` since possible function entry (``yield`` or function definition). _`ASYNC912` : cancel-scope-no-guaranteed-checkpoint - A timeout/cancelscope has :ref:`checkpoints `, but they're not guaranteed to run. - Similar to `ASYNC100`_, but it does not warn on trivial cases where there is no checkpoint at all. + A timeout/cancelscope has :ref:`cancel points `, but they're not guaranteed to run. + Similar to `ASYNC100`_, but it does not warn on trivial cases where there is no cancel point at all. It instead shares logic with `ASYNC910`_ and `ASYNC911`_ for parsing conditionals and branches. _`ASYNC913` : indefinite-loop-no-guaranteed-checkpoint An indefinite loop (e.g. ``while True``) has no guaranteed :ref:`checkpoint `. This could potentially cause a deadlock. + This will also error if there's no guaranteed :ref:`cancel point`, where even though it won't deadlock the loop might become an uncancelable dry-run loop. .. _autofix-support: diff --git a/docs/usage.rst b/docs/usage.rst index d72e812..e6e116f 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -33,7 +33,7 @@ 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.1.1 + rev: 25.2.1 hooks: - id: flake8-async # args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC] diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py index 405aa28..aa715cb 100644 --- a/flake8_async/__init__.py +++ b/flake8_async/__init__.py @@ -38,7 +38,7 @@ # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1" -__version__ = "25.1.1" +__version__ = "25.2.1" # taken from https://github.com/Zac-HD/shed diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py index 0036c79..b664e64 100644 --- a/flake8_async/visitors/visitor91x.py +++ b/flake8_async/visitors/visitor91x.py @@ -354,10 +354,10 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors): "on line {1.lineno}." ), "ASYNC912": ( - "CancelScope with no guaranteed checkpoint. This makes it potentially " + "CancelScope with no guaranteed cancel point. This makes it potentially " "impossible to cancel." ), - "ASYNC913": ("Indefinite loop with no guaranteed checkpoint."), + "ASYNC913": ("Indefinite loop with no guaranteed cancel points."), "ASYNC100": ( "{0}.{1} context contains no checkpoints, remove the context or add" " `await {0}.lowlevel.checkpoint()`." @@ -401,10 +401,16 @@ def checkpoint_cancel_point(self) -> None: self.taskgroup_has_start_soon.clear() def checkpoint_schedule_point(self) -> None: - self.uncheckpointed_statements = set() + # ASYNC912&ASYNC913 only cares about cancel points, so don't remove + # them if we only do a schedule point + self.uncheckpointed_statements = { + s + for s in self.uncheckpointed_statements + if isinstance(s, ArtificialStatement) + } def checkpoint(self) -> None: - self.checkpoint_schedule_point() + self.uncheckpointed_statements = set() self.checkpoint_cancel_point() def checkpoint_statement(self) -> cst.SimpleStatementLine: diff --git a/tests/autofix_files/async913_trio_anyio.py b/tests/autofix_files/async913_trio_anyio.py new file mode 100644 index 0000000..76157ed --- /dev/null +++ b/tests/autofix_files/async913_trio_anyio.py @@ -0,0 +1,12 @@ +# ARG --enable=ASYNC913 +# AUTOFIX +# ASYNCIO_NO_ERROR + +import trio + + +async def nursery_no_cancel_point(): + while True: # ASYNC913: 4 + await trio.lowlevel.checkpoint() + async with trio.open_nursery(): + pass diff --git a/tests/autofix_files/async913_trio_anyio.py.diff b/tests/autofix_files/async913_trio_anyio.py.diff new file mode 100644 index 0000000..dc16728 --- /dev/null +++ b/tests/autofix_files/async913_trio_anyio.py.diff @@ -0,0 +1,9 @@ +--- ++++ +@@ x,5 x,6 @@ + + async def nursery_no_cancel_point(): + while True: # ASYNC913: 4 ++ await trio.lowlevel.checkpoint() + async with trio.open_nursery(): + pass diff --git a/tests/eval_files/async912.py b/tests/eval_files/async912.py index 1813ff2..040c39c 100644 --- a/tests/eval_files/async912.py +++ b/tests/eval_files/async912.py @@ -125,7 +125,7 @@ def customWrapper(a: T) -> T: with (res := trio.fail_at(10)): ... # but saving with `as` does - with trio.fail_at(10) as res: # ASYNC912: 9 + with trio.fail_at(10) as res2: # ASYNC912: 9 if bar(): await trio.lowlevel.checkpoint() @@ -189,3 +189,10 @@ async def check_yield_logic(): if bar(): await trio.lowlevel.checkpoint() yield + + +async def nursery_no_cancel_point(): + with trio.move_on_after(10): # ASYNC912: 9 + async with trio.open_nursery(): + if bar(): + await trio.lowlevel.checkpoint() diff --git a/tests/eval_files/async913_trio_anyio.py b/tests/eval_files/async913_trio_anyio.py new file mode 100644 index 0000000..28fc0a1 --- /dev/null +++ b/tests/eval_files/async913_trio_anyio.py @@ -0,0 +1,11 @@ +# ARG --enable=ASYNC913 +# AUTOFIX +# ASYNCIO_NO_ERROR + +import trio + + +async def nursery_no_cancel_point(): + while True: # ASYNC913: 4 + async with trio.open_nursery(): + pass diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py index 733ec0c..0895764 100644 --- a/tests/test_flake8_async.py +++ b/tests/test_flake8_async.py @@ -103,7 +103,14 @@ def diff_strings(first: str, second: str, /) -> str: # replaces all instances of `original` with `new` in string # unless it's preceded by a `-`, which indicates it's part of a command-line flag def replace_library(string: str, original: str = "trio", new: str = "anyio") -> str: - return re.sub(rf"(? str: + return re.sub(rf"(? Date: Fri, 31 Jan 2025 18:13:18 +0100 Subject: [PATCH 02/10] fix doc reference --- docs/rules.rst | 2 +- tox.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/rules.rst b/docs/rules.rst index 9d32e14..974d99c 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -203,7 +203,7 @@ _`ASYNC912` : cancel-scope-no-guaranteed-checkpoint _`ASYNC913` : indefinite-loop-no-guaranteed-checkpoint An indefinite loop (e.g. ``while True``) has no guaranteed :ref:`checkpoint `. This could potentially cause a deadlock. - This will also error if there's no guaranteed :ref:`cancel point`, where even though it won't deadlock the loop might become an uncancelable dry-run loop. + This will also error if there's no guaranteed :ref:`cancel point `, where even though it won't deadlock the loop might become an uncancelable dry-run loop. .. _autofix-support: diff --git a/tox.ini b/tox.ini index b7579d7..2bcca72 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,7 @@ allowlist_externals = make changedir = docs skip_install = True commands = + make clean make html # Settings for other tools From 88d2e1f93bef19bc80d07cd7ba32d6d1868a00e5 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 31 Jan 2025 18:50:12 +0100 Subject: [PATCH 03/10] all tests passing :tada: --- flake8_async/visitors/visitors.py | 20 ++++++++++++-------- tests/autofix_files/async100_anyio.py | 1 - tests/autofix_files/async100_trio.py | 1 - tests/eval_files/async100_anyio.py | 1 - tests/eval_files/async100_trio.py | 1 - tests/eval_files/async101.py | 15 +++++++++++---- tests/eval_files/async101_anyio.py | 1 - tests/eval_files/async101_trio.py | 1 - tests/eval_files/async111.py | 1 - tests/eval_files/async111_anyio.py | 1 - tests/eval_files/async112.py | 1 - tests/eval_files/async112_anyio.py | 5 ++--- tests/eval_files/async112_asyncio.py | 2 +- tests/eval_files/async113_trio.py | 1 - tests/eval_files/async121.py | 1 - tests/eval_files/async121_anyio.py | 1 - tests/test_flake8_async.py | 3 +++ 17 files changed, 29 insertions(+), 28 deletions(-) diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 54f14cf..2a8d824 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -121,10 +121,10 @@ def visit_With(self, node: ast.With | ast.AsyncWith): elif get_matching_call( item.context_expr, "create_task_group", base="anyio" ): - nursery_type = "taskgroup" + nursery_type = "task group" # check for asyncio.TaskGroup elif get_matching_call(item.context_expr, "TaskGroup", base="asyncio"): - nursery_type = "taskgroup" + nursery_type = "task group" start_methods = ("create_task",) else: # incorrectly marked as not covered on py39 @@ -213,12 +213,16 @@ def is_nursery_call(node: ast.expr): ): return False var = ast.unparse(node.value) - return ("trio" in self.library and var.endswith("nursery")) or ( - self.variables.get(var, "") - in ( - "trio.Nursery", - "anyio.TaskGroup", - "asyncio.TaskGroup", + return ( + ("trio" in self.library and var.endswith("nursery")) + or ("anyio" in self.library and var.endswith("task_group")) + or ( + self.variables.get(var, "") + in ( + "trio.Nursery", + "anyio.TaskGroup", + "asyncio.TaskGroup", + ) ) ) diff --git a/tests/autofix_files/async100_anyio.py b/tests/autofix_files/async100_anyio.py index 80516cf..9671661 100644 --- a/tests/autofix_files/async100_anyio.py +++ b/tests/autofix_files/async100_anyio.py @@ -1,6 +1,5 @@ # AUTOFIX # BASE_LIBRARY anyio -# NOTRIO # trio.create_task_group doesn't exist # ASYNCIO_NO_ERROR import anyio diff --git a/tests/autofix_files/async100_trio.py b/tests/autofix_files/async100_trio.py index ed61139..875a8ff 100644 --- a/tests/autofix_files/async100_trio.py +++ b/tests/autofix_files/async100_trio.py @@ -1,6 +1,5 @@ # AUTOFIX # ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist -# NOANYIO # anyio.open_nursery doesn't exist import trio diff --git a/tests/eval_files/async100_anyio.py b/tests/eval_files/async100_anyio.py index f274969..edf1ac6 100644 --- a/tests/eval_files/async100_anyio.py +++ b/tests/eval_files/async100_anyio.py @@ -1,6 +1,5 @@ # AUTOFIX # BASE_LIBRARY anyio -# NOTRIO # trio.create_task_group doesn't exist # ASYNCIO_NO_ERROR import anyio diff --git a/tests/eval_files/async100_trio.py b/tests/eval_files/async100_trio.py index d722185..d3d263c 100644 --- a/tests/eval_files/async100_trio.py +++ b/tests/eval_files/async100_trio.py @@ -1,6 +1,5 @@ # AUTOFIX # ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist -# NOANYIO # anyio.open_nursery doesn't exist import trio diff --git a/tests/eval_files/async101.py b/tests/eval_files/async101.py index 1623f24..f936b87 100644 --- a/tests/eval_files/async101.py +++ b/tests/eval_files/async101.py @@ -2,10 +2,6 @@ # ARG --no-checkpoint-warning-decorator=no_checkpoint_warning_decorator # ARG --transform-async-generator-decorators=transform_async_gen_decorator -# This file contains errors shared between trio and anyio, since they have some -# overlap in naming. -# See async101_xxx which has errors specific to trio/asyncio/anyio. - import contextlib import contextlib as bla @@ -149,3 +145,14 @@ def no_checkpoint_warning_deco_fun(): def transfor_async_gen_deco_fun(): with trio.CancelScope(): yield 1 # safe + + +async def foo_open_nursery(): + async with trio.open_nursery() as _: + yield 1 # error: 8 + + +@asynccontextmanager +async def foo_open_nursery_contextmanager(): + async with trio.open_nursery() as _: + yield 1 # safe diff --git a/tests/eval_files/async101_anyio.py b/tests/eval_files/async101_anyio.py index 9f16b8c..0108ad2 100644 --- a/tests/eval_files/async101_anyio.py +++ b/tests/eval_files/async101_anyio.py @@ -1,5 +1,4 @@ # BASE_LIBRARY anyio -# TRIO_NO_ERROR # ASYNCIO_NO_ERROR import anyio diff --git a/tests/eval_files/async101_trio.py b/tests/eval_files/async101_trio.py index 3ceda36..f146a32 100644 --- a/tests/eval_files/async101_trio.py +++ b/tests/eval_files/async101_trio.py @@ -1,5 +1,4 @@ # ASYNCIO_NO_ERROR -# ANYIO_NO_ERROR from contextlib import asynccontextmanager diff --git a/tests/eval_files/async111.py b/tests/eval_files/async111.py index cf29a05..a0ebc08 100644 --- a/tests/eval_files/async111.py +++ b/tests/eval_files/async111.py @@ -3,7 +3,6 @@ # It's possible there's an equivalent asyncio construction/gotcha, but methods are differently named, so this file will not raise any errors # nurseries are named taskgroups in asyncio/anyio # ASYNCIO_NO_ERROR -# ANYIO_NO_ERROR from typing import Any import trio diff --git a/tests/eval_files/async111_anyio.py b/tests/eval_files/async111_anyio.py index 7d99fac..01f80c6 100644 --- a/tests/eval_files/async111_anyio.py +++ b/tests/eval_files/async111_anyio.py @@ -2,7 +2,6 @@ # this only tests anyio.create_task_group in particular # BASE_LIBRARY anyio # ASYNCIO_NO_ERROR -# TRIO_NO_ERROR import anyio diff --git a/tests/eval_files/async112.py b/tests/eval_files/async112.py index 687443d..8657973 100644 --- a/tests/eval_files/async112.py +++ b/tests/eval_files/async112.py @@ -1,7 +1,6 @@ # type: ignore # ASYNC112: Nursery body with only a call to nursery.start[_soon] and not passing itself as a parameter can be replaced with a regular function call. # ASYNCIO_NO_ERROR -# ANYIO_NO_ERROR import functools from functools import partial diff --git a/tests/eval_files/async112_anyio.py b/tests/eval_files/async112_anyio.py index 52cf887..5b74f9d 100644 --- a/tests/eval_files/async112_anyio.py +++ b/tests/eval_files/async112_anyio.py @@ -2,7 +2,6 @@ # this only tests anyio.create_task_group in particular # BASE_LIBRARY anyio # ASYNCIO_NO_ERROR -# TRIO_NO_ERROR import anyio @@ -11,13 +10,13 @@ async def bar(*args): ... async def foo(): - async with anyio.create_task_group() as tg: # error: 15, "tg", "taskgroup" + async with anyio.create_task_group() as tg: # error: 15, "tg", "task group" 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", "taskgroup" + async with anyio.create_task_group() as tg: # error: 15, "tg", "task group" 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 d29550c..c0c4b79 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", "taskgroup" + async with asyncio.TaskGroup() as tg: # error: 15, "tg", "task group" tg.create_task(bar()) async with asyncio.TaskGroup() as tg: diff --git a/tests/eval_files/async113_trio.py b/tests/eval_files/async113_trio.py index 3d50c90..983a0e0 100644 --- a/tests/eval_files/async113_trio.py +++ b/tests/eval_files/async113_trio.py @@ -12,7 +12,6 @@ # ARG --startable-in-context-manager=custom_startable_function -# ANYIO_NO_ERROR - anyio uses TaskGroups. Checked in async113.py & async113_anyio.py # ASYNCIO_NO_ERROR - asyncio uses TaskGroups. Checked in async113.py diff --git a/tests/eval_files/async121.py b/tests/eval_files/async121.py index b55f21b..f285597 100644 --- a/tests/eval_files/async121.py +++ b/tests/eval_files/async121.py @@ -1,5 +1,4 @@ # ASYNCIO_NO_ERROR # checked in async121_asyncio.py -# ANYIO_NO_ERROR # checked in async121_anyio.py import trio from typing import Any diff --git a/tests/eval_files/async121_anyio.py b/tests/eval_files/async121_anyio.py index 12be0ae..2a9a322 100644 --- a/tests/eval_files/async121_anyio.py +++ b/tests/eval_files/async121_anyio.py @@ -1,5 +1,4 @@ # ASYNCIO_NO_ERROR # checked in async121_asyncio.py -# TRIO_NO_ERROR # checked in async121.py # BASE_LIBRARY anyio import anyio diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py index 0895764..6a00fe0 100644 --- a/tests/test_flake8_async.py +++ b/tests/test_flake8_async.py @@ -108,8 +108,11 @@ def replace_str(string: str, original: str, new: str) -> str: if original == "trio" and new == "anyio": string = replace_str(string, "trio.open_nursery", "anyio.create_task_group") + string = replace_str(string, '"nursery"', '"task group"') + string = replace_str(string, "nursery", "task_group") elif original == "anyio" and new == "trio": string = replace_str(string, "anyio.create_task_group", "trio.open_nursery") + string = replace_str(string, "task group", "nursery") return replace_str(string, original, new) From 3fb511881719feb6b090571f091492eefcbe2a1d Mon Sep 17 00:00:00 2001 From: jakkdl Date: Mon, 3 Feb 2025 13:21:56 +0100 Subject: [PATCH 04/10] delete now redundant eval files, fix async113 false alarm on [not trio].serve_listeners etc --- flake8_async/visitors/visitors.py | 17 +++-- tests/autofix_files/async100.py | 74 ++++++++++++++++++++++ tests/autofix_files/async100.py.diff | 69 ++++++++++++++++++-- tests/autofix_files/async100_anyio.py | 25 -------- tests/autofix_files/async100_anyio.py.diff | 23 ------- tests/autofix_files/async100_trio.py | 74 ---------------------- tests/autofix_files/async100_trio.py.diff | 57 ----------------- tests/eval_files/async100.py | 74 ++++++++++++++++++++++ tests/eval_files/async100_anyio.py | 25 -------- tests/eval_files/async100_trio.py | 74 ---------------------- tests/eval_files/async101_anyio.py | 9 --- tests/eval_files/async101_trio.py | 16 ----- tests/eval_files/async111_anyio.py | 20 ------ tests/eval_files/async112_anyio.py | 27 -------- tests/eval_files/async113.py | 47 +++++++++----- tests/eval_files/async113_anyio.py | 22 ------- tests/eval_files/async113_trio.py | 13 ++-- tests/test_flake8_async.py | 2 + 18 files changed, 264 insertions(+), 404 deletions(-) delete mode 100644 tests/autofix_files/async100_anyio.py delete mode 100644 tests/autofix_files/async100_anyio.py.diff delete mode 100644 tests/autofix_files/async100_trio.py delete mode 100644 tests/autofix_files/async100_trio.py.diff delete mode 100644 tests/eval_files/async100_anyio.py delete mode 100644 tests/eval_files/async100_trio.py delete mode 100644 tests/eval_files/async101_anyio.py delete mode 100644 tests/eval_files/async101_trio.py delete mode 100644 tests/eval_files/async111_anyio.py delete mode 100644 tests/eval_files/async112_anyio.py delete mode 100644 tests/eval_files/async113_anyio.py diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 2a8d824..02c4948 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -151,12 +151,12 @@ def visit_With(self, node: ast.With | ast.AsyncWith): # used in 113 and 114 -STARTABLE_CALLS = ( +STARTABLE_CALLS = ("serve",) +TRIO_STARTABLE_CALLS = ( "run_process", "serve_ssl_over_tcp", "serve_tcp", "serve_listeners", - "serve", ) @@ -201,7 +201,11 @@ def is_startable(n: ast.expr, *startable_list: str) -> bool: if isinstance(n, ast.Name): return n.id in startable_list if isinstance(n, ast.Attribute): - return n.attr in startable_list + return n.attr in startable_list and not ( + n.attr in TRIO_STARTABLE_CALLS + and isinstance(n.value, ast.Name) + and n.value.id != "trio" + ) if isinstance(n, ast.Call): return any(is_startable(nn, *startable_list) for nn in n.args) return False @@ -233,6 +237,7 @@ def is_nursery_call(node: ast.expr): and is_startable( node.args[0], *STARTABLE_CALLS, + *TRIO_STARTABLE_CALLS, *self.options.startable_in_context_manager, ) ): @@ -258,7 +263,11 @@ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): for n in self.walk(*node.args.args, *node.args.kwonlyargs) ) and not any( node.name == opt - for opt in (*self.options.startable_in_context_manager, *STARTABLE_CALLS) + for opt in ( + *self.options.startable_in_context_manager, + *STARTABLE_CALLS, + *TRIO_STARTABLE_CALLS, + ) ): self.error(node, node.name) diff --git a/tests/autofix_files/async100.py b/tests/autofix_files/async100.py index bfcad5f..2c081cd 100644 --- a/tests/autofix_files/async100.py +++ b/tests/autofix_files/async100.py @@ -140,3 +140,77 @@ async def dont_crash_on_non_name_or_attr_call(): async def another_weird_with_call(): async with a().b(): ... + + +# ---open_nursery / create_task_group stuff--- + + +async def nursery_no_cancel_point(): + # error: 9, "trio", "CancelScope" + async with trio.open_nursery(): + ... + + +# but it is a cancel point if the nursery contains a call to start_soon() + + +async def nursery_start_soon(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + n.start_soon(trio.sleep, 0) + + +async def nursery_start_soon_misnested(): + async with trio.open_nursery() as n: + # error: 13, "trio", "CancelScope" + n.start_soon(trio.sleep, 0) + + +async def nested_scope(): + with trio.CancelScope(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + n.start_soon(trio.sleep, 0) + + +async def nested_nursery(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + async with trio.open_nursery() as n2: + n2.start_soon(trio.sleep, 0) + + +async def nested_function_call(): + + # error: 9, "trio", "CancelScope" + async with trio.open_nursery() as n: + + def foo(): + n.start_soon(trio.sleep, 0) + + # a false alarm in case we call foo()... but we can't check if they do + foo() + + +# insert cancel point on nursery exit, not at the start_soon call +async def cancel_point_on_nursery_exit(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + # error: 17, "trio", "CancelScope" + n.start_soon(trio.sleep, 0) + + +# async100 does not consider *redundant* cancel scopes +async def redundant_cancel_scope(): + with trio.CancelScope(): + with trio.CancelScope(): + await trio.lowlevel.checkpoint() + + +# but if it did then none of these scopes should be marked redundant +# The inner checks task startup, the outer checks task exit +async def nursery_exit_blocks_with_start(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + with trio.CancelScope(): + await n.start(trio.sleep, 0) diff --git a/tests/autofix_files/async100.py.diff b/tests/autofix_files/async100.py.diff index 268f7d8..a6095a2 100644 --- a/tests/autofix_files/async100.py.diff +++ b/tests/autofix_files/async100.py.diff @@ -96,14 +96,15 @@ await trio.sleep_forever() - with trio.CancelScope(): # error: 13, "trio", "CancelScope" - ... -+ # error: 13, "trio", "CancelScope" -+ ... - +- - with trio.fail_after(1): # error: 9, "trio", "fail_after" - with trio.CancelScope(): # error: 13, "trio", "CancelScope" - ... - with trio.CancelScope(): # error: 13, "trio", "CancelScope" - ... ++ # error: 13, "trio", "CancelScope" ++ ... ++ + # error: 9, "trio", "fail_after" + # error: 13, "trio", "CancelScope" + ... @@ -119,10 +120,11 @@ - with trio.fail_after(1): # error: 9, "trio", "fail_after" - with contextlib.suppress(Exception): - print("foo") -+ # error: 9, "trio", "fail_after" - with contextlib.suppress(Exception): +- with contextlib.suppress(Exception): - with trio.fail_after(1): # error: 13, "trio", "fail_after" - print("foo") ++ # error: 9, "trio", "fail_after" ++ with contextlib.suppress(Exception): + print("foo") + with contextlib.suppress(Exception): + # error: 13, "trio", "fail_after" @@ -130,3 +132,60 @@ with contextlib.suppress(Exception): with open("blah") as file: +@@ x,9 x,9 @@ + + + async def nursery_no_cancel_point(): +- with trio.CancelScope(): # error: 9, "trio", "CancelScope" +- async with trio.open_nursery(): +- ... ++ # error: 9, "trio", "CancelScope" ++ async with trio.open_nursery(): ++ ... + + + # but it is a cancel point if the nursery contains a call to start_soon() +@@ x,8 x,8 @@ + + async def nursery_start_soon_misnested(): + async with trio.open_nursery() as n: +- with trio.CancelScope(): # error: 13, "trio", "CancelScope" +- n.start_soon(trio.sleep, 0) ++ # error: 13, "trio", "CancelScope" ++ n.start_soon(trio.sleep, 0) + + + async def nested_scope(): +@@ x,22 x,22 @@ + + async def nested_function_call(): + +- with trio.CancelScope(): # error: 9, "trio", "CancelScope" +- async with trio.open_nursery() as n: +- +- def foo(): +- n.start_soon(trio.sleep, 0) +- +- # a false alarm in case we call foo()... but we can't check if they do +- foo() ++ # error: 9, "trio", "CancelScope" ++ async with trio.open_nursery() as n: ++ ++ def foo(): ++ n.start_soon(trio.sleep, 0) ++ ++ # a false alarm in case we call foo()... but we can't check if they do ++ foo() + + + # insert cancel point on nursery exit, not at the start_soon call + async def cancel_point_on_nursery_exit(): + with trio.CancelScope(): + async with trio.open_nursery() as n: +- with trio.CancelScope(): # error: 17, "trio", "CancelScope" +- n.start_soon(trio.sleep, 0) ++ # error: 17, "trio", "CancelScope" ++ n.start_soon(trio.sleep, 0) + + + # async100 does not consider *redundant* cancel scopes diff --git a/tests/autofix_files/async100_anyio.py b/tests/autofix_files/async100_anyio.py deleted file mode 100644 index 9671661..0000000 --- a/tests/autofix_files/async100_anyio.py +++ /dev/null @@ -1,25 +0,0 @@ -# AUTOFIX -# BASE_LIBRARY anyio -# ASYNCIO_NO_ERROR -import anyio - - -async def bar() -> None: ... - - -async def anyio_cancelscope(): - # error: 9, "anyio", "CancelScope" - ... - - -# see async100_trio for more comprehensive tests -async def nursery_no_cancel_point(): - # error: 9, "anyio", "CancelScope" - async with anyio.create_task_group(): - ... - - -async def nursery_with_start_soon(): - with anyio.CancelScope(): - async with anyio.create_task_group() as tg: - tg.start_soon(bar) diff --git a/tests/autofix_files/async100_anyio.py.diff b/tests/autofix_files/async100_anyio.py.diff deleted file mode 100644 index 3e57cd9..0000000 --- a/tests/autofix_files/async100_anyio.py.diff +++ /dev/null @@ -1,23 +0,0 @@ ---- -+++ -@@ x,15 x,15 @@ - - - async def anyio_cancelscope(): -- with anyio.CancelScope(): # error: 9, "anyio", "CancelScope" -- ... -+ # error: 9, "anyio", "CancelScope" -+ ... - - - # see async100_trio for more comprehensive tests - async def nursery_no_cancel_point(): -- with anyio.CancelScope(): # error: 9, "anyio", "CancelScope" -- async with anyio.create_task_group(): -- ... -+ # error: 9, "anyio", "CancelScope" -+ async with anyio.create_task_group(): -+ ... - - - async def nursery_with_start_soon(): diff --git a/tests/autofix_files/async100_trio.py b/tests/autofix_files/async100_trio.py deleted file mode 100644 index 875a8ff..0000000 --- a/tests/autofix_files/async100_trio.py +++ /dev/null @@ -1,74 +0,0 @@ -# AUTOFIX -# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist -import trio - - -async def nursery_no_cancel_point(): - # error: 9, "trio", "CancelScope" - async with trio.open_nursery(): - ... - - -# but it is a cancel point if the nursery contains a call to start_soon() - - -async def nursery_start_soon(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - n.start_soon(trio.sleep, 0) - - -async def nursery_start_soon_misnested(): - async with trio.open_nursery() as n: - # error: 13, "trio", "CancelScope" - n.start_soon(trio.sleep, 0) - - -async def nested_scope(): - with trio.CancelScope(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - n.start_soon(trio.sleep, 0) - - -async def nested_nursery(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - async with trio.open_nursery() as n2: - n2.start_soon(trio.sleep, 0) - - -async def nested_function_call(): - - # error: 9, "trio", "CancelScope" - async with trio.open_nursery() as n: - - def foo(): - n.start_soon(trio.sleep, 0) - - # a false alarm in case we call foo()... but we can't check if they do - foo() - - -# insert cancel point on nursery exit, not at the start_soon call -async def cancel_point_on_nursery_exit(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - # error: 17, "trio", "CancelScope" - n.start_soon(trio.sleep, 0) - - -# async100 does not consider *redundant* cancel scopes -async def redundant_cancel_scope(): - with trio.CancelScope(): - with trio.CancelScope(): - await trio.lowlevel.checkpoint() - - -# but if it did then none of these scopes should be marked redundant -# The inner checks task startup, the outer checks task exit -async def nursery_exit_blocks_with_start(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - with trio.CancelScope(): - await n.start(trio.sleep, 0) diff --git a/tests/autofix_files/async100_trio.py.diff b/tests/autofix_files/async100_trio.py.diff deleted file mode 100644 index e9ff0d1..0000000 --- a/tests/autofix_files/async100_trio.py.diff +++ /dev/null @@ -1,57 +0,0 @@ ---- -+++ -@@ x,9 x,9 @@ - - - async def nursery_no_cancel_point(): -- with trio.CancelScope(): # error: 9, "trio", "CancelScope" -- async with trio.open_nursery(): -- ... -+ # error: 9, "trio", "CancelScope" -+ async with trio.open_nursery(): -+ ... - - - # but it is a cancel point if the nursery contains a call to start_soon() -@@ x,8 x,8 @@ - - async def nursery_start_soon_misnested(): - async with trio.open_nursery() as n: -- with trio.CancelScope(): # error: 13, "trio", "CancelScope" -- n.start_soon(trio.sleep, 0) -+ # error: 13, "trio", "CancelScope" -+ n.start_soon(trio.sleep, 0) - - - async def nested_scope(): -@@ x,22 x,22 @@ - - async def nested_function_call(): - -- with trio.CancelScope(): # error: 9, "trio", "CancelScope" -- async with trio.open_nursery() as n: -+ # error: 9, "trio", "CancelScope" -+ async with trio.open_nursery() as n: - -- def foo(): -- n.start_soon(trio.sleep, 0) -+ def foo(): -+ n.start_soon(trio.sleep, 0) - -- # a false alarm in case we call foo()... but we can't check if they do -- foo() -+ # a false alarm in case we call foo()... but we can't check if they do -+ foo() - - - # insert cancel point on nursery exit, not at the start_soon call - async def cancel_point_on_nursery_exit(): - with trio.CancelScope(): - async with trio.open_nursery() as n: -- with trio.CancelScope(): # error: 17, "trio", "CancelScope" -- n.start_soon(trio.sleep, 0) -+ # error: 17, "trio", "CancelScope" -+ n.start_soon(trio.sleep, 0) - - - # async100 does not consider *redundant* cancel scopes diff --git a/tests/eval_files/async100.py b/tests/eval_files/async100.py index 2c6cf47..c51a126 100644 --- a/tests/eval_files/async100.py +++ b/tests/eval_files/async100.py @@ -140,3 +140,77 @@ async def dont_crash_on_non_name_or_attr_call(): async def another_weird_with_call(): async with a().b(): ... + + +# ---open_nursery / create_task_group stuff--- + + +async def nursery_no_cancel_point(): + with trio.CancelScope(): # error: 9, "trio", "CancelScope" + async with trio.open_nursery(): + ... + + +# but it is a cancel point if the nursery contains a call to start_soon() + + +async def nursery_start_soon(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + n.start_soon(trio.sleep, 0) + + +async def nursery_start_soon_misnested(): + async with trio.open_nursery() as n: + with trio.CancelScope(): # error: 13, "trio", "CancelScope" + n.start_soon(trio.sleep, 0) + + +async def nested_scope(): + with trio.CancelScope(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + n.start_soon(trio.sleep, 0) + + +async def nested_nursery(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + async with trio.open_nursery() as n2: + n2.start_soon(trio.sleep, 0) + + +async def nested_function_call(): + + with trio.CancelScope(): # error: 9, "trio", "CancelScope" + async with trio.open_nursery() as n: + + def foo(): + n.start_soon(trio.sleep, 0) + + # a false alarm in case we call foo()... but we can't check if they do + foo() + + +# insert cancel point on nursery exit, not at the start_soon call +async def cancel_point_on_nursery_exit(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + with trio.CancelScope(): # error: 17, "trio", "CancelScope" + n.start_soon(trio.sleep, 0) + + +# async100 does not consider *redundant* cancel scopes +async def redundant_cancel_scope(): + with trio.CancelScope(): + with trio.CancelScope(): + await trio.lowlevel.checkpoint() + + +# but if it did then none of these scopes should be marked redundant +# The inner checks task startup, the outer checks task exit +async def nursery_exit_blocks_with_start(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + with trio.CancelScope(): + await n.start(trio.sleep, 0) diff --git a/tests/eval_files/async100_anyio.py b/tests/eval_files/async100_anyio.py deleted file mode 100644 index edf1ac6..0000000 --- a/tests/eval_files/async100_anyio.py +++ /dev/null @@ -1,25 +0,0 @@ -# AUTOFIX -# BASE_LIBRARY anyio -# ASYNCIO_NO_ERROR -import anyio - - -async def bar() -> None: ... - - -async def anyio_cancelscope(): - with anyio.CancelScope(): # error: 9, "anyio", "CancelScope" - ... - - -# see async100_trio for more comprehensive tests -async def nursery_no_cancel_point(): - with anyio.CancelScope(): # error: 9, "anyio", "CancelScope" - async with anyio.create_task_group(): - ... - - -async def nursery_with_start_soon(): - with anyio.CancelScope(): - async with anyio.create_task_group() as tg: - tg.start_soon(bar) diff --git a/tests/eval_files/async100_trio.py b/tests/eval_files/async100_trio.py deleted file mode 100644 index d3d263c..0000000 --- a/tests/eval_files/async100_trio.py +++ /dev/null @@ -1,74 +0,0 @@ -# AUTOFIX -# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist -import trio - - -async def nursery_no_cancel_point(): - with trio.CancelScope(): # error: 9, "trio", "CancelScope" - async with trio.open_nursery(): - ... - - -# but it is a cancel point if the nursery contains a call to start_soon() - - -async def nursery_start_soon(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - n.start_soon(trio.sleep, 0) - - -async def nursery_start_soon_misnested(): - async with trio.open_nursery() as n: - with trio.CancelScope(): # error: 13, "trio", "CancelScope" - n.start_soon(trio.sleep, 0) - - -async def nested_scope(): - with trio.CancelScope(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - n.start_soon(trio.sleep, 0) - - -async def nested_nursery(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - async with trio.open_nursery() as n2: - n2.start_soon(trio.sleep, 0) - - -async def nested_function_call(): - - with trio.CancelScope(): # error: 9, "trio", "CancelScope" - async with trio.open_nursery() as n: - - def foo(): - n.start_soon(trio.sleep, 0) - - # a false alarm in case we call foo()... but we can't check if they do - foo() - - -# insert cancel point on nursery exit, not at the start_soon call -async def cancel_point_on_nursery_exit(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - with trio.CancelScope(): # error: 17, "trio", "CancelScope" - n.start_soon(trio.sleep, 0) - - -# async100 does not consider *redundant* cancel scopes -async def redundant_cancel_scope(): - with trio.CancelScope(): - with trio.CancelScope(): - await trio.lowlevel.checkpoint() - - -# but if it did then none of these scopes should be marked redundant -# The inner checks task startup, the outer checks task exit -async def nursery_exit_blocks_with_start(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - with trio.CancelScope(): - await n.start(trio.sleep, 0) diff --git a/tests/eval_files/async101_anyio.py b/tests/eval_files/async101_anyio.py deleted file mode 100644 index 0108ad2..0000000 --- a/tests/eval_files/async101_anyio.py +++ /dev/null @@ -1,9 +0,0 @@ -# BASE_LIBRARY anyio -# ASYNCIO_NO_ERROR - -import anyio - - -async def foo(): - async with anyio.create_task_group(): - yield 1 # error: 8 diff --git a/tests/eval_files/async101_trio.py b/tests/eval_files/async101_trio.py deleted file mode 100644 index f146a32..0000000 --- a/tests/eval_files/async101_trio.py +++ /dev/null @@ -1,16 +0,0 @@ -# ASYNCIO_NO_ERROR - -from contextlib import asynccontextmanager - -import trio - - -async def foo_open_nursery(): - async with trio.open_nursery() as _: - yield 1 # error: 8 - - -@asynccontextmanager -async def foo_open_nursery_contextmanager(): - async with trio.open_nursery() as _: - yield 1 # safe diff --git a/tests/eval_files/async111_anyio.py b/tests/eval_files/async111_anyio.py deleted file mode 100644 index 01f80c6..0000000 --- a/tests/eval_files/async111_anyio.py +++ /dev/null @@ -1,20 +0,0 @@ -# main tests in async111.py -# this only tests anyio.create_task_group in particular -# BASE_LIBRARY anyio -# ASYNCIO_NO_ERROR - - -import anyio - - -async def bar(*args): ... - - -async def foo(): - async with anyio.create_task_group() as tg: - with open("") as f: - await tg.start(bar, f) # error: 32, lineno-1, lineno-2, "f", "start" - tg.start_soon(bar, f) # error: 31, lineno-2, lineno-3, "f", "start_soon" - - # create_task does not exist in anyio, but gets errors anyway - tg.create_task(bar(f)) # type: ignore[attr-defined] # error: 31, lineno-5, lineno-6, "f", "create_task" diff --git a/tests/eval_files/async112_anyio.py b/tests/eval_files/async112_anyio.py deleted file mode 100644 index 5b74f9d..0000000 --- a/tests/eval_files/async112_anyio.py +++ /dev/null @@ -1,27 +0,0 @@ -# main tests in async112.py -# this only tests anyio.create_task_group in particular -# BASE_LIBRARY anyio -# ASYNCIO_NO_ERROR - -import anyio - - -async def bar(*args): ... - - -async def foo(): - async with anyio.create_task_group() as tg: # error: 15, "tg", "task group" - 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", "task group" - tg.start_soon(bar()) - - async with anyio.create_task_group() as tg: - tg.start_soon(bar(tg)) - - # will not trigger on create_task - async with anyio.create_task_group() as tg: - tg.create_task(bar()) # type: ignore[attr-defined] diff --git a/tests/eval_files/async113.py b/tests/eval_files/async113.py index 22fedc2..bd45446 100644 --- a/tests/eval_files/async113.py +++ b/tests/eval_files/async113.py @@ -7,33 +7,50 @@ import trio # set base library to anyio, so we can replace anyio->asyncio and get correct errors -# BASE_LIBRARY anyio +# BASE_LIBRARY trio +# ASYNCIO_NO_ERROR -# NOTRIO - replacing anyio->trio would give mismatching errors. -# This file tests basic trio errors, and async113_trio checks trio-specific errors +# This file tests basic errors, and async113_trio_anyio checks errors not compatible +# with asyncio + + +async def my_startable(task_status: trio.TaskStatus[object] = trio.TASK_STATUS_IGNORED): + task_status.started() + await trio.lowlevel.checkpoint() @asynccontextmanager async def foo(): - with trio.open_nursery() as bar: - bar.start_soon(trio.run_process) # ASYNC113: 8 + # we don't check for `async with` + with trio.open_nursery() as bar: # type: ignore[attr-defined] + bar.start_soon(my_startable) # ASYNC113: 8 async with trio.open_nursery() as bar: - bar.start_soon(trio.run_process) # ASYNC113: 8 + bar.start_soon(my_startable) # ASYNC113: 8 boo: trio.Nursery = ... # type: ignore - boo.start_soon(trio.run_process) # ASYNC113: 4 + boo.start_soon(my_startable) # 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 + # silence type errors + serve = run_process = serve_listeners = serve_tcp = serve_ssl_over_tcp = ( + my_startable + ) + + # we also trigger if they're a standalone name, assuming that this is + # a wrapper. (or they've ignored the error from doing `from trio import run_process`) + boo.start_soon(serve) # error: 4 + boo.start_soon(run_process) # error: 4 + boo.start_soon(serve_listeners) # error: 4 + boo.start_soon(serve_tcp) # error: 4 + boo.start_soon(serve_ssl_over_tcp) # error: 4 yield -async def my_startable(task_status: trio.TaskStatus[object] = trio.TASK_STATUS_IGNORED): - task_status.started() - await trio.lowlevel.checkpoint() +# we don't type-track [trio/anyio].DTLSEndpoint, so we trigger +# on *.serve +@asynccontextmanager +async def foo_serve(nursey: trio.Nursery, thing: object): + nursey.start_soon(thing.serve) # ASYNC113: 4 # name of variable being [xxx.]nursery triggers it @@ -112,6 +129,6 @@ async def foo_nested_sync_def(): with trio.open_nursery() as bar: def non_async_func(): - bar.start_soon(trio.run_process) + bar.start_soon(my_startable) yield diff --git a/tests/eval_files/async113_anyio.py b/tests/eval_files/async113_anyio.py deleted file mode 100644 index acbe7dd..0000000 --- a/tests/eval_files/async113_anyio.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 diff --git a/tests/eval_files/async113_trio.py b/tests/eval_files/async113_trio.py index 983a0e0..c538b43 100644 --- a/tests/eval_files/async113_trio.py +++ b/tests/eval_files/async113_trio.py @@ -13,12 +13,8 @@ # ARG --startable-in-context-manager=custom_startable_function # ASYNCIO_NO_ERROR - asyncio uses TaskGroups. Checked in async113.py - - -@asynccontextmanager -async def custom_startable_externally_tested(): - nursery.start_soon(custom_startable_function) # error: 4 - yield +# We previously errored on anyio.run_process etc +# ANYIO_NO_ERROR @asynccontextmanager @@ -90,8 +86,9 @@ async def __aenter__(self): serve = run_process = myfun = anything # error if name shared with trio - nursery.start_soon(serve) # error: 8 - nursery.start_soon(run_process) # error: 8 + # these error on both trio & anyio, so moved to async113.py + # nursery.start_soon(serve) # error: 8 + # nursery.start_soon(run_process) # error: 8 # don't error if a startable name is a parameter or module nursery.start_soon(myfun(serve=None)) nursery.start_soon(serve.myfun) diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py index 6a00fe0..728968d 100644 --- a/tests/test_flake8_async.py +++ b/tests/test_flake8_async.py @@ -110,9 +110,11 @@ def replace_str(string: str, original: str, new: str) -> str: string = replace_str(string, "trio.open_nursery", "anyio.create_task_group") string = replace_str(string, '"nursery"', '"task group"') string = replace_str(string, "nursery", "task_group") + string = replace_str(string, "Nursery", "TaskGroup") elif original == "anyio" and new == "trio": string = replace_str(string, "anyio.create_task_group", "trio.open_nursery") string = replace_str(string, "task group", "nursery") + string = replace_str(string, "TaskGroup", "Nursery") return replace_str(string, original, new) From d3d18bb312e11450903fbecb3cf326b50f827cf5 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Mon, 3 Feb 2025 16:05:39 +0100 Subject: [PATCH 05/10] clean up implementation --- tests/test_flake8_async.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py index 728968d..5be4865 100644 --- a/tests/test_flake8_async.py +++ b/tests/test_flake8_async.py @@ -106,15 +106,24 @@ def replace_library(string: str, original: str = "trio", new: str = "anyio") -> def replace_str(string: str, original: str, new: str) -> str: return re.sub(rf"(? Date: Mon, 3 Feb 2025 17:18:30 +0100 Subject: [PATCH 06/10] lol that was dumb --- tests/test_flake8_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py index 5be4865..02863b6 100644 --- a/tests/test_flake8_async.py +++ b/tests/test_flake8_async.py @@ -115,7 +115,7 @@ def replace_str(string: str, original: str, new: str) -> str: ("Nursery", "TaskGroup"), ) - if sorted((original, new)) == ["trio", "anyio"]: + if sorted((original, new)) == ["anyio", "trio"]: for trio_, anyio_ in replacements: if original == "trio": from_ = trio_ From 491e21bb359acee50be0a4b96ba82edf88c4d837 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 4 Feb 2025 11:34:04 +0100 Subject: [PATCH 07/10] merge origin/main --- .pre-commit-config.yaml | 8 ++++---- flake8_async/visitors/visitor2xx.py | 6 ++++-- flake8_async/visitors/visitor91x.py | 2 +- flake8_async/visitors/visitors.py | 12 ++++++++---- tests/eval_files/async111.py | 1 - 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f71152..4aef83a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,13 +9,13 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.3 + rev: v0.9.4 hooks: - id: ruff args: [--fix] - repo: https://github.com/psf/black - rev: 24.10.0 + rev: 25.1.0 hooks: - id: black args: [--preview] @@ -45,7 +45,7 @@ repos: exclude: tests/eval_files/.*_py311.py - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.392.post0 + rev: v1.1.393 hooks: - id: pyright # ignore warnings about new version being available, no other warnings @@ -66,7 +66,7 @@ repos: - trio - repo: https://github.com/codespell-project/codespell - rev: v2.4.0 + rev: v2.4.1 hooks: - id: codespell additional_dependencies: diff --git a/flake8_async/visitors/visitor2xx.py b/flake8_async/visitors/visitor2xx.py index b49fe72..cd416d1 100644 --- a/flake8_async/visitors/visitor2xx.py +++ b/flake8_async/visitors/visitor2xx.py @@ -186,7 +186,9 @@ class Visitor22X(Visitor200): "Sync call {} in async function, use " "`asyncio.create_subprocess_[exec/shell]." ), - "ASYNC222": "Sync call {} in async function, wrap in `{}.to_thread.run_sync()`.", + "ASYNC222": ( + "Sync call {} in async function, wrap in `{}.to_thread.run_sync()`." + ), "ASYNC222_asyncio": ( "Sync call {} in async function, use `asyncio.loop.run_in_executor`." ), @@ -397,7 +399,7 @@ def visit_Call(self, node: ast.Call): @error_class class Visitor25X(Visitor200): error_codes: Mapping[str, str] = { - "ASYNC250": ("Blocking sync call `input()` in async function. Wrap in `{}`."), + "ASYNC250": "Blocking sync call `input()` in async function. Wrap in `{}`.", "ASYNC251": ( "Blocking sync call `time.sleep(...)` in async function." " Use `await {}.sleep(...)`." diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py index b664e64..823e8f6 100644 --- a/flake8_async/visitors/visitor91x.py +++ b/flake8_async/visitors/visitor91x.py @@ -357,7 +357,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors): "CancelScope with no guaranteed cancel point. This makes it potentially " "impossible to cancel." ), - "ASYNC913": ("Indefinite loop with no guaranteed cancel points."), + "ASYNC913": "Indefinite loop with no guaranteed cancel points.", "ASYNC100": ( "{0}.{1} context contains no checkpoints, remove the context or add" " `await {0}.lowlevel.checkpoint()`." diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 02c4948..92ed2b1 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -332,8 +332,10 @@ def visit_Call(self, node: ast.Call): @error_class class Visitor119(Flake8AsyncVisitor): error_codes: Mapping[str, str] = { - "ASYNC119": "Yield in contextmanager in async generator might not trigger" - " cleanup. Use `@asynccontextmanager` or refactor." + "ASYNC119": ( + "Yield in contextmanager in async generator might not trigger" + " cleanup. Use `@asynccontextmanager` or refactor." + ) } def __init__(self, *args: Any, **kwargs: Any): @@ -503,8 +505,10 @@ def leave_IfExp_test(self, node: cst.IfExp): @disabled_by_default class Visitor900(Flake8AsyncVisitor): error_codes: Mapping[str, str] = { - "ASYNC900": "Async generator not allowed, unless transformed " - "by a known decorator (one of: {})." + "ASYNC900": ( + "Async generator not allowed, unless transformed " + "by a known decorator (one of: {})." + ) } def __init__(self, *args: Any, **kwargs: Any): diff --git a/tests/eval_files/async111.py b/tests/eval_files/async111.py index a0ebc08..e40d292 100644 --- a/tests/eval_files/async111.py +++ b/tests/eval_files/async111.py @@ -8,7 +8,6 @@ import trio import trio as noterror - # shed/black breaks up a *ton* of lines since adding more detailed error messages, so # disable formatting to avoid having to adjust a ton of line references # fmt: off From 0c697809aaa25857e9d5eb1336a3016d44ebd9ae Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 4 Feb 2025 11:34:50 +0100 Subject: [PATCH 08/10] get rid of two NOAUTOFIX markers --- tests/autofix_files/async124.py | 159 ++++++++++++++++++++++++ tests/autofix_files/async124.py.diff | 86 +++++++++++++ tests/eval_files/async124.py | 9 +- tests/eval_files/async124_no_autofix.py | 9 ++ tests/eval_files/async251.py | 2 - 5 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 tests/autofix_files/async124.py create mode 100644 tests/autofix_files/async124.py.diff create mode 100644 tests/eval_files/async124_no_autofix.py diff --git a/tests/autofix_files/async124.py b/tests/autofix_files/async124.py new file mode 100644 index 0000000..e514bba --- /dev/null +++ b/tests/autofix_files/async124.py @@ -0,0 +1,159 @@ +"""Async function with no awaits could be sync. +It currently does not care if 910/911 would also be triggered.""" + +# ARG --enable=ASYNC124,ASYNC910,ASYNC911 +# ARG --no-checkpoint-warning-decorator=custom_disabled_decorator + +# 910/911 will also autofix async124, in the sense of adding a checkpoint. This is perhaps +# not what the user wants though, so this would be a case in favor of making 910/911 not +# trigger when async124 does. +# AUTOFIX # all errors get "fixed" except for foo_fix_no_subfix in async124_no_autofix.py +# ASYNCIO_NO_AUTOFIX +from typing import Any, overload +from pytest import fixture +import trio + +custom_disabled_decorator: Any = ... + + +def condition() -> bool: + return False + + +async def foo() -> Any: + await foo() + + +async def foo_print(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + print("hello") + await trio.lowlevel.checkpoint() + + +async def conditional_wait(): # ASYNC910: 0, "exit", Statement("function definition", lineno) + if condition(): + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_gen(): # ASYNC124: 0 # ASYNC911: 0, "exit", Statement("yield", lineno+1) + await trio.lowlevel.checkpoint() + yield # ASYNC911: 4, "yield", Statement("function definition", lineno-1) + await trio.lowlevel.checkpoint() + + +async def foo_async_with(): + async with foo_gen(): + ... + + +async def foo_async_for(): + async for i in foo_gen(): + ... + + +async def foo_nested(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + async def foo_nested_2(): + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_nested_sync(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + def foo_nested_sync_child(): + await foo() # type: ignore[await-not-async] + await trio.lowlevel.checkpoint() + + +# We don't want to trigger on empty/pass functions because of inheritance. +# Uses same logic as async91x. + + +async def foo_empty(): + "blah" + ... + + +async def foo_empty_pass(): + "foo" + pass + + +# this was previously silenced, but pytest now gives good errors on sync test + async +# fixture; so in the rare case that it has to be async the user will be able to debug it +async def test_async_fixture( # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + my_async_fixture, +): + assert my_async_fixture.setup_worked_correctly + await trio.lowlevel.checkpoint() + + +# no params -> no async fixtures +async def test_no_fixture(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + print("blah") + await trio.lowlevel.checkpoint() + + +# skip @overload. They should always be empty, but /shrug +@overload +async def foo_overload(): + print("blah") + + +async def foo_overload(): ... + + +# skip @[pytest.]fixture if they have any params, since they might depend on other +# async fixtures +@fixture +async def foo_fix(my_async_fixture): + print("blah") + + +# @fixture with no params can be converted to sync +# see async124_no_autofix.py + + +async def default_value(): + def foo(arg=await foo()): ... + + +# only the expression in genexp's get checked +async def foo_async_gen(): # ASYNC124: 0 + await trio.lowlevel.checkpoint() + return ( # ASYNC910: 4, "return", Statement("function definition", lineno-1) + await a async for a in foo_gen() + ) + + +async def foo_async_gen_await(): + return (a for a in await foo_gen()) + + +async def foo_async_for_comprehension(): + return [a async for a in foo_gen()] + + +class Foo: + # async124 ignores class methods + async def bar( # ASYNC910: 4, "exit", Statement("function definition", lineno) + self, + ): + async def bee(): # ASYNC124: 8 # ASYNC910: 8, "exit", Statement("function definition", lineno) + print("blah") + await trio.lowlevel.checkpoint() + await trio.lowlevel.checkpoint() + + async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition", lineno) + self, + ): + print() + await trio.lowlevel.checkpoint() + + +async def after_class(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + print() + await trio.lowlevel.checkpoint() + + +@custom_disabled_decorator +async def foo_has_custom_disabled_decorator(): + print() diff --git a/tests/autofix_files/async124.py.diff b/tests/autofix_files/async124.py.diff new file mode 100644 index 0000000..3b6f5b0 --- /dev/null +++ b/tests/autofix_files/async124.py.diff @@ -0,0 +1,86 @@ +--- ++++ +@@ x,6 x,7 @@ + # ASYNCIO_NO_AUTOFIX + from typing import Any, overload + from pytest import fixture ++import trio + + custom_disabled_decorator: Any = ... + +@@ x,15 x,19 @@ + + async def foo_print(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + print("hello") ++ await trio.lowlevel.checkpoint() + + + async def conditional_wait(): # ASYNC910: 0, "exit", Statement("function definition", lineno) + if condition(): + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_gen(): # ASYNC124: 0 # ASYNC911: 0, "exit", Statement("yield", lineno+1) ++ await trio.lowlevel.checkpoint() + yield # ASYNC911: 4, "yield", Statement("function definition", lineno-1) ++ await trio.lowlevel.checkpoint() + + + async def foo_async_with(): +@@ x,11 x,13 @@ + async def foo_nested(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + async def foo_nested_2(): + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_nested_sync(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + def foo_nested_sync_child(): + await foo() # type: ignore[await-not-async] ++ await trio.lowlevel.checkpoint() + + + # We don't want to trigger on empty/pass functions because of inheritance. +@@ x,11 x,13 @@ + my_async_fixture, + ): + assert my_async_fixture.setup_worked_correctly ++ await trio.lowlevel.checkpoint() + + + # no params -> no async fixtures + async def test_no_fixture(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + print("blah") ++ await trio.lowlevel.checkpoint() + + + # skip @overload. They should always be empty, but /shrug +@@ x,6 x,7 @@ + + # only the expression in genexp's get checked + async def foo_async_gen(): # ASYNC124: 0 ++ await trio.lowlevel.checkpoint() + return ( # ASYNC910: 4, "return", Statement("function definition", lineno-1) + await a async for a in foo_gen() + ) +@@ x,15 x,19 @@ + ): + async def bee(): # ASYNC124: 8 # ASYNC910: 8, "exit", Statement("function definition", lineno) + print("blah") ++ await trio.lowlevel.checkpoint() ++ await trio.lowlevel.checkpoint() + + async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition", lineno) + self, + ): + print() ++ await trio.lowlevel.checkpoint() + + + async def after_class(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + print() ++ await trio.lowlevel.checkpoint() + + + @custom_disabled_decorator diff --git a/tests/eval_files/async124.py b/tests/eval_files/async124.py index 11db9ba..058f0f3 100644 --- a/tests/eval_files/async124.py +++ b/tests/eval_files/async124.py @@ -7,7 +7,8 @@ # 910/911 will also autofix async124, in the sense of adding a checkpoint. This is perhaps # not what the user wants though, so this would be a case in favor of making 910/911 not # trigger when async124 does. -# NOAUTOFIX # all errors get "fixed" except for foo_fix_no_subfix +# AUTOFIX # all errors get "fixed" except for foo_fix_no_subfix in async124_no_autofix.py +# ASYNCIO_NO_AUTOFIX from typing import Any, overload from pytest import fixture @@ -98,10 +99,8 @@ async def foo_fix(my_async_fixture): print("blah") -# but @fixture with no params can be converted to sync -@fixture -async def foo_fix_no_subfix(): # ASYNC124: 0 - print("blah") +# @fixture with no params can be converted to sync +# see async124_no_autofix.py async def default_value(): diff --git a/tests/eval_files/async124_no_autofix.py b/tests/eval_files/async124_no_autofix.py new file mode 100644 index 0000000..8bc95d0 --- /dev/null +++ b/tests/eval_files/async124_no_autofix.py @@ -0,0 +1,9 @@ +# ARG --enable=ASYNC124,ASYNC910,ASYNC911 +from pytest import fixture + + +# @fixture with no params can be converted to sync +# 910/911 skips funcs with `@fixture` decorator, so this doesn't get auto"fixed" +@fixture +async def foo_fix_no_subfix(): # ASYNC124: 0 + print("blah") diff --git a/tests/eval_files/async251.py b/tests/eval_files/async251.py index d6c72a2..9da4a10 100644 --- a/tests/eval_files/async251.py +++ b/tests/eval_files/async251.py @@ -1,5 +1,3 @@ -# NOAUTOFIX - import time from time import sleep From c66440e0378484e74c12dbe2776aba90dcde5615 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 4 Feb 2025 11:40:44 +0100 Subject: [PATCH 09/10] changelog & version --- docs/changelog.rst | 4 ++++ docs/usage.rst | 2 +- flake8_async/__init__.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9bd7e36..80727b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ Changelog `CalVer, YY.month.patch `_ +25.2.2 +======= +- :ref:`ASYNC133 ` 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). + 25.2.1 ======= - :ref:`ASYNC912 ` and :ref:`ASYNC913 ` will now trigger if there's no *cancel* points. This means that :func:`trio.open_nursery`/`anyio.create_task_group` will not silence them on their own, unless they're guaranteed to start tasks. diff --git a/docs/usage.rst b/docs/usage.rst index e6e116f..df6440f 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -33,7 +33,7 @@ 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.1 + rev: 25.2.2 hooks: - id: flake8-async # args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC] diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py index aa715cb..8dcb1be 100644 --- a/flake8_async/__init__.py +++ b/flake8_async/__init__.py @@ -38,7 +38,7 @@ # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1" -__version__ = "25.2.1" +__version__ = "25.2.2" # taken from https://github.com/Zac-HD/shed From 4e0964c77375f131b1612a02517b21d76fe14ea0 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 4 Feb 2025 11:58:06 +0100 Subject: [PATCH 10/10] very nice test to catch this typo :) --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 80727b0..c8f5083 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,7 @@ Changelog 25.2.2 ======= -- :ref:`ASYNC133 ` 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). +- :ref:`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). 25.2.1 =======