Skip to content

Commit 1e20f99

Browse files
committed
add ASYNC250: blocking sync call input() in async function.
1 parent 8b1a1cf commit 1e20f99

File tree

7 files changed

+47
-2
lines changed

7 files changed

+47
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Changelog
22
*[CalVer, YY.month.patch](https://calver.org/)*
33

4+
## 24.3.2
5+
- Add ASYNC250: blocking sync call `input()` in async method.
6+
47
## 24.3.1
58
- Removed TRIO117, MultiError removed in trio 0.24.0
69
- Renamed the library from flake8-trio to flake8-async, to indicate the checker supports more than just `trio`.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Note: 22X, 23X and 24X has not had asyncio-specific suggestions written.
5555
- **ASYNC231**: Sync IO call in async function, use `[trio/anyio].wrap_file(...)`. `asyncio` users need to use a library such as [aiofiles](https://pypi.org/project/aiofiles/), or switch to [anyio](https://github.com/agronholm/anyio).
5656
- **ASYNC232**: Blocking sync call on file object, wrap the file object in `[trio/anyio].wrap_file()` to get an async file object.
5757
- **ASYNC240**: Avoid using `os.path` in async functions, prefer using `[trio/anyio].Path` objects. `asyncio` users should consider [aiopath](https://pypi.org/project/aiopath) or [anyio](https://github.com/agronholm/anyio).
58+
- **ASYNC250**: Builtin `input()` should not be called from async function.
5859

5960
### Warnings disabled by default
6061
- **ASYNC900**: Async generator without `@asynccontextmanager` not allowed. You might want to enable this on a codebase since async generators are inherently unsafe and cleanup logic might not be performed. See https://github.com/python-trio/flake8-async/issues/211 and https://discuss.python.org/t/using-exceptiongroup-at-anthropic-experience-report/20888/6 for discussion.

flake8_async/__init__.py

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

3838

3939
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
40-
__version__ = "24.3.1"
40+
__version__ = "24.3.2"
4141

4242

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

flake8_async/visitors/visitor2xx.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
220&221 looks for subprocess and os calls that should be wrapped.
77
230&231 looks for os.open and os.fdopen that should be wrapped.
88
240 looks for os.path functions that interact with the disk in various ways.
9+
250 looks for input() that should be wrapped
910
"""
1011

1112
from __future__ import annotations
@@ -53,7 +54,7 @@ def visit_blocking_call(self, node: ast.Call):
5354
self.error(node, key, blocking_calls[key])
5455

5556

56-
# used by Visitor212 and Visitor232
57+
# used by Visitor212 and Visitor232 - ??
5758

5859

5960
@error_class
@@ -384,3 +385,26 @@ def visit_Call(self, node: ast.Call):
384385
"func"
385386
) in self.os_funcs:
386387
self.error(node, m.group("func"), self.library_str, error_code=error_code)
388+
389+
390+
wrappers: Mapping[str, str] = {
391+
"trio": "trio.to_thread.run_sync",
392+
"anyio": "anyio.to_thread.run_sync",
393+
"asyncio": "asyncio.loop.run_in_executor",
394+
}
395+
396+
397+
@error_class
398+
class Visitor25X(Visitor200):
399+
error_codes: Mapping[str, str] = {
400+
"ASYNC250": ("Blocking sync call `input()` in async function. Wrap in `{}`."),
401+
}
402+
403+
def visit_Call(self, node: ast.Call):
404+
if not self.async_function:
405+
return
406+
if isinstance(node.func, ast.Name) and node.func.id == "input":
407+
if len(self.library) == 1:
408+
self.error(node, wrappers[self.library_str])
409+
else:
410+
self.error(node, "/".join(wrappers[lib] for lib in self.library))

tests/eval_files/async250.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# NOASYNCIO # formatting sufficiently different that test infra needs separate file
2+
async def foo():
3+
k = input() # ASYNC250: 8, "trio.to_thread.run_sync"
4+
input("hello world") # ASYNC250: 4, "trio.to_thread.run_sync"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# BASE_LIBRARY trio
2+
# NOASYNCIO # tests asyncio without replacing for it
3+
import trio
4+
import asyncio
5+
6+
7+
async def foo():
8+
k = input() # ASYNC250: 8, 'trio.to_thread.run_sync/asyncio.loop.run_in_executor'
9+
input("$") # ASYNC250: 4, 'trio.to_thread.run_sync/asyncio.loop.run_in_executor'

tests/test_changelog_and_version.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,16 @@ def __str__(self) -> str:
4242

4343
def get_releases() -> Iterable[Version]:
4444
valid_pattern = re.compile(r"^## (\d\d\.\d?\d\.\d?\d)$")
45+
invalid_pattern = re.compile(r"^## ")
4546
with open(CHANGELOG, encoding="utf-8") as f:
4647
lines = f.readlines()
4748
for line in lines:
4849
version_match = valid_pattern.match(line)
4950
if version_match:
5051
yield Version.from_string(version_match.group(1))
52+
else:
53+
# stop lines such as `## Future` making it through to main/
54+
assert not invalid_pattern.match(line)
5155

5256

5357
def test_last_release_against_changelog() -> None:

0 commit comments

Comments
 (0)