Skip to content

Commit 860b83c

Browse files
committed
add ASYNC125 constant-absolute-deadline
1 parent 7c9e3d6 commit 860b83c

File tree

9 files changed

+108
-11
lines changed

9 files changed

+108
-11
lines changed

docs/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changelog
44

55
`CalVer, YY.month.patch <https://calver.org/>`_
66

7+
25.4.2
8+
======
9+
- Add :ref:`ASYNC125 <async125>` constant-absolute-deadline
10+
711
25.4.1
812
======
913
- Add match-case (structural pattern matching) support to ASYNC103, 104, 910, 911 & 912.

docs/rules.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ _`ASYNC124`: async-function-could-be-sync
102102
This currently overlaps with :ref:`ASYNC910 <ASYNC910>` and :ref:`ASYNC911 <ASYNC911>` which, if enabled, will autofix the function to have :ref:`checkpoint`.
103103
This excludes class methods as they often have to be async for other reasons, if you really do want to check those you could manually run :ref:`ASYNC910 <ASYNC910>` and/or :ref:`ASYNC911 <ASYNC911>` and check the methods they trigger on.
104104

105+
_`ASYNC125`: constant-absolute-deadline
106+
Passing constant values (other than :const:`math.inf`) to timeouts expecting absolute
107+
deadlines is nonsensical. These should always be defined relative to
108+
:func:`trio.current_time`/:func:`anyio.current_time`, or you might want to use
109+
:func:`trio.fail_after`/`:func:`trio.move_on_after`/:func:`anyio.fail_after`/
110+
:func:`anyio.move_on_after`, or the ``relative_deadline`` parameter to
111+
:class:`trio.CancelScope`.
112+
105113
Blocking sync calls in async functions
106114
======================================
107115

docs/usage.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ adding the following to your ``.pre-commit-config.yaml``:
3333
minimum_pre_commit_version: '2.9.0'
3434
repos:
3535
- repo: https://github.com/python-trio/flake8-async
36-
rev: 25.4.1
36+
rev: 25.4.2
3737
hooks:
3838
- id: flake8-async
3939
# args: ["--enable=ASYNC100,ASYNC112", "--disable=", "--autofix=ASYNC"]

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__ = "25.4.1"
41+
__version__ = "25.4.2"
4242

4343

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

flake8_async/visitors/helpers.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from __future__ import annotations
77

88
import ast
9+
from dataclasses import dataclass
910
from fnmatch import fnmatch
1011
from typing import TYPE_CHECKING, NamedTuple, TypeVar, Union
1112

@@ -287,11 +288,20 @@ def has_exception(node: ast.expr) -> str | None:
287288
)
288289

289290

291+
@dataclass
292+
class MatchingCall:
293+
node: ast.Call
294+
name: str
295+
base: str
296+
297+
def __str__(self) -> str:
298+
return self.base + "." + self.name
299+
300+
290301
# convenience function used in a lot of visitors
291-
# should probably return a named tuple
292302
def get_matching_call(
293303
node: ast.AST, *names: str, base: Iterable[str] = ("trio", "anyio")
294-
) -> tuple[ast.Call, str, str] | None:
304+
) -> MatchingCall | None:
295305
if isinstance(base, str):
296306
base = (base,)
297307
if (
@@ -301,7 +311,7 @@ def get_matching_call(
301311
and node.func.value.id in base
302312
and node.func.attr in names
303313
):
304-
return node, node.func.attr, node.func.value.id
314+
return MatchingCall(node, node.func.attr, node.func.value.id)
305315
return None
306316

307317

flake8_async/visitors/visitor102_120.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class Visitor102(Flake8AsyncVisitor):
3434
}
3535

3636
class TrioScope:
37-
def __init__(self, node: ast.Call, funcname: str, _):
37+
def __init__(self, node: ast.Call, funcname: str):
3838
super().__init__()
3939
self.node = node
4040
self.funcname = funcname
@@ -126,7 +126,7 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
126126
if call is None:
127127
continue
128128

129-
trio_scope = self.TrioScope(*call)
129+
trio_scope = self.TrioScope(call.node, call.name)
130130
# check if it's saved in a variable
131131
if isinstance(item.optional_vars, ast.Name):
132132
trio_scope.variable_name = item.optional_vars.id

flake8_async/visitors/visitors.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,7 @@ def visit_Call(self, node: ast.Call):
287287
and isinstance(node.args[0], ast.Constant)
288288
and node.args[0].value == 0
289289
):
290-
# m[2] is set to node.func.value.id
291-
self.error(node, m[2])
290+
self.error(node, m.base)
292291

293292

294293
@error_class
@@ -326,7 +325,7 @@ def visit_Call(self, node: ast.Call):
326325
and arg.value > 86400
327326
)
328327
):
329-
self.error(node, m[2])
328+
self.error(node, m.base)
330329

331330

332331
@error_class
@@ -452,7 +451,49 @@ def visit_Call(self, node: ast.Call):
452451
node, "fail_after", "move_on_after", base=("trio", "anyio")
453452
)
454453
):
455-
self.error(node, f"{match[2]}.{match[1]}")
454+
self.error(node, str(match))
455+
456+
457+
@error_class
458+
class Visitor125(Flake8AsyncVisitor):
459+
error_codes: Mapping[str, str] = {
460+
"ASYNC125": (
461+
"Using {} with a constant value is nonsensical, as the value is relative "
462+
"to the runner clock. Use ``fail_after(...)``, ``move_on_after(...)``, "
463+
"``CancelScope(relative_deadline=...)`` or calculate it relative to "
464+
"``{}.current_time()``."
465+
)
466+
}
467+
468+
def visit_Call(self, node: ast.Call):
469+
def is_constant(value: ast.expr) -> bool:
470+
if isinstance(value, ast.Constant):
471+
return True
472+
if isinstance(value, ast.BinOp):
473+
return is_constant(value.left) and is_constant(value.right)
474+
return False
475+
476+
match = get_matching_call(
477+
node, "fail_at", "move_on_at", "CancelScope", base=("trio", "anyio")
478+
)
479+
if match is None:
480+
return
481+
482+
if match.name in ("fail_at", "move_on_at") and len(node.args) == 1:
483+
value = node.args[0]
484+
else:
485+
for kw in node.keywords:
486+
if kw.arg == "deadline":
487+
value = kw.value
488+
break
489+
else:
490+
return
491+
if is_constant(value):
492+
self.error(
493+
value,
494+
str(match),
495+
match.base,
496+
)
456497

457498

458499
@error_class_cst

tests/eval_files/async125.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import trio
2+
from typing import Final
3+
4+
# ASYNCIO_NO_ERROR
5+
# anyio.[fail/move_on]_at doesn't exist, but no harm in erroring if we encounter them
6+
7+
trio.fail_at(5) # ASYNC125: 13, "trio.fail_at", "trio"
8+
trio.fail_at(deadline=5) # ASYNC125: 22, "trio.fail_at", "trio"
9+
trio.move_on_at(10**3) # ASYNC125: 16, "trio.move_on_at", "trio"
10+
trio.fail_at(7 * 3 + 2 / 5 - (8**7)) # ASYNC125: 13, "trio.fail_at", "trio"
11+
12+
trio.CancelScope(deadline=7) # ASYNC125: 26, "trio.CancelScope", "trio"
13+
trio.CancelScope(shield=True, deadline=7) # ASYNC125: 39, "trio.CancelScope", "trio"
14+
15+
# we *could* tell them to use math.inf here ...
16+
trio.fail_at(10**1000) # ASYNC125: 13, "trio.fail_at", "trio"
17+
18+
# _after is fine
19+
trio.fail_after(5)
20+
trio.move_on_after(2.3)
21+
22+
trio.fail_at(trio.current_time())
23+
trio.fail_at(trio.current_time() + 7)
24+
25+
# relative_deadline is fine, though anyio doesn't have it
26+
trio.CancelScope(relative_deadline=7)
27+
28+
# does not trigger on other "constants".. but we could opt to trigger on
29+
# any all-caps variable, or on :Final
30+
MY_CONST_VALUE = 7
31+
trio.fail_at(MY_CONST_VALUE)
32+
my_final_value: Final = 3
33+
trio.fail_at(my_final_value)

tests/test_flake8_async.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ def _parse_eval_file(
510510
"ASYNC121",
511511
"ASYNC122",
512512
"ASYNC123",
513+
"ASYNC125",
513514
"ASYNC300",
514515
"ASYNC912",
515516
}

0 commit comments

Comments
 (0)