From e788a35316a95ed922f0d096791abcd728f73779 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Tue, 14 May 2024 11:20:17 +0200 Subject: [PATCH] Transition from readme to actually using sphinx docs (#245) * remove most stuff from README.md, add links to docs in pypi and readme. Add badge for pypi and coverage to readme. * fix test_messages_documented to look in rules.rst instead of readme * add claude-translated contributing.rst * update contributing.rst, replace CONTRIBUTING.md with a link to contributing.rst. Add TOC link to contributing.rst * transition changelog from markdown to readthedocs * transition from CHANGELOG.md to docs/changelog.rst --------- Co-authored-by: Zac Hatfield-Dodds --- CHANGELOG.md | 183 +--------------------- CONTRIBUTING.md | 83 +--------- README.md | 184 +--------------------- docs/changelog.rst | 231 ++++++++++++++++++++++++++++ docs/contributing.rst | 136 ++++++++++++++++ docs/index.rst | 5 +- setup.py | 12 +- tests/test_changelog_and_version.py | 19 ++- tests/test_messages_documented.py | 6 +- 9 files changed, 397 insertions(+), 462 deletions(-) create mode 100644 docs/changelog.rst create mode 100644 docs/contributing.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 11fcae2..29f7c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,182 +1 @@ -# Changelog -*[CalVer, YY.month.patch](https://calver.org/)* - -## 24.5.2 -- ASYNC101 now also warns on anyio & asyncio taskgroup.s -- Fixed a bug where ASYNC101 and ASYNC91x would not recognize decorators with parameters directly imported. I.e. `@fixture(...)` will now suppress errors. - -## 24.5.1 -- Add ASYNC912: no checkpoints in with statement are guaranteed to run. -- ASYNC100 now properly treats async for comprehensions as checkpoints. -- ASYNC100 now supports autofixing on asyncio. - -## 24.4.2 -- Add ASYNC119: yield in contextmanager in async generator. - -## 24.4.1 -- ASYNC91X fix internal error caused by multiple `try/except` incorrectly sharing state. - -## 24.3.6 -- ASYNC100 no longer triggers if a context manager contains a `yield`. - -## 24.3.5 -- ASYNC102 (no await inside finally or critical except) no longer raises warnings for calls to `aclose()` on objects in trio/anyio code. See https://github.com/python-trio/flake8-async/issues/156 - -## 24.3.4 -- ASYNC110 (don't loop sleep) now also warns if looping `[trio/anyio].lowlevel.checkpoint()`. - -## 24.3.3 -- Add ASYNC251: `time.sleep()` in async method. - -## 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`. -- Renamed all error codes from TRIOxxx to ASYNCxxx -- Renamed the binary from flake8-trio to flake8-async -- Lots of internal renaming. -- Added asyncio support for several error codes -- added `--library` - -## 23.5.1 -- TRIO91X now supports comprehensions -- TRIO100 and TRIO91X now supports autofixing -- Renamed `--enable-visitor-codes-regex` to `--enable` -- Added `--disable`, `--autofix` and `--error-on-autofix` - -## 23.2.5 -- Fix false alarms for `@pytest.fixture`-decorated functions in TRIO101, TRIO910 and TRIO911 - -## 23.2.4 -- Fix TRIO900 false alarm on nested functions -- TRIO113 now also works on `anyio.TaskGroup` - -## 23.2.3 -- Fix get_matching_call when passed a single string as base. Resolves possibly several false alarms, TRIO210 among them. - -## 23.2.2 -- Rename TRIO107 to TRIO910, and TRIO108 to TRIO911, and making them optional by default. -- Allow `@pytest.fixture()`-decorated async generators, since they're morally context managers -- Add support for checking code written against [`anyio`](https://anyio.readthedocs.io/en/stable/) -- Add TRIO118: Don't assign the value of `anyio.get_cancelled_exc_class()` to a variable, since that breaks linter checks and multi-backend programs. - -## 23.2.1 -- TRIO103 and TRIO104 no longer triggers when `trio.Cancelled` has been handled in previous except handlers. -- Add TRIO117: Reference to deprecated `trio.[NonBase]MultiError`; use `[Base]ExceptionGroup` instead. -- Add TRIO232: blocking sync call on file object. -- Add TRIO212: blocking sync call on `httpx.Client` object. -- Add TRIO222: blocking sync call to `os.wait*` -- TRIO221 now also looks for `os.posix_spawn[p]` - -## 23.1.4 -- TRIO114 avoids a false alarm on posonly args named "task_status" -- TRIO116 will now match on any attribute parameter named `.inf`, not just `math.inf`. -- TRIO900 now only checks `@asynccontextmanager`, not other decorators passed with --no-checkpoint-warning-decorators. - -## 23.1.3 -- Add TRIO240: usage of `os.path` in async function. -- Add TRIO900: ban async generators not decorated with known safe decorator - -## 23.1.2 -- Add TRIO230, TRIO231 - sync IO calls in async function - -## 23.1.1 -- Add TRIO210, TRIO211 - blocking sync call in async function, using network packages (requests, httpx, urllib3) -- Add TRIO220, TRIO221 - blocking sync call in async function, using subprocess or os. - -## 22.12.5 -- The `--startable-in-context-manager` and `--trio200-blocking-calls` options now handle spaces and newlines. -- Now compatible with [flake8-noqa](https://pypi.org/project/flake8-noqa/)'s NQA102 and NQA103 checks. - -## 22.12.4 -- TRIO200 no longer warns on directly awaited calls - -## 22.12.3 -- Worked around configuration-parsing bug for TRIO200 warning (more to come) - -## 22.12.2 -- Add TRIO200: User-configured blocking sync call in async function - -## 22.12.1 -- TRIO114 will now trigger on the unqualified name, will now only check the first parameter - directly, and parameters to function calls inside that. -- TRIO113 now only supports names that are valid identifiers, rather than fnmatch patterns. -- Add TRIO115: Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`. - -## 22.11.5 -- Add TRIO116: `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`. - -## 22.11.4 -- Add TRIO114 Startable function not in `--startable-in-context-manager` parameter list. - -## 22.11.3 -- Add TRIO113, prefer `await nursery.start(...)` to `nursery.start_soon()` for compatible functions when opening a context manager - -## 22.11.2 -- TRIO105 now also checks that you `await`ed `nursery.start()`. - -## 22.11.1 -- TRIO102 is no longer skipped in (async) context managers, since it's not a missing-checkpoint warning. - -## 22.9.2 -- Fix a crash on nontrivial decorator expressions (calls, PEP-614) and document behavior. - -## 22.9.1 -- Add `--no-checkpoint-warning-decorators` option, to disable missing-checkpoint warnings for certain decorated functions. - -## 22.8.8 -- Fix false alarm on TRIO107 with checkpointing `try` and empty `finally` -- Fix false alarm on TRIO107&108 with infinite loops - -## 22.8.7 -- TRIO107+108 now ignores `asynccontextmanager`s, since both `__aenter__` and `__aexit__` should checkpoint. `async with` is also treated as checkpointing on both enter and exit. -- TRIO107 now completely ignores any function whose body consists solely of ellipsis, pass, or string constants. -- TRIO103, 107 and 108 now inspects `while` conditions and `for` iterables to avoid false alarms on a couple cases where the loop body is guaranteed to run at least once. - -## 22.8.6 -- TRIO103 now correctly handles raises in loops, i.e. `raise` in else is guaranteed to run unless there's a `break` in the body. - -## 22.8.5 -- Add TRIO111: 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. -- Add TRIO112: this single-task nursery could be replaced by awaiting the function call directly. - -## 22.8.4 -- Fix TRIO108 raising errors on yields in some sync code. -- TRIO109 now skips all decorated functions to avoid false alarms - -## 22.8.3 -- TRIO108 now gives multiple error messages; one for each path lacking a guaranteed checkpoint - -## 22.8.2 -- Merged TRIO108 into TRIO107 -- TRIO108 now handles checkpointing in async iterators - -## 22.8.1 -- Added TRIO109: Async definitions should not have a `timeout` parameter. Use `trio.[fail/move_on]_[at/after]` -- Added TRIO110: `while : await trio.sleep()` should be replaced by a `trio.Event`. - -## 22.7.6 -- Extend TRIO102 to also check inside `except BaseException` and `except trio.Cancelled` -- Extend TRIO104 to also check for `yield` -- Update error messages on TRIO102 and TRIO103 - -## 22.7.5 -- Add TRIO103: `except BaseException` or `except trio.Cancelled` with a code path that doesn't re-raise -- Add TRIO104: "Cancelled and BaseException must be re-raised" if user tries to return or raise a different exception. -- Added TRIO107: Async functions must have at least one checkpoint on every code path, unless an exception is raised -- Added TRIO108: Early return from async function must have at least one checkpoint on every code path before it. - -## 22.7.4 -- Added TRIO105 check for not immediately `await`ing async trio functions. -- Added TRIO106 check that trio is imported in a form that the plugin can easily parse. - -## 22.7.3 -- Added TRIO102 check for unsafe checkpoints inside `finally:` blocks - -## 22.7.2 -- Avoid `TRIO100` false-alarms on cancel scopes containing `async for` or `async with`. - -## 22.7.1 -- Initial release with TRIO100 and TRIO101 +The flake8-async changelog can now be seen at https://flake8-async.readthedocs.io/en/latest/changelog.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 498a9c5..46118ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,82 +1 @@ -# Contributor Guide - -Contributions welcome! We'll expand this guide as we go. - - -## Development - -When you wish to add a check to `flake8-async` please ensure the following: - -- `README.md` gets a one line about your new warning -- Add a CHANGELOG entry (see 'Releasing a new version' below) -- A test in `tests/eval_files` is added for your check. See the "Test generator" heading below. -To run our test suite please use tox. - -```console -# Formatting and Linting -pre-commit run [--all-files] -# Run tests against current version of python and latest flake8 -tox -e testenv -# Run all test environments (tox -a for a complete list) -tox -# Tip: Use --parallel and --develop, to save time when modifying and rerunning. -tox -p --develop -# --quiet and --parallel-no-spinner are also nice for output control. -``` - -## Meta-tests -To check that all codes are tested and documented there's a test that error codes mentioned in `README.md`, `CHANGELOG.md` (matching `TRIO\d\d\d`), the keys in `flake8_async.Error_codes` and codes parsed from filenames and files in `tests/eval_files/`, are all equal. - -## Test generator -Tests are automatically generated for files in the `tests/eval_files/` directory, with the code that it's testing interpreted from the file name. The file extension is split off, if there's a match for for `_py\d*` it strips that off and uses it to determine if there's a minimum python version for which the test should only run. - -### `# AUTOFIX` -Files in `tests/eval_files` with this marker will have two files in `tests/autofix_files/`. One with the same name containing the code after being autofixed, and a diff file between those two. -During tests the result of running the checker on the eval file with autofix enabled will be compared to the content of the autofix file and will print a diff (if `-s` is on) and assert that the content is the same. `--generate-autofix` is added as a pytest flag to ease development, which will print a diff (with `-s`) and overwrite the content of the autofix file. -Files without this marker will be checked that they *don't* modify the file content. - -### `error:` -Lines containing `error:` are parsed as expecting an error of the code matching the file name, with everything on the line after the colon `eval`'d and passed as arguments to `flake8_async.Error_codes[].str_format`. The `globals` argument to `eval` contains a `lineno` variable assigned the current line number, and the `flake8_async.Statement` namedtuple. The first element after `error:` *must* be an integer containing the column where the error on that line originates. -#### `TRIOxxx:` -You can instead of `error` specify the error code. - -### `# ARG` -With `# ARG` lines you can also specify command-line arguments that should be passed to the plugin when parsing a file. Can be specified multiple times for several different arguments. -Generated tests will by default `--select` the error code of the file, which will enable any visitors that can generate that code (and if those visitors can raise other codes they might be raised too). This can be overridden by adding an `# ARG --select=...` line. - -### Library parametrization -Eval files are evaluated with each supported library. It does this by replacing all instances of the `BASE_LIBRARY` ("trio" by default) with the two other libraries, and setting the corresponding flag (`--anyio` or `--asyncio`). -### `# BASE_LIBRARY anyio` / `# BASE_LIBRARY asyncio` -Defaults to `trio`. Used to specify the primary library an eval file is testing. - -#### `# ANYIO_NO_ERROR`, `# TRIO_NO_ERROR`, `# ASYNCIO_NO_ERROR` -A file which is marked with this will ignore all `# error` or `# TRIO...` comments when running with anyio. Use when an error is library-specific and replacing all instances means the file should no longer raise any errors. -### `# NOANYIO`, `# NOTRIO`, `#NOASYNCIO` -Disables checking a file with the specified library. Should be used somewhat sparingly, and always have a comment motivating its use. - -## Running pytest outside tox -If you don't want to bother with tox to quickly test stuff, you'll need to install the following dependencies: -``` -pip install -e . -pip install pytest pytest-cov hypothesis hypothesmith flake8 -``` - -## Style Guide - -**Code style:** code review should focus on correctness, performance, and readability. -Low-level nitpicks are handled *exclusively* by our formatters and linters, so if -`tox` passes there's nothing else to say. - -**Terminology:** use "false/missed alarm" rather than "true/false positive", or the -even worse "type I/II error". "False alarm" or "missed alarm" have obvious meanings -which do not rely on confusing conventions (is noticing an error positive or negative?) -or rote memorization of an arbitrary convention. - - -## Releasing a new version -We want to ship bigfixes or new features as soon as they're ready, -so our release process is automated: - -1. Increment `__version__` in `src/flake8_async.py` -2. Ensure there's a corresponding entry in `CHANGELOG.md` with same version -3. Merge to master, and CI will do the rest! +The flake8-async contributor guide can be seen at https://flake8-async.readthedocs.io/en/latest/contributing.html diff --git a/README.md b/README.md index 751ada1..50508ae 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ +[![Documentation](https://img.shields.io/badge/docs-read%20now-blue.svg)](https://flake8-async.readthedocs.io) +[![Latest PyPi version](https://img.shields.io/pypi/v/flake8-async.svg)](https://pypi.org/project/flake8-async) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/python-trio/flake8-async/main.svg)](https://results.pre-commit.ci/latest/github/python-trio/flake8-async/main) +[![Test coverage](https://codecov.io/gh/python-trio/flake8-async/branch/main/graph/badge.svg)](https://codecov.io/gh/python-trio/flake8-async) [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/) # flake8-async @@ -15,184 +18,3 @@ Pairs well with flake8-bugbear. Some checks are incorporated into [ruff](https://github.com/astral-sh/ruff). This plugin was previously known as flake8-trio, and there was a separate small plugin known as flake8-async for asyncio. But this plugin was a superset of the checks in flake8-async, and support for anyio was added, so it's now named flake8-async to more properly convey its usage. At the same time all error codes were renamed from TRIOxxx to ASYNCxxx, as was previously used by the old flake8-async. - -## Installation - -```console -pip install flake8-async -``` - -## List of warnings -- **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. This check also allows `yield` statements, since checkpoints can happen in the caller we yield to. -- **ASYNC101**: `yield` inside a trio/anyio nursery or cancel scope is only safe when implementing a context manager - otherwise, it breaks exception handling. -- **ASYNC102**: It's unsafe to await inside `finally:` or `except BaseException/trio.Cancelled/anyio.get_cancelled_exc_class()/asyncio.exceptions.CancelledError` unless you use a shielded cancel scope with a timeout. This is currently not able to detect asyncio shields. -- **ASYNC103**: `except BaseException/trio.Cancelled/anyio.get_cancelled_exc_class()/asyncio.exceptions.CancelledError`, or a bare `except:` with a code path that doesn't re-raise. If you don't want to re-raise `BaseException`, add a separate handler for `trio.Cancelled`/`anyio.get_cancelled_exc_class()`/`asyncio.exceptions.CancelledError` before. -- **ASYNC104**: `trio.Cancelled`/`anyio.get_cancelled_exc_class()`/`asyncio.exceptions.CancelledError`/`BaseException` must be re-raised. The same as ASYNC103, except specifically triggered on `return` or a different exception being raised. -- **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`. -- **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()`. -- **ASYNC118**: Don't assign the value of `anyio.get_cancelled_exc_class()` to a variable, since that breaks linter checks and multi-backend programs. -- **ASYNC119**: `yield` in context manager in async generator is unsafe, the cleanup may be delayed until `await` is no longer allowed. We strongly encourage you to read PEP-533 and use `async with aclosing(...)`, or better yet avoid async generators entirely (see ASYNC900) in favor of context managers which return an iterable channel/queue. - -### Warnings for blocking sync calls in async functions -Note: 22X, 23X and 24X has not had asyncio-specific suggestions written. -- **ASYNC200**: User-configured error for blocking sync calls in async functions. Does nothing by default, see [`async200-blocking-calls`](#async200-blocking-calls) for how to configure it. -- **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, ...)`. `asyncio` users can use [`asyncio.create_subprocess_[exec/shell]`](https://docs.python.org/3/library/asyncio-subprocess.html). -- **ASYNC221**: Sync process call in async function, use `await [trio/anyio].run_process(...)`. `asyncio` users can use [`asyncio.create_subprocess_[exec/shell]`](https://docs.python.org/3/library/asyncio-subprocess.html). -- **ASYNC222**: Sync `os.*` call in async function, wrap in `await [trio/anyio].to_thread.run_sync()`. `asyncio` users can use [`asyncio.loop.run_in_executor`](https://docs.python.org/3/library/asyncio-subprocess.html). -- **ASYNC230**: Sync IO call in async function, use `[trio/anyio].open_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). -- **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. 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. -- **ASYNC910**: Exit or `return` from async function with no guaranteed checkpoint or exception since function definition. You might want to enable this on a codebase to make it easier to reason about checkpoints, and make the logic of ASYNC911 correct. -- **ASYNC911**: Exit, `yield` or `return` from async iterable with no guaranteed checkpoint since possible function entry (yield or function definition) - Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit). -- **ASYNC912**: Timeout/Cancelscope has no awaits that are guaranteed to run. If the scope has no checkpoints at all, then `ASYNC100` will be raised instead. - -### Removed Warnings -- **TRIOxxx**: All error codes are now renamed ASYNCxxx -- **TRIO107**: Renamed to TRIO910 -- **TRIO108**: Renamed to TRIO911 -- **TRIO117**: Don't raise or catch `trio.[NonBase]MultiError`, prefer `[exceptiongroup.]BaseExceptionGroup`. `MultiError` was removed in trio==0.24.0. - -## Examples -### install and run through flake8 -```sh -pip install flake8 flake8-async -flake8 . -``` -### install and run with pre-commit -If you use [pre-commit](https://pre-commit.com/), you can use it with flake8-async by -adding the following to your `.pre-commit-config.yaml`: - -```yaml -minimum_pre_commit_version: '2.9.0' -repos: -- repo: https://github.com/python-trio/flake8-async - rev: 23.2.5 - hooks: - - id: flake8-async - # args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC] -``` - -This is often considerably faster for large projects, because `pre-commit` -can avoid running `flake8-async` on unchanged files. - - -Afterwards, run -```sh -pip install pre-commit flake8-async -pre-commit run . -``` -### install and run as standalone -If inside a git repository, running without arguments will run it against all `*.py` files in the repository. -```sh -pip install flake8-async -flake8-async -``` -#### with autofixes -```sh -flake8-async --autofix=ASYNC -``` -#### specifying source files -```sh -flake8-async my_python_file.py -``` -##### zsh-only -```zsh -flake8-async **/*.py -``` - -## Configuration -[You can configure `flake8` with command-line options](https://flake8.pycqa.org/en/latest/user/configuration.html), -but we prefer using a config file. The file needs to start with a section marker `[flake8]` and the following options are then parsed using flake8's config parser, and can be used just like any other flake8 options. -Note that it's not currently possible to use a configuration file when running `flake8-async` standalone. - -### `ValueError` when trying to `ignore` error codes in config file -Error codes with more than three letters are not possible to `ignore` in config files since flake8>=6, as flake8 tries to validate correct configuration with a regex. We have decided not to conform to this, as it would be a breaking change for end-users requiring them to update `noqa`s and configurations, we think the `ASYNC` code is much more readable than e.g. `ASYxxx`, and ruff does not enforce such a limit. The easiest option for users hitting this error is to instead use the `--disable` option as documented [below](#--disable). See further discussion and other workarounds in https://github.com/python-trio/flake8-async/issues/230 - -### `--enable` -Comma-separated list of error codes to enable, similar to flake8 --select but is additionally more performant as it will disable non-enabled visitors from running instead of just silencing their errors. - -### `--disable` -Comma-separated list of error codes to disable, similar to flake8 --ignore but is additionally more performant as it will disable non-enabled visitors from running instead of just silencing their errors. - -### `--autofix` -Comma-separated list of error-codes to enable autofixing for if implemented. Requires running as a standalone program. Pass `--autofix=ASYNC` to enable all autofixes. - -### `--error-on-autofix` -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]. - -### `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`. - -Decorators-to-match must be identifiers or dotted names only (not PEP-614 expressions), and will match against the name only - e.g. `foo.bar` matches `foo.bar`, `foo.bar()`, and `foo.bar(args, here)`, etc. - -For example: -``` -no-checkpoint-warning-decorators = - mydecorator, - mydecoratorpackage.checkpointing_decorators.*, - ign*, - *.ignore, -``` - - -### `startable-in-context-manager` -Comma-separated list of methods which should be used with `.start()` when opening a context manager, -in addition to the default `trio.run_process`, `trio.serve_tcp`, `trio.serve_ssl_over_tcp`, and -`trio.serve_listeners`. Names must be valid identifiers as per `str.isidentifier()`. For example: -``` -startable-in-context-manager = - myfun, - myfun2, -``` - -### `async200-blocking-calls` -Comma-separated list of pairs of values separated by `->` (optional whitespace stripped), where the first is a pattern for a call that should raise an error if found inside an async function, and the second is what should be suggested to use instead. It uses fnmatch as per [`no-checkpoint-warning-decorators`](#no-checkpoint-warning-decorators) for matching. The part after `->` is not used by the checker other than when printing the error, so you could add extra info there if you want. - -The format of the error message is `User-configured blocking sync call {0} in async function, consider replacing with {1}.`, where `{0}` is the pattern the call matches and `{1}` is the suggested replacement. - -Example: -```ini -async200-blocking-calls = - my_blocking_call -> async.alternative, - module.block_call -> other_function_to_use, - common_error_call -> alternative(). But sometimes you should use other_function(). Ask joe if you're unsure which one, - dangerous_module.* -> corresponding function in safe_module, - *.dangerous_call -> .safe_call() -``` -Specified patterns must not have parentheses, and will only match when the pattern is the name of a call, so given the above configuration -```python -async def my_function(): - my_blocking_call() # this would raise an error - x = my_blocking_call(a, b, c) # as would this - y = my_blocking_call # but not this - y() # or this - [my_blocking_call][0]() # nor this - - def my_blocking_call(): # it's also safe to use the name in other contexts - ... - - arbitrary_other_function(my_blocking_call=None) -``` diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..9251246 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,231 @@ +######### +Changelog +######### + +*[CalVer, YY.month.patch](https://calver.org/)* + +24.5.2 +====== +- ASYNC101 now also warns on anyio & asyncio taskgroups. +- Fixed a bug where ASYNC101 and ASYNC91x would not recognize decorators with parameters directly imported. I.e. ``@fixture(...)`` will now suppress errors. + +24.5.1 +====== +- Add ASYNC912: no checkpoints in with statement are guaranteed to run. +- ASYNC100 now properly treats async for comprehensions as checkpoints. +- ASYNC100 now supports autofixing on asyncio. + +24.4.2 +====== +- Add ASYNC119: yield in contextmanager in async generator. + +24.4.1 +====== +- ASYNC91X: fix internal error caused by multiple ``try/except`` incorrectly sharing state. + +24.3.6 +====== +- ASYNC100 no longer triggers if a context manager contains a ``yield``. + +24.3.5 +====== +- ASYNC102 (no await inside finally or critical except) no longer raises warnings for calls to ``aclose()`` on objects in trio/anyio code. See https://github.com/python-trio/flake8-async/issues/156 + +24.3.4 +====== +- ASYNC110 (don't loop sleep) now also warns if looping ``[trio/anyio].lowlevel.checkpoint()``. + +24.3.3 +====== +- Add ASYNC251: ``time.sleep()`` in async method. + +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``. +- Renamed all error codes from TRIOxxx to ASYNCxxx +- Renamed the binary from flake8-trio to flake8-async +- Lots of internal renaming. +- Added asyncio support for several error codes +- added ``--library`` + +23.5.1 +====== +- TRIO91X now supports comprehensions +- TRIO100 and TRIO91X now supports autofixing +- Renamed ``--enable-visitor-codes-regex`` to ``--enable`` +- Added ``--disable``, ``--autofix`` and ``--error-on-autofix`` + +23.2.5 +====== +- Fix false alarms for ``@pytest.fixture``-decorated functions in TRIO101, TRIO910 and TRIO911 + +23.2.4 +====== +- Fix TRIO900 false alarm on nested functions +- TRIO113 now also works on ``anyio.TaskGroup`` + +23.2.3 +====== +- Fix ``get_matching_call`` when passed a single string as base. Resolves possibly several false alarms, TRIO210 among them. + +23.2.2 +====== +- Rename TRIO107 to TRIO910, and TRIO108 to TRIO911, and making them optional by default. +- Allow ``@pytest.fixture()``-decorated async generators, since they're morally context managers +- Add support for checking code written against `AnyIO `_ +- Add TRIO118: Don't assign the value of ``anyio.get_cancelled_exc_class()`` to a variable, since that breaks linter checks and multi-backend programs. + +23.2.1 +====== +- TRIO103 and TRIO104 no longer triggers when ``trio.Cancelled`` has been handled in previous except handlers. +- Add TRIO117: Reference to deprecated ``trio.[NonBase]MultiError``; use ``[Base]ExceptionGroup`` instead. +- Add TRIO232: blocking sync call on file object. +- Add TRIO212: blocking sync call on ``httpx.Client`` object. +- Add TRIO222: blocking sync call to ``os.wait*`` +- TRIO221 now also looks for ``os.posix_spawn[p]`` + +23.1.4 +====== +- TRIO114 avoids a false alarm on posonly args named "task_status" +- TRIO116 will now match on any attribute parameter named ``.inf``, not just ``math.inf``. +- TRIO900 now only checks ``@asynccontextmanager``, not other decorators passed with --no-checkpoint-warning-decorators. + +23.1.3 +====== +- Add TRIO240: usage of ``os.path`` in async function. +- Add TRIO900: ban async generators not decorated with known safe decorator + +23.1.2 +====== +- Add TRIO230, TRIO231 - sync IO calls in async function + +23.1.1 +====== +- Add TRIO210, TRIO211 - blocking sync call in async function, using network packages (requests, httpx, urllib3) +- Add TRIO220, TRIO221 - blocking sync call in async function, using subprocess or os. + +22.12.5 +======= +- The ``--startable-in-context-manager`` and ``--trio200-blocking-calls`` options now handle spaces and newlines. +- Now compatible with `flake8-noqa `_ NQA102 and NQA103 checks. + +22.12.4 +======= +- TRIO200 no longer warns on directly awaited calls + +22.12.3 +======= +- Worked around configuration-parsing bug for TRIO200 warning (more to come) + +22.12.2 +======= +- Add TRIO200: User-configured blocking sync call in async function + +22.12.1 +======= +- TRIO114 will now trigger on the unqualified name, will now only check the first parameter + directly, and parameters to function calls inside that. +- TRIO113 now only supports names that are valid identifiers, rather than fnmatch patterns. +- Add TRIO115: Use ``trio.lowlevel.checkpoint()`` instead of ``trio.sleep(0)``. + +22.11.5 +======= +- Add TRIO116: ``trio.sleep()`` with >24 hour interval should usually be ``trio.sleep_forever()``. + +22.11.4 +======= +- Add TRIO114 Startable function not in ``--startable-in-context-manager`` parameter list. + +22.11.3 +======= +- Add TRIO113, prefer ``await nursery.start(...)`` to ``nursery.start_soon()`` for compatible functions when opening a context manager + +22.11.2 +======= +- TRIO105 now also checks that you ``await``ed ``nursery.start()``. + +22.11.1 +======= +- TRIO102 is no longer skipped in (async) context managers, since it's not a missing-checkpoint warning. + +22.9.2 +====== +- Fix a crash on nontrivial decorator expressions (calls, PEP-614) and document behavior. + +22.9.1 +====== +- Add ``--no-checkpoint-warning-decorators`` option, to disable missing-checkpoint warnings for certain decorated functions. + +22.8.8 +====== +- Fix false alarm on TRIO107 with checkpointing ``try`` and empty ``finally`` +- Fix false alarm on TRIO107&108 with infinite loops + +22.8.7 +====== +- TRIO107+108 now ignores ``asynccontextmanager`s, since both `__aenter__`` and ``__aexit__`` should checkpoint. ``async with`` is also treated as checkpointing on both enter and exit. +- TRIO107 now completely ignores any function whose body consists solely of ellipsis, pass, or string constants. +- TRIO103, 107 and 108 now inspects ``while`` conditions and ``for`` iterables to avoid false alarms on a couple cases where the loop body is guaranteed to run at least once. + +22.8.6 +====== +- TRIO103 now correctly handles raises in loops, i.e. ``raise`` in else is guaranteed to run unless there's a ``break`` in the body. + +22.8.5 +====== +- Add TRIO111: 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. +- Add TRIO112: this single-task nursery could be replaced by awaiting the function call directly. + +22.8.4 +====== +- Fix TRIO108 raising errors on yields in some sync code. +- TRIO109 now skips all decorated functions to avoid false alarms + +22.8.3 +====== +- TRIO108 now gives multiple error messages; one for each path lacking a guaranteed checkpoint + +22.8.2 +====== +- Merged TRIO108 into TRIO107 +- TRIO108 now handles checkpointing in async iterators + +22.8.1 +====== +- Added TRIO109: Async definitions should not have a ``timeout`` parameter. Use ``trio.[fail/move_on]_[at/after]`` +- Added TRIO110: ``while : await trio.sleep()`` should be replaced by a ``trio.Event``. + +22.7.6 +====== +- Extend TRIO102 to also check inside ``except BaseException`` and ``except trio.Cancelled`` +- Extend TRIO104 to also check for ``yield`` +- Update error messages on TRIO102 and TRIO103 + +22.7.5 +====== +- Add TRIO103: ``except BaseException`` or ``except trio.Cancelled`` with a code path that doesn't re-raise +- Add TRIO104: "Cancelled and BaseException must be re-raised" if user tries to return or raise a different exception. +- Added TRIO107: Async functions must have at least one checkpoint on every code path, unless an exception is raised +- Added TRIO108: Early return from async function must have at least one checkpoint on every code path before it. + +22.7.4 +====== +- Added TRIO105 check for not immediately ``await`` ing async trio functions. +- Added TRIO106 check that trio is imported in a form that the plugin can easily parse. + +22.7.3 +====== +- Added TRIO102 check for unsafe checkpoints inside ``finally:`` blocks + +22.7.2 +====== +- Avoid ``TRIO100`` false-alarms on cancel scopes containing ``async for`` or ``async with``. + +22.7.1 +====== +- Initial release with TRIO100 and TRIO101 diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..35bf401 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,136 @@ +***************** +Contributor Guide +***************** + +Contributions welcome! We'll expand this guide as we go. + +Development +=========== + +When you wish to add a check to ``flake8-async`` please ensure the following: + +- ``rules.rst`` gets an entry about your new warning +- Add a changelog entry (see 'Releasing a new version' below) +- A test in ``tests/eval_files`` is added for your check. See the "Test generator" heading below. + +Checks +------ +We use pre-commit for managing formatters, linters, type-checkers, etc. Passing the checks is required to merge a pull request. + +.. code-block:: + + pre-commit run [--all-files] + + +Running tests +------------- +Run tests against current version of python and latest flake8 + +.. code-block:: + + tox -e testenv + +Run all test environments (``tox -a`` for a complete list) + +.. code-block:: + + tox + +Run a specific python version + +.. code-block:: + + tox -e py311 + +Tip: Use ``--parallel`` and ``--develop``, to save time when modifying and rerunning. + +.. code-block:: + + tox -p --develop + +``--quiet`` and ``--parallel-no-spinner`` are also nice for output control. + +Meta-tests +========== +To check that all codes are tested and documented there's a test that error codes mentioned in ``docs/rules.rst``, ``docs/changelog.rst`` (matching ``ASYNC\d\d\d``), the keys in ``flake8_async.Error_codes`` and codes parsed from filenames and files in ``tests/eval_files/``, are all equal. + +Test generator +============== +Tests are automatically generated for files in the ``tests/eval_files/`` directory, with the code that it's testing interpreted from the file name. The file extension is split off, if there's a match for for ``_py\d*`` it strips that off and uses it to determine if there's a minimum python version for which the test should only run. (Note: no tests are currently version-gated). Other descriptions after ``async\d\d\d_`` are not interpreted by the test generator. + +``error:`` +---------- +Lines containing ``error:`` are parsed as expecting an error of the code matching the file name, with everything on the line after the colon ``eval``'d and passed as arguments to ``flake8_async.Error_codes[].str_format``. The ``globals`` argument to ``eval`` contains a ``lineno`` variable assigned the current line number, and the ``flake8_async.Statement`` namedtuple. The first element after ``error:`` *must* be an integer containing the column where the error on that line originates. + +``ASYNCxxx:`` +^^^^^^^^^^^^^ +You can instead of ``error`` specify the error code. This is required if multiple different errors are enabled for the same test file. + +Library parametrization +----------------------- +Eval files are evaluated with each supported library. It does this by replacing all instances of the ``BASE_LIBRARY`` ("trio" by default) with the two other libraries, and setting the corresponding flag (``--anyio`` or ``--asyncio``). See :ref:`BASE_LIBRARY` + + +Magic Markers +------------- +Special comments can be put into eval files to customize different aspects of the test generator. + +.. _BASE_LIBRARY: + +``# BASE_LIBRARY`` +^^^^^^^^^^^^^^^^^^ +Used to specify the primary library an eval file is testing. Defaults to ``trio``. E.g. + +.. code-block:: python + + # BASE_LIBRARY anyio + + +``# AUTOFIX`` +^^^^^^^^^^^^^ +Files in ``tests/eval_files`` with this marker will have two files in ``tests/autofix_files/``. One with the same name containing the code after being autofixed, and a diff file between those two. +During tests the result of running the checker on the eval file with autofix enabled will be compared to the content of the autofix file and will print a diff (if ``-s`` is on) and assert that the content is the same. ``--generate-autofix`` is added as a pytest flag to ease development, which will print a diff (with ``-s``) and overwrite the content of the autofix file. +Files without this marker will be checked that they *don't* modify the file content. + +``# ARG`` +^^^^^^^^^ +With ``# ARG`` lines you can also specify command-line arguments that should be passed to the plugin when parsing a file. Can be specified multiple times for several different arguments. +Generated tests will by default ``--select`` the error code of the file, which will enable any visitors that can generate that code (and if those visitors can raise other codes they might be raised too). This can be overridden by adding an ``# ARG --select=...`` line. + +``# ANYIO_NO_ERROR``, ``# TRIO_NO_ERROR``, ``# ASYNCIO_NO_ERROR`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +A file which is marked with this will ignore all ``# error`` or ``# TRIO...`` comments when running with anyio. Use when an error is library-specific and replacing all instances means the file should no longer raise any errors. + +``# NOANYIO``, ``# NOTRIO``, ``#NOASYNCIO`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Disables checking a file with the specified library. Should be used somewhat sparingly, and always have a comment motivating its use. + +Running pytest outside tox +========================== +If you don't want to bother with tox to quickly test stuff, you'll need to install the following dependencies: + +.. code-block:: console + + pip install -e . + pip install pytest pytest-cov hypothesis hypothesmith flake8 + +Style Guide +=========== + +**Code style:** code review should focus on correctness, performance, and readability. +Low-level nitpicks are handled *exclusively* by our formatters and linters, so if +``pre-commit`` passes there's nothing else to say. + +**Terminology:** use "false/missed alarm" rather than "true/false positive", or the +even worse "type I/II error". "False alarm" or "missed alarm" have obvious meanings +which do not rely on confusing conventions (is noticing an error positive or negative?) +or rote memorization of an arbitrary convention. + +Releasing a new version +======================= +We want to ship bugfixes or new features as soon as they're ready, +so our release process is automated: + +1. Increment ``__version__`` in ``flake8_async/__init__.py`` +2. Ensure there's a corresponding entry in ``docs/changelog.rst`` with same version +3. Merge to main, and CI will do the rest! diff --git a/docs/index.rst b/docs/index.rst index d41e7ff..c610291 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,7 +25,6 @@ Some rules are incorporated into `ruff Path: author="Zac Hatfield-Dodds, John Litborn, and Contributors", author_email="zac@zhd.dev", packages=find_packages(include=["flake8_async", "flake8_async.*"]), - url="https://github.com/python-trio/flake8-async", + project_urls={ + "Homepage": "https://github.com/python-trio/flake8-async", + "Documentation": "https://flake8-async.readthedocs.io/", + "Changelog": "https://flake8-async.readthedocs.io/en/latest/changelog.html", + }, license="MIT", description="A highly opinionated flake8 plugin for Trio-related problems.", zip_safe=False, @@ -44,11 +48,7 @@ def local_file(name: str) -> Path: "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ], - long_description=( - local_file("README.md").open().read() - + "\n\n" - + local_file("CHANGELOG.md").open().read() - ), + long_description=(local_file("README.md").open().read()), long_description_content_type="text/markdown", entry_points={ # You're not allowed to register error codes longer than 3 characters. But flake8 diff --git a/tests/test_changelog_and_version.py b/tests/test_changelog_and_version.py index 9b44f88..1d64676 100755 --- a/tests/test_changelog_and_version.py +++ b/tests/test_changelog_and_version.py @@ -12,7 +12,7 @@ from collections.abc import Iterable ROOT_PATH = Path(__file__).parent.parent -CHANGELOG = ROOT_PATH / "CHANGELOG.md" +CHANGELOG = ROOT_PATH / "docs" / "changelog.rst" README = ROOT_PATH / "README.md" INIT_FILE = ROOT_PATH / "flake8_async" / "__init__.py" @@ -41,21 +41,26 @@ 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"^## ") + valid_pattern = re.compile(r"^(\d\d\.\d?\d\.\d?\d)$") + header_pattern = re.compile(r"^=+$") + last_line_was_date = False with open(CHANGELOG, encoding="utf-8") as f: lines = f.readlines() for line in lines: version_match = valid_pattern.match(line) - if version_match: + if last_line_was_date: + assert header_pattern.match(line) + last_line_was_date = False + elif version_match: yield Version.from_string(version_match.group(1)) + last_line_was_date = True else: - # stop lines such as `## Future` making it through to main/ - assert not invalid_pattern.match(line) + # stop lines such as `Future\n=====` making it through to main/ + assert not header_pattern.match(line), line def test_last_release_against_changelog() -> None: - """Ensure we have the latest version covered in 'CHANGELOG.md'.""" + """Ensure we have the latest version covered in 'changelog.rst'.""" latest_release = next(iter(get_releases())) assert latest_release == VERSION diff --git a/tests/test_messages_documented.py b/tests/test_messages_documented.py index 1b24da7..dae79a1 100755 --- a/tests/test_messages_documented.py +++ b/tests/test_messages_documented.py @@ -10,8 +10,8 @@ from .test_flake8_async import ERROR_CODES ROOT_PATH = Path(__file__).parent.parent -CHANGELOG = ROOT_PATH / "CHANGELOG.md" -README = CHANGELOG.parent / "README.md" +CHANGELOG = ROOT_PATH / "docs" / "changelog.rst" +RULES_DOC = ROOT_PATH / "docs" / "rules.rst" # 107, 108 & 117 are removed (but still mentioned in changelog & readme) # ASYNCxxx_* are fake codes to get different error messages for the same code @@ -26,7 +26,7 @@ def rename_trio_to_async(s: str) -> str: def test_messages_documented(): documented_errors: dict[str, set[str]] = {} - for path in (CHANGELOG, README): + for path in (CHANGELOG, RULES_DOC): with open(path, encoding="utf-8") as f: lines = f.readlines() filename = path.name