diff --git a/README.rst b/README.rst index d62087bb..81984e88 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -pytest-asyncio: pytest support for asyncio -========================================== +pytest-asyncio +============== .. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg :target: https://pypi.python.org/pypi/pytest-asyncio @@ -13,12 +13,9 @@ pytest-asyncio: pytest support for asyncio .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black -pytest-asyncio is an Apache2 licensed library, written in Python, for testing -asyncio code with pytest. +pytest-asyncio is a `pytest `_ plugin. It facilitates testing of code that uses the `asyncio `_ library. -asyncio code is usually written in the form of coroutines, which makes it -slightly more difficult to test using normal testing tools. pytest-asyncio -provides useful fixtures and markers to make testing easier. +Specifically, pytest-asyncio provides support for coroutines as test functions. This allows users to *await* code inside their tests. For example, the following code is executed as a test item by pytest: .. code-block:: python @@ -27,21 +24,14 @@ provides useful fixtures and markers to make testing easier. res = await library.do_something() assert b"expected result" == res -pytest-asyncio has been strongly influenced by pytest-tornado_. -.. _pytest-tornado: https://github.com/eugeniy/pytest-tornado +Note that test classes subclassing the standard `unittest `__ library are not supported. Users +are advised to use `unittest.IsolatedAsyncioTestCase `__ +or an async framework such as `asynctest `__. + -Features --------- +pytest-asyncio is available under the `Apache License 2.0 `_. -- fixtures for creating and injecting versions of the asyncio event loop -- fixtures for injecting unused tcp/udp ports -- pytest markers for treating tests as asyncio coroutines -- easy testing with non-default event loops -- support for `async def` fixtures and async generator fixtures -- support *auto* mode to handle all async fixtures and tests automatically by asyncio; - provide *strict* mode if a test suite should work with different async frameworks - simultaneously, e.g. ``asyncio`` and ``trio``. Installation ------------ @@ -54,191 +44,6 @@ To install pytest-asyncio, simply: This is enough for pytest to pick up pytest-asyncio. -Modes ------ - -Pytest-asyncio provides two modes: *auto* and *strict* with *strict* mode being the default. - -The mode can be set by ``asyncio_mode`` configuration option in `configuration file -`_: - -.. code-block:: ini - - # pytest.ini - [pytest] - asyncio_mode = auto - -The value can be overridden by command-line option for ``pytest`` invocation: - -.. code-block:: bash - - $ pytest tests --asyncio-mode=strict - -Auto mode -~~~~~~~~~ - -When the mode is auto, all discovered *async* tests are considered *asyncio-driven* even -if they have no ``@pytest.mark.asyncio`` marker. - -All async fixtures are considered *asyncio-driven* as well, even if they are decorated -with a regular ``@pytest.fixture`` decorator instead of dedicated -``@pytest_asyncio.fixture`` counterpart. - -*asyncio-driven* means that tests and fixtures are executed by ``pytest-asyncio`` -plugin. - -This mode requires the simplest tests and fixtures configuration and is -recommended for default usage *unless* the same project and its test suite should -execute tests from different async frameworks, e.g. ``asyncio`` and ``trio``. In this -case, auto-handling can break tests designed for other framework; please use *strict* -mode instead. - -Strict mode -~~~~~~~~~~~ - -Strict mode enforces ``@pytest.mark.asyncio`` and ``@pytest_asyncio.fixture`` usage. -Without these markers, tests and fixtures are not considered as *asyncio-driven*, other -pytest plugin can handle them. - -Please use this mode if multiple async frameworks should be combined in the same test -suite. - -This mode is used by default for the sake of project inter-compatibility. - - -Fixtures --------- - -``event_loop`` -~~~~~~~~~~~~~~ -Creates a new asyncio event loop based on the current event loop policy. The new loop -is available as the return value of this fixture or via `asyncio.get_running_loop `__. -The event loop is closed when the fixture scope ends. The fixture scope defaults -to ``function`` scope. - -Note that just using the ``event_loop`` fixture won't make your test function -a coroutine. You'll need to interact with the event loop directly, using methods -like ``event_loop.run_until_complete``. See the ``pytest.mark.asyncio`` marker -for treating test functions like coroutines. - -.. code-block:: python - - def test_http_client(event_loop): - url = "http://httpbin.org/get" - resp = event_loop.run_until_complete(http_client(url)) - assert b"HTTP/1.1 200 OK" in resp - -The ``event_loop`` fixture can be overridden in any of the standard pytest locations, -e.g. directly in the test file, or in ``conftest.py``. This allows redefining the -fixture scope, for example: - -.. code-block:: python - - @pytest.fixture(scope="session") - def event_loop(): - policy = asyncio.get_event_loop_policy() - loop = policy.new_event_loop() - yield loop - loop.close() - -If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture. - -If the ``pytest.mark.asyncio`` marker is applied to a test function, the ``event_loop`` -fixture will be requested automatically by the test function. - -``unused_tcp_port`` -~~~~~~~~~~~~~~~~~~~ -Finds and yields a single unused TCP port on the localhost interface. Useful for -binding temporary test servers. - -``unused_tcp_port_factory`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A callable which returns a different unused TCP port each invocation. Useful -when several unused TCP ports are required in a test. - -.. code-block:: python - - def a_test(unused_tcp_port_factory): - port1, port2 = unused_tcp_port_factory(), unused_tcp_port_factory() - ... - -``unused_udp_port`` and ``unused_udp_port_factory`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Work just like their TCP counterparts but return unused UDP ports. - - -Async fixtures -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``. - -.. code-block:: python3 - - import pytest_asyncio - - - @pytest_asyncio.fixture - async def async_gen_fixture(): - await asyncio.sleep(0.1) - yield "a value" - - - @pytest_asyncio.fixture(scope="module") - async def async_fixture(): - return await asyncio.sleep(0.1) - -All scopes are supported, but if you use a non-function scope you will need -to redefine the ``event_loop`` fixture to have the same or broader scope. -Async fixtures need the event loop, and so must have the same or narrower scope -than the ``event_loop`` fixture. - -*auto* mode automatically converts async fixtures declared with the -standard ``@pytest.fixture`` decorator to *asyncio-driven* versions. - - -Markers -------- - -``pytest.mark.asyncio`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Mark your test coroutine with this marker and pytest will execute it as an -asyncio task using the event loop provided by the ``event_loop`` fixture. See -the introductory section for an example. - -The event loop used can be overridden by overriding the ``event_loop`` fixture -(see above). - -In order to make your test code a little more concise, the pytest |pytestmark|_ -feature can be used to mark entire modules or classes with this marker. -Only test coroutines will be affected (by default, coroutines prefixed by -``test_``), so, for example, fixtures are safe to define. - -.. code-block:: python - - import asyncio - - import pytest - - # All test coroutines will be treated as marked. - pytestmark = pytest.mark.asyncio - - - async def test_example(event_loop): - """No marker!""" - await asyncio.sleep(0, loop=event_loop) - -In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added -automatically to *async* test functions. - - -.. |pytestmark| replace:: ``pytestmark`` -.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules - -Note about unittest -------------------- - -Test classes subclassing the standard `unittest `__ library are not supported, users -are recommended to use `unittest.IsolatedAsyncioTestCase `__ -or an async framework such as `asynctest `__. Contributing ------------ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /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 = source +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/make.bat b/docs/make.bat new file mode 100644 index 00000000..dc1312ab --- /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/source/concepts.rst b/docs/source/concepts.rst new file mode 100644 index 00000000..eb08bae6 --- /dev/null +++ b/docs/source/concepts.rst @@ -0,0 +1,38 @@ +======== +Concepts +======== + +asyncio event loops +=================== +pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via the ``event_loop`` fixture, which is automatically requested by all async tests. + +.. code-block:: python + + async def test_provided_loop_is_running_loop(event_loop): + assert event_loop is asyncio.get_running_loop() + +You can think of `event_loop` as an autouse fixture for async tests. + +Test discovery modes +==================== + +Pytest-asyncio provides two modes for test discovery, *strict* and *auto*. + + +Strict mode +----------- + +In strict mode pytest-asyncio will only run tests that have the *asyncio* marker and will only evaluate async fixtures decorated with ``@pytest_asyncio.fixture``. Test functions and fixtures without these markers and decorators will not be handled by pytest-asyncio. + +This mode is intended for projects that want so support multiple asynchronous programming libraries as it allows pytest-asyncio to coexist with other async testing plugins in the same codebase. + +Pytest automatically enables installed plugins. As a result pytest plugins need to coexist peacefully in their default configuration. This is why strict mode is the default mode. + +Auto mode +--------- + +In *auto* mode pytest-asyncio automatically adds the *asyncio* marker to all asynchronous test functions. It will also take ownership of all async fixtures, regardless of whether they are decorated with ``@pytest.fixture`` or ``@pytest_asyncio.fixture``. + +This mode is intended for projects that use *asyncio* as their only asynchronous programming library. Auto mode makes for the simplest test and fixture configuration and is the recommended default. + +If you intend to support multiple asynchronous programming libraries, e.g. *asyncio* and *trio*, strict mode will be the preferred option. diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..e7c54f2e --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,27 @@ +# 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 + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'pytest-asyncio' +copyright = '2022, pytest-asyncio contributors' +author = 'Tin Tvrtković' +release = 'v0.20.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ['_templates'] +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..153fe9e8 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,30 @@ +========================== +Welcome to pytest-asyncio! +========================== + +.. toctree:: + :maxdepth: 1 + :hidden: + + concepts + reference + support + +pytest-asyncio is a `pytest `_ plugin. It facilitates testing of code that uses the `asyncio `_ library. + +Specifically, pytest-asyncio provides support for coroutines as test functions. This allows users to *await* code inside their tests. For example, the following code is executed as a test item by pytest: + +.. code-block:: python + + @pytest.mark.asyncio + async def test_some_asyncio_code(): + res = await library.do_something() + assert b"expected result" == res + + +Note that test classes subclassing the standard `unittest `__ library are not supported. Users +are advised to use `unittest.IsolatedAsyncioTestCase `__ +or an async framework such as `asynctest `__. + + +pytest-asyncio is available under the `Apache License 2.0 `_. diff --git a/docs/source/reference.rst b/docs/source/reference.rst new file mode 100644 index 00000000..2fa77ff4 --- /dev/null +++ b/docs/source/reference.rst @@ -0,0 +1,145 @@ +========= +Reference +========= + +Configuration +============= + +The pytest-asyncio mode can be set by the ``asyncio_mode`` configuration option in the `configuration file +`_: + +.. code-block:: ini + + # pytest.ini + [pytest] + asyncio_mode = auto + +The value can also be set via the ``--asyncio-mode`` command-line option: + +.. code-block:: bash + + $ pytest tests --asyncio-mode=strict + + +If the asyncio mode is set in both the pytest configuration file and the command-line option, the command-line option takes precedence. If no asyncio mode is specified, the mode defaults to `strict`. + +Fixtures +======== + +``event_loop`` +-------------- +Creates a new asyncio event loop based on the current event loop policy. The new loop +is available as the return value of this fixture or via `asyncio.get_running_loop `__. +The event loop is closed when the fixture scope ends. The fixture scope defaults +to ``function`` scope. + +.. code-block:: python + + def test_http_client(event_loop): + url = "http://httpbin.org/get" + resp = event_loop.run_until_complete(http_client(url)) + assert b"HTTP/1.1 200 OK" in resp + +Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The `asyncio <#pytest-mark-asyncio>`__ marker +is used to mark coroutines that should be treated as test functions. + +The ``event_loop`` fixture can be overridden in any of the standard pytest locations, +e.g. directly in the test file, or in ``conftest.py``. This allows redefining the +fixture scope, for example: + +.. code-block:: python + + @pytest.fixture(scope="session") + def event_loop(): + policy = asyncio.get_event_loop_policy() + loop = policy.new_event_loop() + yield loop + loop.close() + +If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture. + +If the ``pytest.mark.asyncio`` decorator is applied to a test function, the ``event_loop`` +fixture will be requested automatically by the test function. + +``unused_tcp_port`` +------------------- +Finds and yields a single unused TCP port on the localhost interface. Useful for +binding temporary test servers. + +``unused_tcp_port_factory`` +--------------------------- +A callable which returns a different unused TCP port each invocation. Useful +when several unused TCP ports are required in a test. + +.. code-block:: python + + def a_test(unused_tcp_port_factory): + port1, port2 = unused_tcp_port_factory(), unused_tcp_port_factory() + ... + +``unused_udp_port`` and ``unused_udp_port_factory`` +--------------------------------------------------- +Works just like their TCP counterparts but returns unused UDP ports. + + +Markers +======= + +``pytest.mark.asyncio`` +----------------------- +A coroutine or async generator with this marker will be treated as a test function by pytest. The marked function will be executed as an +asyncio task in the event loop provided by the ``event_loop`` fixture. + +In order to make your test code a little more concise, the pytest |pytestmark|_ +feature can be used to mark entire modules or classes with this marker. +Only test coroutines will be affected (by default, coroutines prefixed by +``test_``), so, for example, fixtures are safe to define. + +.. code-block:: python + + import asyncio + + import pytest + + # All test coroutines will be treated as marked. + pytestmark = pytest.mark.asyncio + + + async def test_example(event_loop): + """No marker!""" + await asyncio.sleep(0, loop=event_loop) + +In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added +automatically to *async* test functions. + + +.. |pytestmark| replace:: ``pytestmark`` +.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules + + +Decorators +========== +Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``. + +.. code-block:: python3 + + import pytest_asyncio + + + @pytest_asyncio.fixture + async def async_gen_fixture(): + await asyncio.sleep(0.1) + yield "a value" + + + @pytest_asyncio.fixture(scope="module") + async def async_fixture(): + return await asyncio.sleep(0.1) + +All scopes are supported, but if you use a non-function scope you will need +to redefine the ``event_loop`` fixture to have the same or broader scope. +Async fixtures need the event loop, and so must have the same or narrower scope +than the ``event_loop`` fixture. + +*auto* mode automatically converts async fixtures declared with the +standard ``@pytest.fixture`` decorator to *asyncio-driven* versions. diff --git a/docs/source/support.rst b/docs/source/support.rst new file mode 100644 index 00000000..30981d94 --- /dev/null +++ b/docs/source/support.rst @@ -0,0 +1,21 @@ +=============== +Getting support +=============== + +Enterprise support +================== +`Tidelift `_ works with maintainers of numerous open source projects to ensure enterprise-grade support for your software supply chain. + +The Tidelift subscription includes security updates, verified license compliance, continuous software maintenance, and more. As a result, you get the guarantees provided by commercial software for the open source packages you use. + +Consider `signing up for the Tidelift subscription `__. + + +Direct maintainer support +========================= +If you require commercial support outside of the Tidelift subscription, reach out to `Michael Seifert, `__ one of the project's maintainers. + + +Community support +================= +The GitHub page of pytest-asyncio offers free community support on a best-effort basis. Please use the `issue tracker `__ to report bugs and the `discussions `__ to ask questions. diff --git a/setup.cfg b/setup.cfg index 08739b30..04ea3d90 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,9 @@ testing = flaky >= 3.5.0 mypy >= 0.931 pytest-trio >= 0.7.0 +docs = + sphinx >= 5.3 + sphinx-rtd-theme >= 1.0 [options.entry_points] pytest11 =