From 368b915e19a0c68d857e0d56a4b16c21cfadbf21 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:35:24 +0200 Subject: [PATCH] add basic sphinx & readthedocs infra (#223) * add basic sphinx & readthedocs setup * copy over most of existing documentation, with some changes. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Zac Hatfield-Dodds --- .readthedocs.yaml | 33 +++++++ docs/Makefile | 20 ++++ docs/__init__.py | 4 + docs/conf.py | 43 +++++++++ docs/index.rst | 47 ++++++++++ docs/make.bat | 35 +++++++ docs/requirements.txt | 1 + docs/rules.rst | 59 ++++++++++++ docs/usage.rst | 214 ++++++++++++++++++++++++++++++++++++++++++ tox.ini | 12 +++ 10 files changed, 468 insertions(+) create mode 100644 .readthedocs.yaml create mode 100644 docs/Makefile create mode 100644 docs/__init__.py create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/requirements.txt create mode 100644 docs/rules.rst create mode 100644 docs/usage.rst diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..07156ef --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,33 @@ +--- +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-lts-latest + tools: + python: latest + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 0000000..6e0a700 --- /dev/null +++ b/docs/__init__.py @@ -0,0 +1,4 @@ +"""Documentation for flake8-async. + +Ruff raised INP001 "implicit namespace package" without this file. +""" diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..830d78b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,43 @@ +"""Configuration file for the Sphinx documentation builder. + +For the full list of built-in configuration values, see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path("..").resolve())) +import flake8_async + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "flake8-async" +# A001: shadowing python builtin +copyright = "2024, Zac Hatfield-Dodds, John Litborn, and Contributors" # noqa: A001 +author = "Zac Hatfield-Dodds, John Litborn, and Contributors" + + +version = flake8_async.__version__ +release = version + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions: list[str] = [] + +templates_path = ["_templates"] +exclude_patterns: list[str] = ["_build", "Thumbs.db", ".DS_Store"] + +# Warn about all references to unknown targets +nitpicky = True + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +# We don't currently use the _static directory, and git doesn't allow empty directories, +# so leaving it commented out for now to silence a warning. +# `html_static_path = ["_static"]` diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d41e7ff --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,47 @@ +.. flake8-async documentation master file, created by + sphinx-quickstart on Wed Mar 20 13:37:26 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +############ +flake8-async +############ + + +A highly opinionated flake8 plugin for problems related to `Trio `_, `AnyIO `_, or `asyncio `_. + + +This can include anything from outright bugs, to pointless/dead code, +to likely performance issues, to minor points of idiom that might signal +a misunderstanding. + + +The plugin may well be too noisy or pedantic depending on your requirements or opinions, in which case you should consider :ref:`--disable` for those rules. +Pairs well with flake8-bugbear. + + +Some rules are incorporated into `ruff `_. + + +We previously maintained separate flake8-async and flake8-trio plugins, but merged both into this plugin under the more general "flake8-async" name after flake8-trio grew support for anyio and asyncio and became a superset of the former flake8-async. All flake8-trio error codes were renamed from TRIOxxx to ASYNCxxx and the flake8-trio package is now deprecated. + + +********* +Contents: +********* +.. toctree:: + :maxdepth: 2 + + usage + rules + + +****************** +Indices and tables +****************** + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` +* :doc:`usage` +* :doc:`rules` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..d3a945f --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +libcst diff --git a/docs/rules.rst b/docs/rules.rst new file mode 100644 index 0000000..007a548 --- /dev/null +++ b/docs/rules.rst @@ -0,0 +1,59 @@ +**************** +List of rules +**************** + +General rules +============= + +- **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. + +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 :ref:`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] `_. +- **ASYNC221**: Sync process call in async function, use ``await [trio/anyio].run_process(...)``. ``asyncio`` users can use `asyncio.create_subprocess_[exec/shell] `_. +- **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 `_. +- **ASYNC230**: Sync IO call in async function, use ``[trio/anyio].open_file(...)``. ``asyncio`` users need to use a library such as `aiofiles `_, or switch to `anyio `_. +- **ASYNC231**: Sync IO call in async function, use ``[trio/anyio].wrap_file(...)``. ``asyncio`` users need to use a library such as `aiofiles `_, or switch to `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 `_ or `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(...)``. + +Optional rules 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). + +Removed rules +================ + +- **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. diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..78aba3b --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,214 @@ +************ +Installation +************ + +.. code-block:: console + + pip install flake8-async + +***** +Usage +***** + +install and run through flake8 +============================== + +.. code-block:: sh + + pip install flake8 flake8-async + flake8 . + +.. _install-run-pre-commit: + +install and run with pre-commit +=============================== + +If you use `pre-commit `_, you can use it with flake8-async by +adding the following to your ``.pre-commit-config.yaml``: + +.. code-block:: 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 + +.. code-block:: 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. + +.. code-block:: sh + + pip install flake8-async + flake8-async + +with autofixes +-------------- + +.. code-block:: sh + + flake8-async --autofix=ASYNC + +specifying source files +----------------------- + +.. code-block:: sh + + flake8-async my_python_file.py + +zsh-only +^^^^^^^^ + +.. code-block:: zsh + + flake8-async **/*.py + + +Run through ruff +================ +`Ruff `_ is a linter and formatter that reimplements a lot of rules from various flake8 plugins. + +They currently only support a small subset of the rules though, see https://github.com/astral-sh/ruff/issues/8451 for current status and https://docs.astral.sh/ruff/rules/#flake8-async-async for documentation. + +************* +Configuration +************* + +`You can configure flake8 with command-line options `_, +but we prefer using a config file. See general documentation for `configuring flake8 `_ which also handles options registered by plugins such as ``flake8-async``. + +If you want to use a ``pyproject.toml`` file for configuring flake8 we recommend `pyproject-flake8 ` or similar. + +Note that when running ``flake8-async`` as a standalone it's not currently possible to use a configuration file. Consider using some wrapper that lets you specify command-line flags in a file. For example, :ref:`install-run-pre-commit`, `tox `, `hatch scripts `, MakeFiles, etc. + +Selecting rules +=============== + +``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: + +``--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. It will also bypass errors introduced in flake8>=6, see above. + +``--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. + +Modifying rule behaviour +======================== + +.. _--anyio: + +``--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]. + +``--asyncio`` +------------- +Set default library to be ``asyncio``. See :ref:`--anyio` + + +``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 `_. 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: + +``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`_ 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: + +:: + + 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 + +:: + + 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/tox.ini b/tox.ini index b7e9feb..761ce97 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,18 @@ deps = commands = pytest {posargs:-n auto} +[testenv:docs] +description = Generate docs locally +deps = + sphinx + readthedocs-sphinx-ext + -r docs/requirements.txt +allowlist_externals = make +changedir = docs +skip_install = True +commands = + make html + # Settings for other tools [pytest] addopts =