diff --git a/README.md b/README.md index 9fc0a48..f6f1eaa 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ pip install flake8-async ``` ## List of warnings -- **ASYNC100**: A `with [trio|anyio].fail_after(...):` or `with [trio|anyio].move_on_after(...):` +- **ASYNC100**: A `with [trio/anyio].fail_after(...):` or `with [trio/anyio].move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. - **ASYNC101**: `yield` inside a trio/anyio nursery or cancel scope is only safe when implementing a context manager - otherwise, it breaks exception handling. @@ -33,13 +33,13 @@ pip install flake8-async - **ASYNC105**: Calling a trio async function without immediately `await`ing it. This is only supported with trio functions, but you can get similar functionality with a type-checker. - **ASYNC106**: `trio`/`anyio`/`asyncio` must be imported with `import trio`/`import anyio`/`import asyncio` for the linter to work. - **ASYNC109**: Async function definition with a `timeout` parameter - use `[trio/anyio].[fail/move_on]_[after/at]` instead. -- **ASYNC110**: `while : await [trio/anyio].sleep()` should be replaced by a `[trio|anyio].Event`. +- **ASYNC110**: `while : await [trio/anyio].sleep()` should be replaced by a `[trio/anyio].Event`. - **ASYNC111**: Variable, from context manager opened inside nursery, passed to `start[_soon]` might be invalidly accessed while in use, due to context manager closing before the nursery. This is usually a bug, and nurseries should generally be the inner-most context manager. - **ASYNC112**: Nursery body with only a call to `nursery.start[_soon]` and not passing itself as a parameter can be replaced with a regular function call. - **ASYNC113**: Using `nursery.start_soon` in `__aenter__` doesn't wait for the task to begin. Consider replacing with `nursery.start`. - **ASYNC114**: Startable function (i.e. has a `task_status` keyword parameter) not in `--startable-in-context-manager` parameter list, please add it so ASYNC113 can catch errors when using it. -- **ASYNC115**: Replace `[trio|anyio].sleep(0)` with the more suggestive `[trio|anyio].lowlevel.checkpoint()`. -- **ASYNC116**: `[trio|anyio].sleep()` with >24 hour interval should usually be `[trio|anyio].sleep_forever()`. +- **ASYNC115**: Replace `[trio/anyio].sleep(0)` with the more suggestive `[trio/anyio].lowlevel.checkpoint()`. +- **ASYNC116**: `[trio/anyio].sleep()` with >24 hour interval should usually be `[trio/anyio].sleep_forever()`. - **ASYNC118**: Don't assign the value of `anyio.get_cancelled_exc_class()` to a variable, since that breaks linter checks and multi-backend programs. ### Warnings for blocking sync calls in async functions @@ -48,13 +48,13 @@ Note: 22X, 23X and 24X has not had asyncio-specific suggestions written. - **ASYNC210**: Sync HTTP call in async function, use `httpx.AsyncClient`. This and the other ASYNC21x checks look for usage of `urllib3` and `httpx.Client`, and recommend using `httpx.AsyncClient` as that's the largest http client supporting anyio/trio. - **ASYNC211**: Likely sync HTTP call in async function, use `httpx.AsyncClient`. Looks for `urllib3` method calls on pool objects, but only matching on the method signature and not the object. - **ASYNC212**: Blocking sync HTTP call on httpx object, use httpx.AsyncClient. -- **ASYNC220**: Sync process call in async function, use `await nursery.start([trio|anyio].run_process, ...)`. -- **ASYNC221**: Sync process call in async function, use `await [trio|anyio].run_process(...)`. -- **ASYNC222**: Sync `os.*` call in async function, wrap in `await [trio|anyio].to_thread.run_sync()`. -- **ASYNC230**: Sync IO call in async function, use `[trio|anyio].open_file(...)`. -- **ASYNC231**: Sync IO call in async function, use `[trio|anyio].wrap_file(...)`. -- **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. +- **ASYNC220**: Sync process call in async function, use `await nursery.start([trio/anyio].run_process, ...)`. +- **ASYNC221**: Sync process call in async function, use `await [trio/anyio].run_process(...)`. +- **ASYNC222**: Sync `os.*` call in async function, wrap in `await [trio/anyio].to_thread.run_sync()`. +- **ASYNC230**: Sync IO call in async function, use `[trio/anyio].open_file(...)`. +- **ASYNC231**: Sync IO call in async function, use `[trio/anyio].wrap_file(...)`. +- **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. ### 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. @@ -134,7 +134,7 @@ Comma-separated list of error-codes to enable autofixing for if implemented. Req Whether to also print an error message for autofixed errors. ### `--anyio` -Change the default library to be anyio instead of trio. If trio is imported it will assume both are available and print suggestions with [anyio|trio]. +Change the default library to be anyio instead of trio. If trio is imported it will assume both are available and print suggestions with [anyio/trio]. ### `no-checkpoint-warning-decorators` Comma-separated list of decorators to disable checkpointing checks for, turning off ASYNC910 and ASYNC911 warnings for functions decorated with any decorator matching any in the list. Matching is done with [fnmatch](https://docs.python.org/3/library/fnmatch.html). Defaults to disabling for `asynccontextmanager`. diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py index 7d69d5b..15ad2c1 100644 --- a/flake8_async/__init__.py +++ b/flake8_async/__init__.py @@ -293,9 +293,9 @@ def add_options(option_manager: OptionManager | ArgumentParser): required=False, default=False, help=( - "Change the default library to be anyio instead of trio." - " If trio is imported it will assume both are available and print" - " suggestions with [anyio|trio]." + "Change the default library for suggestions to be anyio instead of trio." + " If asyncio/trio is imported it will assume that is also available and" + " print suggestions with [asyncio/anyio/trio]." ), ) add_argument( @@ -306,9 +306,10 @@ def add_options(option_manager: OptionManager | ArgumentParser): required=False, default=False, help=( - "Change the default library to be asyncio instead of trio." + "Change the default library for suggestions to be asyncio instead of" + " trio." " If anyio/trio is imported it will assume that is also available and" - " print suggestions with [asyncio|anyio/trio]." + " print suggestions with [asyncio/anyio/trio]." ), ) diff --git a/flake8_async/visitors/flake8asyncvisitor.py b/flake8_async/visitors/flake8asyncvisitor.py index d53249f..160bedf 100644 --- a/flake8_async/visitors/flake8asyncvisitor.py +++ b/flake8_async/visitors/flake8asyncvisitor.py @@ -148,7 +148,7 @@ def library(self) -> tuple[str, ...]: def library_str(self) -> str: if len(self.library) == 1: return self.library[0] - return "[" + "|".join(self.library) + "]" + return "[" + "/".join(self.library) + "]" def add_library(self, name: str) -> None: if name not in self.__state.library: diff --git a/flake8_async/visitors/visitor103_104.py b/flake8_async/visitors/visitor103_104.py index 7bd815e..0ee6e42 100644 --- a/flake8_async/visitors/visitor103_104.py +++ b/flake8_async/visitors/visitor103_104.py @@ -27,7 +27,7 @@ # TODO: ugly for a, b in (("anyio", "trio"), ("anyio", "asyncio"), ("asyncio", "trio")): _suggestion_dict[(a, b)] = ( - "[" + "|".join((_suggestion_dict[(a,)], _suggestion_dict[(b,)])) + "]" + "[" + "/".join((_suggestion_dict[(a,)], _suggestion_dict[(b,)])) + "]" ) _suggestion_dict[ ( @@ -110,7 +110,7 @@ def visit_ExceptHandler(self, node: ast.ExceptHandler): # Don't save the state of cancelled_caught, that's handled in Try and would # reset it between each except - # Don't need to reset the values of unraised_[break|continue] since that's handled + # Don't need to reset the values of unraised_[break/continue] since that's handled # by visit_For, but need to save the state of them to not mess up loops we're # nested inside self.save_state( diff --git a/tests/eval_files/anyio_trio.py b/tests/eval_files/anyio_trio.py index 4d5fff6..2e5dd2e 100644 --- a/tests/eval_files/anyio_trio.py +++ b/tests/eval_files/anyio_trio.py @@ -10,4 +10,4 @@ async def foo(): - subprocess.Popen() # ASYNC220: 4, 'subprocess.Popen', "[anyio|trio]" + subprocess.Popen() # ASYNC220: 4, 'subprocess.Popen', "[anyio/trio]" diff --git a/tests/eval_files/trio_anyio.py b/tests/eval_files/trio_anyio.py index 25439f2..37918f0 100644 --- a/tests/eval_files/trio_anyio.py +++ b/tests/eval_files/trio_anyio.py @@ -7,4 +7,4 @@ async def foo(): - subprocess.Popen() # ASYNC220: 4, 'subprocess.Popen', "[trio|anyio]" + subprocess.Popen() # ASYNC220: 4, 'subprocess.Popen', "[trio/anyio]" diff --git a/tests/test_config_and_args.py b/tests/test_config_and_args.py index 90764b8..8d57157 100644 --- a/tests/test_config_and_args.py +++ b/tests/test_config_and_args.py @@ -172,7 +172,7 @@ def test_anyio_from_config(tmp_path: Path, capsys: pytest.CaptureFixture[str]): err_msg = Visitor22X.error_codes["ASYNC220"].format( "subprocess.Popen", - "[anyio|trio]", + "[anyio/trio]", ) err_file = Path(__file__).parent / "eval_files" / "anyio_trio.py"