Skip to content

Commit

Permalink
add ASYNC120 create-task-no-reference
Browse files Browse the repository at this point in the history
  • Loading branch information
jakkdl committed May 27, 2024
1 parent 85fe612 commit aedd6e4
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 7 deletions.
12 changes: 8 additions & 4 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ Changelog

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

24.5.5
======
- Add :ref:`ASYNC120 <async120>` create-task-no-reference

24.5.4
======
- Add ASYNC913: Indefinite loop with no guaranteed checkpoint.
- Fix bugs in ASYNC910 and ASYNC911 autofixing where they sometimes didn't add a library import.
- Fix crash in ASYNC911 when trying to autofix a one-line ``while ...: yield``
- Add :ref:`ASYNC913 <async913>`: Indefinite loop with no guaranteed checkpoint.
- Fix bugs in :ref:`ASYNC910 <async910>` and :ref:`ASYNC911 <async911>` autofixing where they sometimes didn't add a library import.
- Fix crash in :ref:`ASYNC911 <async911>` when trying to autofix a one-line ``while ...: yield``
- Add :ref:`exception-suppress-context-managers`. Contextmanagers that may suppress exceptions.
- ASYNC91x now treats checkpoints inside ``with contextlib.suppress`` as unreliable.
- :ref:`ASYNC91x <ASYNC910>` now treats checkpoints inside ``with contextlib.suppress`` as unreliable.

24.5.3
======
Expand Down
4 changes: 4 additions & 0 deletions docs/rules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ _`ASYNC119` : yield-in-cm-in-async-gen
``yield`` in context manager in async generator is unsafe, the cleanup may be delayed until ``await`` is no longer allowed.
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>`.

_`ASYNC120` : create-task-no-reference
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.
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.
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.


Blocking sync calls in async functions
Expand Down
2 changes: 1 addition & 1 deletion flake8_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@


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


# taken from https://github.com/Zac-HD/shed
Expand Down
60 changes: 58 additions & 2 deletions flake8_async/visitors/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@
import ast
from typing import TYPE_CHECKING, Any, cast

from .flake8asyncvisitor import Flake8AsyncVisitor
from .helpers import disabled_by_default, error_class, get_matching_call, has_decorator
import libcst as cst

from .flake8asyncvisitor import Flake8AsyncVisitor, Flake8AsyncVisitor_cst
from .helpers import (
disabled_by_default,
error_class,
error_class_cst,
get_matching_call,
has_decorator,
identifier_to_string,
)

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


@error_class_cst
class Visitor120(Flake8AsyncVisitor_cst):
error_codes: Mapping[str, str] = {
"ASYNC120": "asyncio.create_task() called without saving the result"
}

def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.safe_to_create_task: bool = False

def visit_Assign(self, node: cst.CSTNode):
self.save_state(node, "safe_to_create_task")
self.safe_to_create_task = True

def visit_CompIf(self, node: cst.CSTNode):
self.save_state(node, "safe_to_create_task")
self.safe_to_create_task = False

def visit_Call(self, node: cst.Call):
if (
isinstance(node.func, (cst.Name, cst.Attribute))
and identifier_to_string(node.func) == "asyncio.create_task"
and not self.safe_to_create_task
):
self.error(node)
self.visit_Assign(node)

visit_NamedExpr = visit_Assign
visit_AugAssign = visit_Assign
visit_IfExp_test = visit_CompIf

# because this is a Flake8AsyncVisitor_cst, we need to manually call restore_state
def leave_Assign(
self, original_node: cst.CSTNode, updated_node: cst.CSTNode
) -> Any:
self.restore_state(original_node)
return updated_node

leave_Call = leave_Assign
leave_CompIf = leave_Assign
leave_NamedExpr = leave_Assign
leave_AugAssign = leave_Assign

def leave_IfExp_test(self, node: cst.IfExp):
self.restore_state(node)


@error_class
@disabled_by_default
class Visitor900(Flake8AsyncVisitor):
Expand Down
74 changes: 74 additions & 0 deletions tests/eval_files/async120.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# BASE_LIBRARY asyncio
# TRIO_NO_ERROR
# ANYIO_NO_ERROR

from typing import Any

import asyncio


def handle_things(*args: object): ...


class TaskStorer:
def __init__(self):
self.tasks: set[Any] = set()

def __ior__(self, obj: object):
self.tasks.add(obj)

def __iadd__(self, obj: object):
self.tasks.add(obj)


async def foo():
args: Any
asyncio.create_task(*args) # ASYNC120: 4

k = asyncio.create_task(*args)

mylist = []
mylist.append(asyncio.create_task(*args))

handle_things(asyncio.create_task(*args))

(l := asyncio.create_task(*args))

mylist = [asyncio.create_task(*args)]

task_storer = TaskStorer()
task_storer |= asyncio.create_task(*args)
task_storer += asyncio.create_task(*args)

mylist = [asyncio.create_task(*args) for i in range(10)]

# non-call usage is fine
asyncio.create_task
asyncio.create_task = args

# more or less esoteric ways of not saving the value

[asyncio.create_task(*args)] # ASYNC120: 5

(asyncio.create_task(*args) for i in range(10)) # ASYNC120: 5

args = 1 if asyncio.create_task(*args) else 2 # ASYNC120: 16

args = (i for i in range(10) if asyncio.create_task(*args)) # ASYNC120: 36

# not supported, it can't be used as a context manager
with asyncio.create_task(*args) as k: # type: ignore[attr-defined] # ASYNC120: 9
...

# import aliasing is not supported (this would raise ASYNC106 bad-async-library-import)
from asyncio import create_task

create_task(*args)

# nor is assigning it
boo = asyncio.create_task
boo(*args)

# or any lambda thing
my_lambda = lambda: asyncio.create_task(*args)
my_lambda(*args)
1 change: 1 addition & 0 deletions tests/test_flake8_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ def _parse_eval_file(
"ASYNC116",
"ASYNC117",
"ASYNC118",
"ASYNC120",
"ASYNC912",
}

Expand Down

0 comments on commit aedd6e4

Please sign in to comment.