From 2742ad1d0a356837ce486373fb7b7586e92c9d8d Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 27 Aug 2024 17:36:12 +0200 Subject: [PATCH 1/6] add async121 control-flow-in-taskgroup --- docs/changelog.rst | 4 ++ docs/rules.rst | 3 ++ flake8_async/visitors/visitors.py | 52 +++++++++++++++++++++++ tests/eval_files/async121.py | 66 +++++++++++++++++++++++++++++ tests/eval_files/async121_anyio.py | 67 ++++++++++++++++++++++++++++++ tests/test_flake8_async.py | 3 ++ 6 files changed, 195 insertions(+) create mode 100644 tests/eval_files/async121.py create mode 100644 tests/eval_files/async121_anyio.py diff --git a/docs/changelog.rst b/docs/changelog.rst index daefaa6e..4338c748 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ Changelog *[CalVer, YY.month.patch](https://calver.org/)* +24.8.1 +====== +- Add :ref:`ASYNC121 ` control-flow-in-taskgroup + 24.8.1 ====== - Add config option ``transform-async-generator-decorators``, to list decorators which diff --git a/docs/rules.rst b/docs/rules.rst index 3eede308..757d7cce 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -83,6 +83,9 @@ _`ASYNC120` : await-in-except This will not trigger when :ref:`ASYNC102 ` does, and if you don't care about losing non-cancelled exceptions you could disable this rule. This is currently not able to detect asyncio shields. +_`ASYNC121`: control-flow-in-taskgroup + `return`, `continue`, and `break` inside a :ref:`taskgroup_nursery` can lead to counterintuitive behaviour. Refactor the code to instead cancel the :ref:`cancel_scope` and place the statement outside of the TaskGroup/Nursery block. See `trio#1493 `. + Blocking sync calls in async functions ====================================== diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index d6b1363b..f5ef4c1e 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -350,6 +350,58 @@ def visit_Yield(self, node: ast.Yield): visit_Lambda = visit_AsyncFunctionDef +@error_class +class Visitor121(Flake8AsyncVisitor): + error_codes: Mapping[str, str] = { + "ASYNC121": ( + "{0} in a {1} block behaves counterintuitively in several" + " situations. Refactor to have the {0} outside." + ) + } + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.unsafe_stack: list[str] = [] + + def visit_AsyncWith(self, node: ast.AsyncWith): + self.save_state(node, "unsafe_stack", copy=True) + + for item in node.items: + if get_matching_call(item.context_expr, "open_nursery", base="trio"): + self.unsafe_stack.append("nursery") + elif get_matching_call( + item.context_expr, "create_task_group", base="anyio" + ): + self.unsafe_stack.append("task group") + + def visit_While(self, node: ast.While | ast.For): + self.save_state(node, "unsafe_stack", copy=True) + self.unsafe_stack.append("loop") + + visit_For = visit_While + + def check_loop_flow(self, node: ast.Continue | ast.Break, statement: str) -> None: + # self.unsafe_stack should never be empty, but no reason not to avoid a crash + # for invalid code. + if self.unsafe_stack and self.unsafe_stack[-1] != "loop": + self.error(node, statement, self.unsafe_stack[-1]) + + def visit_Continue(self, node: ast.Continue) -> None: + self.check_loop_flow(node, "continue") + + def visit_Break(self, node: ast.Break) -> None: + self.check_loop_flow(node, "break") + + def visit_Return(self, node: ast.Return) -> None: + for unsafe_cm in "nursery", "task group": + if unsafe_cm in self.unsafe_stack: + self.error(node, "return", unsafe_cm) + + def visit_FunctionDef(self, node: ast.FunctionDef): + self.save_state(node, "unsafe_stack", copy=True) + self.unsafe_stack = [] + + @error_class_cst class Visitor300(Flake8AsyncVisitor_cst): error_codes: Mapping[str, str] = { diff --git a/tests/eval_files/async121.py b/tests/eval_files/async121.py new file mode 100644 index 00000000..5986c8db --- /dev/null +++ b/tests/eval_files/async121.py @@ -0,0 +1,66 @@ +# ASYNCIO_NO_ERROR # not a problem in asyncio +# ANYIO_NO_ERROR # checked in async121_anyio.py + +import trio + + +async def foo_return(): + async with trio.open_nursery(): + return # ASYNC121: 8, "return", "nursery" + + +async def foo_return_nested(): + async with trio.open_nursery(): + + def bar(): + return # safe + + +# continue +async def foo_while_continue_safe(): + async with trio.open_nursery(): + while True: + continue # safe + + +async def foo_while_continue_unsafe(): + while True: + async with trio.open_nursery(): + continue # ASYNC121: 12, "continue", "nursery" + + +async def foo_for_continue_safe(): + async with trio.open_nursery(): + for _ in range(5): + continue # safe + + +async def foo_for_continue_unsafe(): + for _ in range(5): + async with trio.open_nursery(): + continue # ASYNC121: 12, "continue", "nursery" + + +# break +async def foo_while_break_safe(): + async with trio.open_nursery(): + while True: + break # safe + + +async def foo_while_break_unsafe(): + while True: + async with trio.open_nursery(): + break # ASYNC121: 12, "break", "nursery" + + +async def foo_for_break_safe(): + async with trio.open_nursery(): + for _ in range(5): + break # safe + + +async def foo_for_break_unsafe(): + for _ in range(5): + async with trio.open_nursery(): + break # ASYNC121: 12, "break", "nursery" diff --git a/tests/eval_files/async121_anyio.py b/tests/eval_files/async121_anyio.py new file mode 100644 index 00000000..01b5244d --- /dev/null +++ b/tests/eval_files/async121_anyio.py @@ -0,0 +1,67 @@ +# ASYNCIO_NO_ERROR # not a problem in asyncio +# TRIO_NO_ERROR # checked in async121.py +# BASE_LIBRARY anyio + +import anyio + + +async def foo_return(): + async with anyio.create_task_group(): + return # ASYNC121: 8, "return", "task group" + + +async def foo_return_nested(): + async with anyio.create_task_group(): + + def bar(): + return # safe + + +# continue +async def foo_while_continue_safe(): + async with anyio.create_task_group(): + while True: + continue # safe + + +async def foo_while_continue_unsafe(): + while True: + async with anyio.create_task_group(): + continue # ASYNC121: 12, "continue", "task group" + + +async def foo_for_continue_safe(): + async with anyio.create_task_group(): + for _ in range(5): + continue # safe + + +async def foo_for_continue_unsafe(): + for _ in range(5): + async with anyio.create_task_group(): + continue # ASYNC121: 12, "continue", "task group" + + +# break +async def foo_while_break_safe(): + async with anyio.create_task_group(): + while True: + break # safe + + +async def foo_while_break_unsafe(): + while True: + async with anyio.create_task_group(): + break # ASYNC121: 12, "break", "task group" + + +async def foo_for_break_safe(): + async with anyio.create_task_group(): + for _ in range(5): + break # safe + + +async def foo_for_break_unsafe(): + for _ in range(5): + async with anyio.create_task_group(): + break # ASYNC121: 12, "break", "task group" diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py index 910df8cd..8d9d94ca 100644 --- a/tests/test_flake8_async.py +++ b/tests/test_flake8_async.py @@ -478,6 +478,9 @@ def _parse_eval_file( "ASYNC116", "ASYNC117", "ASYNC118", + # opening nurseries & taskgroups can only be done in async context, so ASYNC121 + # doesn't check for it + "ASYNC121", "ASYNC300", "ASYNC912", } From 459c7483a5a64bd6bd2f78b19d0cddefbf2d9e56 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 27 Aug 2024 17:47:43 +0200 Subject: [PATCH 2/6] bump __version__ --- flake8_async/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py index 9e229e6f..8b253e8e 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__ = "24.8.1" +__version__ = "24.8.2" # taken from https://github.com/Zac-HD/shed From ff22623356cde212a5332e3ebdd9df2fb7e338f9 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 27 Aug 2024 17:50:06 +0200 Subject: [PATCH 3/6] aaand fix the version # in the changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4338c748..6678e14c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog *[CalVer, YY.month.patch](https://calver.org/)* -24.8.1 +24.8.2 ====== - Add :ref:`ASYNC121 ` control-flow-in-taskgroup From 402993ea19f23d48d283285e46da1d4cb88f6e18 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 27 Aug 2024 18:08:18 +0200 Subject: [PATCH 4/6] reword the description after feedback from oremanj --- docs/rules.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules.rst b/docs/rules.rst index 757d7cce..9db42c1e 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -84,7 +84,7 @@ _`ASYNC120` : await-in-except This is currently not able to detect asyncio shields. _`ASYNC121`: control-flow-in-taskgroup - `return`, `continue`, and `break` inside a :ref:`taskgroup_nursery` can lead to counterintuitive behaviour. Refactor the code to instead cancel the :ref:`cancel_scope` and place the statement outside of the TaskGroup/Nursery block. See `trio#1493 `. + `return`, `continue`, and `break` inside a :ref:`taskgroup_nursery` can lead to counterintuitive behaviour. Refactor the code to instead cancel the :ref:`cancel_scope` inside the TaskGroup/Nursery and place the statement outside of the TaskGroup/Nursery block. See `Trio issue #1493 `. Blocking sync calls in async functions From 3435f63fbc201fbaa93730b53391a3eb442ab77d Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 29 Aug 2024 15:04:41 +0200 Subject: [PATCH 5/6] enable rule for asyncio, add more details to rule explanation. Extend tests to be more thorough with state management. --- docs/rules.rst | 2 +- flake8_async/visitors/visitors.py | 2 +- tests/eval_files/async121.py | 43 ++++++++++++++--- tests/eval_files/async121_anyio.py | 2 +- tests/eval_files/async121_asyncio.py | 69 ++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 tests/eval_files/async121_asyncio.py diff --git a/docs/rules.rst b/docs/rules.rst index 9db42c1e..64e6156e 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -84,7 +84,7 @@ _`ASYNC120` : await-in-except This is currently not able to detect asyncio shields. _`ASYNC121`: control-flow-in-taskgroup - `return`, `continue`, and `break` inside a :ref:`taskgroup_nursery` can lead to counterintuitive behaviour. Refactor the code to instead cancel the :ref:`cancel_scope` inside the TaskGroup/Nursery and place the statement outside of the TaskGroup/Nursery block. See `Trio issue #1493 `. + `return`, `continue`, and `break` inside a :ref:`taskgroup_nursery` can lead to counterintuitive behaviour. Refactor the code to instead cancel the :ref:`cancel_scope` inside the TaskGroup/Nursery and place the statement outside of the TaskGroup/Nursery block. In asyncio a user might expect the statement to have an immediate effect, but it will wait for all tasks to finish before having an effect. See `Trio issue #1493 ` for further issues specific to trio/anyio. Blocking sync calls in async functions diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index f5ef4c1e..a553a0a5 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -371,7 +371,7 @@ def visit_AsyncWith(self, node: ast.AsyncWith): self.unsafe_stack.append("nursery") elif get_matching_call( item.context_expr, "create_task_group", base="anyio" - ): + ) or get_matching_call(item.context_expr, "TaskGroup", base="asyncio"): self.unsafe_stack.append("task group") def visit_While(self, node: ast.While | ast.For): diff --git a/tests/eval_files/async121.py b/tests/eval_files/async121.py index 5986c8db..0beb8518 100644 --- a/tests/eval_files/async121.py +++ b/tests/eval_files/async121.py @@ -1,12 +1,21 @@ -# ASYNCIO_NO_ERROR # not a problem in asyncio +# ASYNCIO_NO_ERROR # checked in async121_asyncio.py # ANYIO_NO_ERROR # checked in async121_anyio.py import trio +def condition() -> bool: + return False + + async def foo_return(): async with trio.open_nursery(): - return # ASYNC121: 8, "return", "nursery" + if condition(): + return # ASYNC121: 12, "return", "nursery" + while condition(): + return # ASYNC121: 12, "return", "nursery" + + return # safe async def foo_return_nested(): @@ -26,7 +35,9 @@ async def foo_while_continue_safe(): async def foo_while_continue_unsafe(): while True: async with trio.open_nursery(): - continue # ASYNC121: 12, "continue", "nursery" + if condition(): + continue # ASYNC121: 16, "continue", "nursery" + continue # safe async def foo_for_continue_safe(): @@ -38,7 +49,9 @@ async def foo_for_continue_safe(): async def foo_for_continue_unsafe(): for _ in range(5): async with trio.open_nursery(): - continue # ASYNC121: 12, "continue", "nursery" + if condition(): + continue # ASYNC121: 16, "continue", "nursery" + continue # safe # break @@ -51,7 +64,9 @@ async def foo_while_break_safe(): async def foo_while_break_unsafe(): while True: async with trio.open_nursery(): - break # ASYNC121: 12, "break", "nursery" + if condition(): + break # ASYNC121: 16, "break", "nursery" + continue # safe async def foo_for_break_safe(): @@ -63,4 +78,20 @@ async def foo_for_break_safe(): async def foo_for_break_unsafe(): for _ in range(5): async with trio.open_nursery(): - break # ASYNC121: 12, "break", "nursery" + if condition(): + break # ASYNC121: 16, "break", "nursery" + continue # safe + + +# nested nursery +async def foo_nested_nursery(): + async with trio.open_nursery(): + if condition(): + return # ASYNC121: 12, "return", "nursery" + async with trio.open_nursery(): + if condition(): + return # ASYNC121: 16, "return", "nursery" + if condition(): + return # ASYNC121: 12, "return", "nursery" + if condition(): + return # safe diff --git a/tests/eval_files/async121_anyio.py b/tests/eval_files/async121_anyio.py index 01b5244d..2505e23c 100644 --- a/tests/eval_files/async121_anyio.py +++ b/tests/eval_files/async121_anyio.py @@ -1,4 +1,4 @@ -# ASYNCIO_NO_ERROR # not a problem in asyncio +# ASYNCIO_NO_ERROR # checked in async121_asyncio.py # TRIO_NO_ERROR # checked in async121.py # BASE_LIBRARY anyio diff --git a/tests/eval_files/async121_asyncio.py b/tests/eval_files/async121_asyncio.py new file mode 100644 index 00000000..80b6126d --- /dev/null +++ b/tests/eval_files/async121_asyncio.py @@ -0,0 +1,69 @@ +# ANYIO_NO_ERROR +# TRIO_NO_ERROR # checked in async121.py +# BASE_LIBRARY asyncio +# TaskGroup was added in 3.11, we run type checking with 3.9 +# mypy: disable-error-code=attr-defined + +import asyncio + + +async def foo_return(): + async with asyncio.TaskGroup(): + return # ASYNC121: 8, "return", "task group" + + +async def foo_return_nested(): + async with asyncio.TaskGroup(): + + def bar(): + return # safe + + +# continue +async def foo_while_continue_safe(): + async with asyncio.TaskGroup(): + while True: + continue # safe + + +async def foo_while_continue_unsafe(): + while True: + async with asyncio.TaskGroup(): + continue # ASYNC121: 12, "continue", "task group" + + +async def foo_for_continue_safe(): + async with asyncio.TaskGroup(): + for _ in range(5): + continue # safe + + +async def foo_for_continue_unsafe(): + for _ in range(5): + async with asyncio.TaskGroup(): + continue # ASYNC121: 12, "continue", "task group" + + +# break +async def foo_while_break_safe(): + async with asyncio.TaskGroup(): + while True: + break # safe + + +async def foo_while_break_unsafe(): + while True: + async with asyncio.TaskGroup(): + break # ASYNC121: 12, "break", "task group" + + +async def foo_for_break_safe(): + async with asyncio.TaskGroup(): + for _ in range(5): + break # safe + + +async def foo_for_break_unsafe(): + for _ in range(5): + async with asyncio.TaskGroup(): + break # ASYNC121: 12, "break", "task group" From ceadc3ddaf109f553df56c3e3498b2630bcb8c50 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 5 Sep 2024 10:40:56 +0200 Subject: [PATCH 6/6] also check AsyncFor, restructure tests --- docs/changelog.rst | 2 +- flake8_async/__init__.py | 2 +- flake8_async/visitors/visitors.py | 3 +- tests/eval_files/async121.py | 58 +++++++++++++----------- tests/eval_files/async121_anyio.py | 67 +++++----------------------- tests/eval_files/async121_asyncio.py | 67 +++++----------------------- 6 files changed, 59 insertions(+), 140 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6678e14c..cf750677 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog *[CalVer, YY.month.patch](https://calver.org/)* -24.8.2 +24.9.1 ====== - Add :ref:`ASYNC121 ` control-flow-in-taskgroup diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py index 8b253e8e..d4827f86 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__ = "24.8.2" +__version__ = "24.9.1" # taken from https://github.com/Zac-HD/shed diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index a553a0a5..6d9ca23f 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -374,11 +374,12 @@ def visit_AsyncWith(self, node: ast.AsyncWith): ) or get_matching_call(item.context_expr, "TaskGroup", base="asyncio"): self.unsafe_stack.append("task group") - def visit_While(self, node: ast.While | ast.For): + def visit_While(self, node: ast.While | ast.For | ast.AsyncFor): self.save_state(node, "unsafe_stack", copy=True) self.unsafe_stack.append("loop") visit_For = visit_While + visit_AsyncFor = visit_While def check_loop_flow(self, node: ast.Continue | ast.Break, statement: str) -> None: # self.unsafe_stack should never be empty, but no reason not to avoid a crash diff --git a/tests/eval_files/async121.py b/tests/eval_files/async121.py index 0beb8518..78a6b6c4 100644 --- a/tests/eval_files/async121.py +++ b/tests/eval_files/async121.py @@ -2,12 +2,18 @@ # ANYIO_NO_ERROR # checked in async121_anyio.py import trio +from typing import Any +# To avoid mypy unreachable-statement we wrap control flow calls in if statements +# they should have zero effect on the visitor logic. def condition() -> bool: return False +def bar() -> Any: ... + + async def foo_return(): async with trio.open_nursery(): if condition(): @@ -25,59 +31,61 @@ def bar(): return # safe -# continue -async def foo_while_continue_safe(): +async def foo_while_safe(): async with trio.open_nursery(): while True: + if condition(): + break # safe + if condition(): + continue # safe continue # safe -async def foo_while_continue_unsafe(): +async def foo_while_unsafe(): while True: async with trio.open_nursery(): if condition(): continue # ASYNC121: 16, "continue", "nursery" - continue # safe + if condition(): + break # ASYNC121: 16, "break", "nursery" + if condition(): + continue # safe + break # safe -async def foo_for_continue_safe(): +async def foo_for_safe(): async with trio.open_nursery(): for _ in range(5): - continue # safe + if condition(): + continue # safe + if condition(): + break # safe -async def foo_for_continue_unsafe(): +async def foo_for_unsafe(): for _ in range(5): async with trio.open_nursery(): if condition(): continue # ASYNC121: 16, "continue", "nursery" - continue # safe - - -# break -async def foo_while_break_safe(): - async with trio.open_nursery(): - while True: - break # safe - - -async def foo_while_break_unsafe(): - while True: - async with trio.open_nursery(): if condition(): break # ASYNC121: 16, "break", "nursery" continue # safe -async def foo_for_break_safe(): +async def foo_async_for_safe(): async with trio.open_nursery(): - for _ in range(5): - break # safe + async for _ in bar(): + if condition(): + continue # safe + if condition(): + break # safe -async def foo_for_break_unsafe(): - for _ in range(5): +async def foo_async_for_unsafe(): + async for _ in bar(): async with trio.open_nursery(): + if condition(): + continue # ASYNC121: 16, "continue", "nursery" if condition(): break # ASYNC121: 16, "break", "nursery" continue # safe diff --git a/tests/eval_files/async121_anyio.py b/tests/eval_files/async121_anyio.py index 2505e23c..12be0aed 100644 --- a/tests/eval_files/async121_anyio.py +++ b/tests/eval_files/async121_anyio.py @@ -5,63 +5,18 @@ import anyio -async def foo_return(): - async with anyio.create_task_group(): - return # ASYNC121: 8, "return", "task group" - - -async def foo_return_nested(): - async with anyio.create_task_group(): - - def bar(): - return # safe - - -# continue -async def foo_while_continue_safe(): - async with anyio.create_task_group(): - while True: - continue # safe - - -async def foo_while_continue_unsafe(): - while True: - async with anyio.create_task_group(): - continue # ASYNC121: 12, "continue", "task group" - - -async def foo_for_continue_safe(): - async with anyio.create_task_group(): - for _ in range(5): - continue # safe +# To avoid mypy unreachable-statement we wrap control flow calls in if statements +# they should have zero effect on the visitor logic. +def condition() -> bool: + return False -async def foo_for_continue_unsafe(): - for _ in range(5): - async with anyio.create_task_group(): - continue # ASYNC121: 12, "continue", "task group" - - -# break -async def foo_while_break_safe(): - async with anyio.create_task_group(): - while True: - break # safe - - -async def foo_while_break_unsafe(): +# only tests that asyncio.TaskGroup is detected, main tests in async121.py +async def foo_return(): while True: async with anyio.create_task_group(): - break # ASYNC121: 12, "break", "task group" - - -async def foo_for_break_safe(): - async with anyio.create_task_group(): - for _ in range(5): - break # safe - - -async def foo_for_break_unsafe(): - for _ in range(5): - async with anyio.create_task_group(): - break # ASYNC121: 12, "break", "task group" + if condition(): + continue # ASYNC121: 16, "continue", "task group" + if condition(): + break # ASYNC121: 16, "break", "task group" + return # ASYNC121: 12, "return", "task group" diff --git a/tests/eval_files/async121_asyncio.py b/tests/eval_files/async121_asyncio.py index 80b6126d..e8b8e275 100644 --- a/tests/eval_files/async121_asyncio.py +++ b/tests/eval_files/async121_asyncio.py @@ -7,63 +7,18 @@ import asyncio -async def foo_return(): - async with asyncio.TaskGroup(): - return # ASYNC121: 8, "return", "task group" - - -async def foo_return_nested(): - async with asyncio.TaskGroup(): - - def bar(): - return # safe - - -# continue -async def foo_while_continue_safe(): - async with asyncio.TaskGroup(): - while True: - continue # safe - - -async def foo_while_continue_unsafe(): - while True: - async with asyncio.TaskGroup(): - continue # ASYNC121: 12, "continue", "task group" - - -async def foo_for_continue_safe(): - async with asyncio.TaskGroup(): - for _ in range(5): - continue # safe +# To avoid mypy unreachable-statement we wrap control flow calls in if statements +# they should have zero effect on the visitor logic. +def condition() -> bool: + return False -async def foo_for_continue_unsafe(): - for _ in range(5): - async with asyncio.TaskGroup(): - continue # ASYNC121: 12, "continue", "task group" - - -# break -async def foo_while_break_safe(): - async with asyncio.TaskGroup(): - while True: - break # safe - - -async def foo_while_break_unsafe(): +# only tests that asyncio.TaskGroup is detected, main tests in async121.py +async def foo_return(): while True: async with asyncio.TaskGroup(): - break # ASYNC121: 12, "break", "task group" - - -async def foo_for_break_safe(): - async with asyncio.TaskGroup(): - for _ in range(5): - break # safe - - -async def foo_for_break_unsafe(): - for _ in range(5): - async with asyncio.TaskGroup(): - break # ASYNC121: 12, "break", "task group" + if condition(): + continue # ASYNC121: 16, "continue", "task group" + if condition(): + break # ASYNC121: 16, "break", "task group" + return # ASYNC121: 12, "return", "task group"