diff --git a/CHANGELOG.md b/CHANGELOG.md index ddce6a7..a925d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog *[CalVer, YY.month.patch](https://calver.org/)* +## 24.3.3 +- Add ASYNC251: `time.sleep()` in async method. + ## 24.3.2 - Add ASYNC250: blocking sync call `input()` in async method. diff --git a/README.md b/README.md index b9cbdb4..fdf01c7 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ 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. +- **ASYNC250**: Builtin `input()` should not be called from async function. Wrap in `[trio/anyio].to_thread.run_sync()` or `asyncio.loop.run_in_executor()`. +- **ASYNC251**: `time.sleep(...)` should not be called from async function. Use `[trio/anyio/asyncio].sleep(...)`. ### 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. diff --git a/flake8_async/visitors/__init__.py b/flake8_async/visitors/__init__.py index d4f17c4..bd858ec 100644 --- a/flake8_async/visitors/__init__.py +++ b/flake8_async/visitors/__init__.py @@ -25,7 +25,7 @@ utility_visitors: set[type[Flake8AsyncVisitor]] = set() utility_visitors_cst: set[type[Flake8AsyncVisitor_cst]] = set() -# Import all visitors so their decorators run, filling the above containers +# Import all files with visitors so their decorators run, filling the above containers # This has to be done at the end to avoid circular imports from . import ( visitor2xx, diff --git a/flake8_async/visitors/visitor2xx.py b/flake8_async/visitors/visitor2xx.py index 9005234..099a3ed 100644 --- a/flake8_async/visitors/visitor2xx.py +++ b/flake8_async/visitors/visitor2xx.py @@ -398,13 +398,25 @@ def visit_Call(self, node: ast.Call): class Visitor25X(Visitor200): error_codes: Mapping[str, str] = { "ASYNC250": ("Blocking sync call `input()` in async function. Wrap in `{}`."), + "ASYNC251": ( + "Blocking sync call `time.sleep(...)` in async function." + " Use `await {}.sleep(...)`." + ), } def visit_Call(self, node: ast.Call): if not self.async_function: return - if isinstance(node.func, ast.Name) and node.func.id == "input": + func_name = ast.unparse(node.func) + if func_name == "input": + error_code = "ASYNC250" if len(self.library) == 1: - self.error(node, wrappers[self.library_str]) + msg_param = wrappers[self.library_str] else: - self.error(node, "/".join(wrappers[lib] for lib in self.library)) + msg_param = "[" + "/".join(wrappers[lib] for lib in self.library) + "]" + elif func_name == "time.sleep": + error_code = "ASYNC251" + msg_param = self.library_str + else: + return + self.error(node, msg_param, error_code=error_code) diff --git a/tests/eval_files/async251.py b/tests/eval_files/async251.py new file mode 100644 index 0000000..9dd6836 --- /dev/null +++ b/tests/eval_files/async251.py @@ -0,0 +1,13 @@ +# NOAUTOFIX + +import time +from time import sleep + + +async def foo(): + time.sleep(5) if 5 else None # ASYNC251: 8, "trio" + time.sleep(5) # ASYNC251: 4, "trio" + + # Not handled due to difficulty tracking imports and not wanting to trigger + # false positives. But could definitely be handled by ruff et al. + sleep(5) diff --git a/tests/eval_files/async251_multi_library.py b/tests/eval_files/async251_multi_library.py new file mode 100644 index 0000000..f736c5d --- /dev/null +++ b/tests/eval_files/async251_multi_library.py @@ -0,0 +1,9 @@ +# BASE_LIBRARY trio +# NOASYNCIO # tests asyncio without replacing for it +import trio +import time +import asyncio + + +async def foo(): + time.sleep(5) # ASYNC251: 4, "[trio/asyncio]"