Skip to content

Commit

Permalink
Merge pull request #216 from jakkdl/input_in_async
Browse files Browse the repository at this point in the history
add ASYNC250: blocking sync call input() in async function.
  • Loading branch information
Zac-HD authored Mar 8, 2024
2 parents 8b1a1cf + 1e20f99 commit 7fb2572
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog
*[CalVer, YY.month.patch](https://calver.org/)*

## 24.3.2
- Add ASYNC250: blocking sync call `input()` in async method.

## 24.3.1
- Removed TRIO117, MultiError removed in trio 0.24.0
- Renamed the library from flake8-trio to flake8-async, to indicate the checker supports more than just `trio`.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Note: 22X, 23X and 24X has not had asyncio-specific suggestions written.
- **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).
- **ASYNC232**: Blocking sync call on file object, wrap the file object in `[trio/anyio].wrap_file()` to get an async file object.
- **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).
- **ASYNC250**: Builtin `input()` should not be called from async function.

### Warnings disabled by default
- **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.
Expand Down
2 changes: 1 addition & 1 deletion flake8_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@


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


# taken from https://github.com/Zac-HD/shed
Expand Down
26 changes: 25 additions & 1 deletion flake8_async/visitors/visitor2xx.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
220&221 looks for subprocess and os calls that should be wrapped.
230&231 looks for os.open and os.fdopen that should be wrapped.
240 looks for os.path functions that interact with the disk in various ways.
250 looks for input() that should be wrapped
"""

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


# used by Visitor212 and Visitor232
# used by Visitor212 and Visitor232 - ??


@error_class
Expand Down Expand Up @@ -384,3 +385,26 @@ def visit_Call(self, node: ast.Call):
"func"
) in self.os_funcs:
self.error(node, m.group("func"), self.library_str, error_code=error_code)


wrappers: Mapping[str, str] = {
"trio": "trio.to_thread.run_sync",
"anyio": "anyio.to_thread.run_sync",
"asyncio": "asyncio.loop.run_in_executor",
}


@error_class
class Visitor25X(Visitor200):
error_codes: Mapping[str, str] = {
"ASYNC250": ("Blocking sync call `input()` in async function. Wrap in `{}`."),
}

def visit_Call(self, node: ast.Call):
if not self.async_function:
return
if isinstance(node.func, ast.Name) and node.func.id == "input":
if len(self.library) == 1:
self.error(node, wrappers[self.library_str])
else:
self.error(node, "/".join(wrappers[lib] for lib in self.library))
4 changes: 4 additions & 0 deletions tests/eval_files/async250.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# NOASYNCIO # formatting sufficiently different that test infra needs separate file
async def foo():
k = input() # ASYNC250: 8, "trio.to_thread.run_sync"
input("hello world") # ASYNC250: 4, "trio.to_thread.run_sync"
9 changes: 9 additions & 0 deletions tests/eval_files/async250_multi_library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# BASE_LIBRARY trio
# NOASYNCIO # tests asyncio without replacing for it
import trio
import asyncio


async def foo():
k = input() # ASYNC250: 8, 'trio.to_thread.run_sync/asyncio.loop.run_in_executor'
input("$") # ASYNC250: 4, 'trio.to_thread.run_sync/asyncio.loop.run_in_executor'
4 changes: 4 additions & 0 deletions tests/test_changelog_and_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,16 @@ def __str__(self) -> str:

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


def test_last_release_against_changelog() -> None:
Expand Down

0 comments on commit 7fb2572

Please sign in to comment.