Skip to content

Commit aedd6e4

Browse files
committed
add ASYNC120 create-task-no-reference
1 parent 85fe612 commit aedd6e4

File tree

6 files changed

+146
-7
lines changed

6 files changed

+146
-7
lines changed

docs/changelog.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ Changelog
44

55
*[CalVer, YY.month.patch](https://calver.org/)*
66

7+
24.5.5
8+
======
9+
- Add :ref:`ASYNC120 <async120>` create-task-no-reference
10+
711
24.5.4
812
======
9-
- Add ASYNC913: Indefinite loop with no guaranteed checkpoint.
10-
- Fix bugs in ASYNC910 and ASYNC911 autofixing where they sometimes didn't add a library import.
11-
- Fix crash in ASYNC911 when trying to autofix a one-line ``while ...: yield``
13+
- Add :ref:`ASYNC913 <async913>`: Indefinite loop with no guaranteed checkpoint.
14+
- Fix bugs in :ref:`ASYNC910 <async910>` and :ref:`ASYNC911 <async911>` autofixing where they sometimes didn't add a library import.
15+
- Fix crash in :ref:`ASYNC911 <async911>` when trying to autofix a one-line ``while ...: yield``
1216
- Add :ref:`exception-suppress-context-managers`. Contextmanagers that may suppress exceptions.
13-
- ASYNC91x now treats checkpoints inside ``with contextlib.suppress`` as unreliable.
17+
- :ref:`ASYNC91x <ASYNC910>` now treats checkpoints inside ``with contextlib.suppress`` as unreliable.
1418

1519
24.5.3
1620
======

docs/rules.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ _`ASYNC119` : yield-in-cm-in-async-gen
7070
``yield`` in context manager in async generator is unsafe, the cleanup may be delayed until ``await`` is no longer allowed.
7171
We strongly encourage you to read `PEP 533 <https://peps.python.org/pep-0533/>`_ and use `async with aclosing(...) <https://docs.python.org/3/library/contextlib.html#contextlib.aclosing>`_, or better yet avoid async generators entirely (see `ASYNC900`_ ) in favor of context managers which return an iterable :ref:`channel/stream/queue <channel_stream_queue>`.
7272

73+
_`ASYNC120` : create-task-no-reference
74+
Calling :func:`asyncio.create_task` without saving the result. A task that isn't referenced elsewhere may get garbage collected at any time, even before it's done.
75+
Note that this rule won't check whether the variable the result is saved in is susceptible to being garbage-collected itself. See the asyncio documentation for best practices.
76+
You might consider instead using a :ref:`TaskGroup <taskgroup_nursery>` and calling :meth:`asyncio.TaskGroup.create_task` to avoid this problem, and gain the advantages of structured concurrency with e.g. better cancellation semantics.
7377

7478

7579
Blocking sync calls in async functions

flake8_async/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939

4040
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
41-
__version__ = "24.5.4"
41+
__version__ = "24.5.5"
4242

4343

4444
# taken from https://github.com/Zac-HD/shed

flake8_async/visitors/visitors.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,17 @@
55
import ast
66
from typing import TYPE_CHECKING, Any, cast
77

8-
from .flake8asyncvisitor import Flake8AsyncVisitor
9-
from .helpers import disabled_by_default, error_class, get_matching_call, has_decorator
8+
import libcst as cst
9+
10+
from .flake8asyncvisitor import Flake8AsyncVisitor, Flake8AsyncVisitor_cst
11+
from .helpers import (
12+
disabled_by_default,
13+
error_class,
14+
error_class_cst,
15+
get_matching_call,
16+
has_decorator,
17+
identifier_to_string,
18+
)
1019

1120
if TYPE_CHECKING:
1221
from collections.abc import Mapping
@@ -332,6 +341,53 @@ def visit_Yield(self, node: ast.Yield):
332341
visit_Lambda = visit_AsyncFunctionDef
333342

334343

344+
@error_class_cst
345+
class Visitor120(Flake8AsyncVisitor_cst):
346+
error_codes: Mapping[str, str] = {
347+
"ASYNC120": "asyncio.create_task() called without saving the result"
348+
}
349+
350+
def __init__(self, *args: Any, **kwargs: Any):
351+
super().__init__(*args, **kwargs)
352+
self.safe_to_create_task: bool = False
353+
354+
def visit_Assign(self, node: cst.CSTNode):
355+
self.save_state(node, "safe_to_create_task")
356+
self.safe_to_create_task = True
357+
358+
def visit_CompIf(self, node: cst.CSTNode):
359+
self.save_state(node, "safe_to_create_task")
360+
self.safe_to_create_task = False
361+
362+
def visit_Call(self, node: cst.Call):
363+
if (
364+
isinstance(node.func, (cst.Name, cst.Attribute))
365+
and identifier_to_string(node.func) == "asyncio.create_task"
366+
and not self.safe_to_create_task
367+
):
368+
self.error(node)
369+
self.visit_Assign(node)
370+
371+
visit_NamedExpr = visit_Assign
372+
visit_AugAssign = visit_Assign
373+
visit_IfExp_test = visit_CompIf
374+
375+
# because this is a Flake8AsyncVisitor_cst, we need to manually call restore_state
376+
def leave_Assign(
377+
self, original_node: cst.CSTNode, updated_node: cst.CSTNode
378+
) -> Any:
379+
self.restore_state(original_node)
380+
return updated_node
381+
382+
leave_Call = leave_Assign
383+
leave_CompIf = leave_Assign
384+
leave_NamedExpr = leave_Assign
385+
leave_AugAssign = leave_Assign
386+
387+
def leave_IfExp_test(self, node: cst.IfExp):
388+
self.restore_state(node)
389+
390+
335391
@error_class
336392
@disabled_by_default
337393
class Visitor900(Flake8AsyncVisitor):

tests/eval_files/async120.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# BASE_LIBRARY asyncio
2+
# TRIO_NO_ERROR
3+
# ANYIO_NO_ERROR
4+
5+
from typing import Any
6+
7+
import asyncio
8+
9+
10+
def handle_things(*args: object): ...
11+
12+
13+
class TaskStorer:
14+
def __init__(self):
15+
self.tasks: set[Any] = set()
16+
17+
def __ior__(self, obj: object):
18+
self.tasks.add(obj)
19+
20+
def __iadd__(self, obj: object):
21+
self.tasks.add(obj)
22+
23+
24+
async def foo():
25+
args: Any
26+
asyncio.create_task(*args) # ASYNC120: 4
27+
28+
k = asyncio.create_task(*args)
29+
30+
mylist = []
31+
mylist.append(asyncio.create_task(*args))
32+
33+
handle_things(asyncio.create_task(*args))
34+
35+
(l := asyncio.create_task(*args))
36+
37+
mylist = [asyncio.create_task(*args)]
38+
39+
task_storer = TaskStorer()
40+
task_storer |= asyncio.create_task(*args)
41+
task_storer += asyncio.create_task(*args)
42+
43+
mylist = [asyncio.create_task(*args) for i in range(10)]
44+
45+
# non-call usage is fine
46+
asyncio.create_task
47+
asyncio.create_task = args
48+
49+
# more or less esoteric ways of not saving the value
50+
51+
[asyncio.create_task(*args)] # ASYNC120: 5
52+
53+
(asyncio.create_task(*args) for i in range(10)) # ASYNC120: 5
54+
55+
args = 1 if asyncio.create_task(*args) else 2 # ASYNC120: 16
56+
57+
args = (i for i in range(10) if asyncio.create_task(*args)) # ASYNC120: 36
58+
59+
# not supported, it can't be used as a context manager
60+
with asyncio.create_task(*args) as k: # type: ignore[attr-defined] # ASYNC120: 9
61+
...
62+
63+
# import aliasing is not supported (this would raise ASYNC106 bad-async-library-import)
64+
from asyncio import create_task
65+
66+
create_task(*args)
67+
68+
# nor is assigning it
69+
boo = asyncio.create_task
70+
boo(*args)
71+
72+
# or any lambda thing
73+
my_lambda = lambda: asyncio.create_task(*args)
74+
my_lambda(*args)

tests/test_flake8_async.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ def _parse_eval_file(
476476
"ASYNC116",
477477
"ASYNC117",
478478
"ASYNC118",
479+
"ASYNC120",
479480
"ASYNC912",
480481
}
481482

0 commit comments

Comments
 (0)