From dcebf1fe82067bbddeec24c97d11a6e4e0364063 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sat, 16 Oct 2021 01:54:12 +0200 Subject: [PATCH 01/39] Update CI badge --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7e6c975d..dd9459c2 100644 --- a/README.rst +++ b/README.rst @@ -3,10 +3,10 @@ pytest-asyncio: pytest support for asyncio .. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg :target: https://pypi.python.org/pypi/pytest-asyncio -.. image:: https://travis-ci.org/pytest-dev/pytest-asyncio.svg?branch=master - :target: https://travis-ci.org/pytest-dev/pytest-asyncio +.. image:: https://github.com/Tinche/cattrs/workflows/CI/badge.svg + :target: https://github.com/Tinche/cattrs/actions?workflow=CI .. image:: https://codecov.io/gh/pytest-dev/pytest-asyncio/branch/master/graph/badge.svg - :target: https://codecov.io/gh/pytest-dev/pytest-asyncio + :target: https://codecov.io/gh/pytest-dev/pytest-asyncio .. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg :target: https://github.com/pytest-dev/pytest-asyncio :alt: Supported Python versions From f21e0da345f877755b89ff87b6dcea70815b4497 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sat, 16 Oct 2021 01:55:22 +0200 Subject: [PATCH 02/39] Durr --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index dd9459c2..d3c155ee 100644 --- a/README.rst +++ b/README.rst @@ -3,8 +3,8 @@ pytest-asyncio: pytest support for asyncio .. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg :target: https://pypi.python.org/pypi/pytest-asyncio -.. image:: https://github.com/Tinche/cattrs/workflows/CI/badge.svg - :target: https://github.com/Tinche/cattrs/actions?workflow=CI +.. image:: https://github.com/pytest-dev/pytest-asyncio/workflows/CI/badge.svg + :target: https://github.com/pytest-dev/pytest-asyncio/actions?workflow=CI .. image:: https://codecov.io/gh/pytest-dev/pytest-asyncio/branch/master/graph/badge.svg :target: https://codecov.io/gh/pytest-dev/pytest-asyncio .. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg From b521f40787067cc42edf8dfafab2bb7870a8bfeb Mon Sep 17 00:00:00 2001 From: "Kian-Meng, Ang" Date: Wed, 29 Dec 2021 13:51:14 +0800 Subject: [PATCH 03/39] Fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d3c155ee..a4525476 100644 --- a/README.rst +++ b/README.rst @@ -139,7 +139,7 @@ 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 overriden by overriding the ``event_loop`` fixture +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|_ From dceeb686ce1e7b24761e21cf09951ec013066ccd Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 31 Dec 2021 19:57:19 +0300 Subject: [PATCH 04/39] Use `python@3.10` in CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b249ffb..96f26196 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - uses: "actions/checkout@v2" From a6758a1c55c71e2a229905ee1b2ba9d7e8010b69 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 5 Oct 2020 14:23:28 +0200 Subject: [PATCH 05/39] Teardown of the event_loop fixture no longer replaces the event loop policy. Signed-off-by: Michael Seifert --- README.rst | 4 ++++ pytest_asyncio/plugin.py | 7 +++++-- tests/respect_event_loop_policy/conftest.py | 16 ++++++++++++++++ .../test_respects_event_loop_policy.py | 16 ++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/respect_event_loop_policy/conftest.py create mode 100644 tests/respect_event_loop_policy/test_respects_event_loop_policy.py diff --git a/README.rst b/README.rst index a4525476..b1eaa0b1 100644 --- a/README.rst +++ b/README.rst @@ -164,6 +164,10 @@ Only test coroutines will be affected (by default, coroutines prefixed by Changelog --------- +0.17.0 (UNRELEASED) +~~~~~~~~~~~~~~~~~~~ +- `pytest-asyncio` no longer alters existing event loop policies. `#168 `_, `#188 `_ + 0.16.0 (2021-10-16) ~~~~~~~~~~~~~~~~~~~ - Add support for Python 3.10 diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 7665ff4d..4fbabfd1 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -80,8 +80,11 @@ def get_and_strip_from(self, name, data_dict): def pytest_fixture_post_finalizer(fixturedef, request): """Called after fixture teardown""" if fixturedef.argname == "event_loop": - # Set empty loop policy, so that subsequent get_event_loop() provides a new loop - asyncio.set_event_loop_policy(None) + policy = asyncio.get_event_loop_policy() + policy.get_event_loop().close() # Clean up existing loop to avoid ResourceWarnings + new_loop = policy.new_event_loop() # Replace existing event loop + # Ensure subsequent calls to get_event_loop() succeed + policy.set_event_loop(new_loop) @pytest.hookimpl(hookwrapper=True) diff --git a/tests/respect_event_loop_policy/conftest.py b/tests/respect_event_loop_policy/conftest.py new file mode 100644 index 00000000..2c5cef24 --- /dev/null +++ b/tests/respect_event_loop_policy/conftest.py @@ -0,0 +1,16 @@ +"""Defines and sets a custom event loop policy""" +import asyncio +from asyncio import DefaultEventLoopPolicy, SelectorEventLoop + + +class TestEventLoop(SelectorEventLoop): + pass + + +class TestEventLoopPolicy(DefaultEventLoopPolicy): + def new_event_loop(self): + return TestEventLoop() + + +# This statement represents a code which sets a custom event loop policy +asyncio.set_event_loop_policy(TestEventLoopPolicy()) diff --git a/tests/respect_event_loop_policy/test_respects_event_loop_policy.py b/tests/respect_event_loop_policy/test_respects_event_loop_policy.py new file mode 100644 index 00000000..2537ca24 --- /dev/null +++ b/tests/respect_event_loop_policy/test_respects_event_loop_policy.py @@ -0,0 +1,16 @@ +"""Tests that any externally provided event loop policy remains unaltered.""" +import asyncio + +import pytest + + +@pytest.mark.asyncio +async def test_uses_loop_provided_by_custom_policy(): + """Asserts that test cases use the event loop provided by the custom event loop policy""" + assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop" + + +@pytest.mark.asyncio +async def test_custom_policy_is_not_overwritten(): + """Asserts that any custom event loop policy stays the same across test cases""" + assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop" From 5079642c7de82ff8dfe5b1dcd1544017dfb6c196 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 5 Jan 2022 11:18:45 +0100 Subject: [PATCH 06/39] refactor: Removed use of obsolete transfer_markers during test collection phase. transfer_markers was removed in Pytest 4.1. The minimum required pytest version for pytest-asyncio is currently v5.4.0. That means that `transfer_markers` is always a no-op. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 4fbabfd1..70564ff8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -7,15 +7,6 @@ import pytest -try: - from _pytest.python import transfer_markers -except ImportError: # Pytest 4.1.0 removes the transfer_marker api (#104) - - def transfer_markers(*args, **kwargs): # noqa - """Noop when over pytest 4.1.0""" - pass - - from inspect import isasyncgenfunction @@ -39,13 +30,6 @@ def pytest_pycollect_makeitem(collector, name, obj): """A pytest hook to collect asyncio coroutines.""" if collector.funcnamefilter(name) and _is_coroutine(obj): item = pytest.Function.from_parent(collector, name=name) - - # Due to how pytest test collection works, module-level pytestmarks - # are applied after the collection step. Since this is the collection - # step, we look ourselves. - transfer_markers(obj, item.cls, item.module) - item = pytest.Function.from_parent(collector, name=name) # To reload keywords. - if "asyncio" in item.keywords: return list(collector._genfunctions(name, obj)) From f6fe76849c26a53f9702a3fadd3b5863c4656e72 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 2 Aug 2020 10:41:37 -0300 Subject: [PATCH 07/39] Add note about unittest.TestCase not being supported --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index b1eaa0b1..53a270ee 100644 --- a/README.rst +++ b/README.rst @@ -162,6 +162,12 @@ Only test coroutines will be affected (by default, coroutines prefixed by .. |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 one of the async frameworks available for that such as `asynctest `__. + Changelog --------- 0.17.0 (UNRELEASED) From f56021d8ac5eeb8187b06cd69d39e3b58733a274 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 2 Aug 2020 13:50:42 -0300 Subject: [PATCH 08/39] Mention IsolatedAsyncioTestCase --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 53a270ee..e15f3acf 100644 --- a/README.rst +++ b/README.rst @@ -166,7 +166,8 @@ Note about unittest ------------------- Test classes subclassing the standard `unittest `__ library are not supported, users -are recommended to use one of the async frameworks available for that such as `asynctest `__. +are recommended to use `unitest.IsolatedAsyncioTestCase `__ +or an async framework such as `asynctest `__. Changelog --------- From 7b2bcc000034b0850abc15877bce2007a2cfad34 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 6 Jan 2022 14:27:56 +0100 Subject: [PATCH 09/39] feat!: Drop support for Python 3.6. CPython 3.6 reached end-of-life on 2021-12-23 [1]. Future releases of pytest will no longer support Python 3.6 [2]. [1] https://www.python.org/dev/peps/pep-0494/ [2] https://github.com/pytest-dev/pytest/pull/9437 Signed-off-by: Michael Seifert --- .github/workflows/main.yml | 4 ++-- README.rst | 1 + setup.py | 3 +-- tox.ini | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 96f26196..b448cd6c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,11 +13,11 @@ jobs: name: "Python ${{ matrix.python-version }}" runs-on: "ubuntu-latest" env: - USING_COVERAGE: "3.6,3.7,3.8,3.9,3.10" + USING_COVERAGE: "3.7,3.8,3.9,3.10" strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: "actions/checkout@v2" diff --git a/README.rst b/README.rst index e15f3acf..f2d64987 100644 --- a/README.rst +++ b/README.rst @@ -174,6 +174,7 @@ Changelog 0.17.0 (UNRELEASED) ~~~~~~~~~~~~~~~~~~~ - `pytest-asyncio` no longer alters existing event loop policies. `#168 `_, `#188 `_ +- Drop support for Python 3.6 0.16.0 (2021-10-16) ~~~~~~~~~~~~~~~~~~~ diff --git a/setup.py b/setup.py index e15080fe..ad3877ca 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,6 @@ def find_version(): "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -41,7 +40,7 @@ def find_version(): "Topic :: Software Development :: Testing", "Framework :: Pytest", ], - python_requires=">= 3.6", + python_requires=">= 3.7", install_requires=["pytest >= 5.4.0"], extras_require={ "testing": [ diff --git a/tox.ini b/tox.ini index ef60cba0..edae7dec 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.14.0 -envlist = py36, py37, py38, py39, py310, lint +envlist = py37, py38, py39, py310, lint skip_missing_interpreters = true [testenv] @@ -26,7 +26,6 @@ commands = [gh-actions] python = - 3.6: py36 3.7: py37 3.8: py38 3.9: py39, lint From 9f59e28298d17dbf88b9ec23142316c63926aaa7 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 6 Jan 2022 17:36:24 +0200 Subject: [PATCH 10/39] Add 'Framework :: Asyncio' trove classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ad3877ca..684e315e 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ def find_version(): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Testing", + "Framework :: Asyncio", "Framework :: Pytest", ], python_requires=">= 3.7", From 57ccbc79f816313411227bdfeef08556a0468c76 Mon Sep 17 00:00:00 2001 From: wjsi Date: Sat, 10 Jul 2021 16:11:09 +0800 Subject: [PATCH 11/39] Support flaky on async tests --- .gitignore | 3 ++- pytest_asyncio/plugin.py | 7 +++++++ setup.py | 1 + tests/test_flaky_integration.py | 17 +++++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/test_flaky_integration.py diff --git a/.gitignore b/.gitignore index 09758085..447dbc4d 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,5 @@ docs/_build/ target/ .venv* -.idea \ No newline at end of file +.idea +.vscode \ No newline at end of file diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 70564ff8..461bbe5f 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -173,6 +173,12 @@ def wrap_in_sync(func, _loop): """Return a sync wrapper around an async function executing it in the current event loop.""" + # if the function is already wrapped, we rewrap using the original one + # not using __wrapped__ because the original function may already be + # a wrapped one + if hasattr(func, '_raw_test_func'): + func = func._raw_test_func + @functools.wraps(func) def inner(**kwargs): coro = func(**kwargs) @@ -188,6 +194,7 @@ def inner(**kwargs): task.exception() raise + inner._raw_test_func = func return inner diff --git a/setup.py b/setup.py index 684e315e..a0e4aa3e 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ def find_version(): "testing": [ "coverage", "hypothesis >= 5.7.1", + "flaky >= 3.5.0" ], }, entry_points={"pytest11": ["asyncio = pytest_asyncio.plugin"]}, diff --git a/tests/test_flaky_integration.py b/tests/test_flaky_integration.py new file mode 100644 index 00000000..4628c6a0 --- /dev/null +++ b/tests/test_flaky_integration.py @@ -0,0 +1,17 @@ +"""Tests for the Flaky integration, which retries failed tests. +""" +import asyncio + +import flaky +import pytest + +_threshold = -1 + + +@flaky.flaky(3, 2) +@pytest.mark.asyncio +async def test_asyncio_flaky_thing_that_fails_then_succeeds(): + global _threshold + await asyncio.sleep(0.1) + _threshold += 1 + assert _threshold != 1 From 4ec2b8f944472e66807b831c796429c76184a9d7 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 7 Jan 2022 12:56:43 +0200 Subject: [PATCH 12/39] Reformat with black --- pytest_asyncio/plugin.py | 2 +- setup.py | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 461bbe5f..81ac2e3f 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -176,7 +176,7 @@ def wrap_in_sync(func, _loop): # if the function is already wrapped, we rewrap using the original one # not using __wrapped__ because the original function may already be # a wrapped one - if hasattr(func, '_raw_test_func'): + if hasattr(func, "_raw_test_func"): func = func._raw_test_func @functools.wraps(func) diff --git a/setup.py b/setup.py index a0e4aa3e..1cd1415c 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,9 @@ def find_version(): version_file = ( - Path(__file__) - .parent.joinpath("pytest_asyncio", "__init__.py") - .read_text() - ) - version_match = re.search( - r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M + Path(__file__).parent.joinpath("pytest_asyncio", "__init__.py").read_text() ) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) @@ -44,11 +40,7 @@ def find_version(): python_requires=">= 3.7", install_requires=["pytest >= 5.4.0"], extras_require={ - "testing": [ - "coverage", - "hypothesis >= 5.7.1", - "flaky >= 3.5.0" - ], + "testing": ["coverage", "hypothesis >= 5.7.1", "flaky >= 3.5.0"], }, entry_points={"pytest11": ["asyncio = pytest_asyncio.plugin"]}, ) From ff497f7ad6e4e866f0d19c2d02317d47cb85bf14 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 6 Jan 2022 18:39:02 +0100 Subject: [PATCH 13/39] refactor: Moved test_hypothesis_integration to "hypothesis" subfolder and renamed it to "test_base". This allows us to add more tests for the Hypothesis integration while at the same time keep different tests tidy by using different files. Signed-off-by: Michael Seifert --- tests/{test_hypothesis_integration.py => hypothesis/test_base.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_hypothesis_integration.py => hypothesis/test_base.py} (100%) diff --git a/tests/test_hypothesis_integration.py b/tests/hypothesis/test_base.py similarity index 100% rename from tests/test_hypothesis_integration.py rename to tests/hypothesis/test_base.py From 7fea8635b00aedd4e42e3c855f71cc6eedf16159 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 6 Jan 2022 18:56:00 +0100 Subject: [PATCH 14/39] fix: Fixed double wrapping of inherited Hypothesis tests. Pytest-asyncio identifies Hypothesis test cases by their `is_hypothesis_test` flag. When setting up an async Hypothesis test pytest-asyncio replaces the function's `hypothesis.inner_test` attribute. The the top level function never changes. When a Hypothesis test case is defined in a base class and inherited by subclasses, the test is collected in each subclass. Since the top-level Hypothesis test never changes, its inner test will be wrapped multiple times. Double wrapping leads to execution errors caused by stale (closed) event loops in all test executions after the first. This change adds an `original_test_function` attribute to the async function wrapper, in order to keep track of the original Hypothesis test. When re-wrapping would occur in subclasses pytest-asyncio wraps the original test function rather than the wrapper function. Closes #231 Signed-off-by: Michael Seifert --- README.rst | 1 + pytest_asyncio/plugin.py | 2 +- tests/hypothesis/test_inherited_test.py | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/hypothesis/test_inherited_test.py diff --git a/README.rst b/README.rst index f2d64987..0faac438 100644 --- a/README.rst +++ b/README.rst @@ -175,6 +175,7 @@ Changelog ~~~~~~~~~~~~~~~~~~~ - `pytest-asyncio` no longer alters existing event loop policies. `#168 `_, `#188 `_ - Drop support for Python 3.6 +- Fixed an issue when pytest-asyncio was used in combination with `flaky` or inherited asynchronous Hypothesis tests. `#178 `_ `#231 `_ 0.16.0 (2021-10-16) ~~~~~~~~~~~~~~~~~~~ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 461bbe5f..81ac2e3f 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -176,7 +176,7 @@ def wrap_in_sync(func, _loop): # if the function is already wrapped, we rewrap using the original one # not using __wrapped__ because the original function may already be # a wrapped one - if hasattr(func, '_raw_test_func'): + if hasattr(func, "_raw_test_func"): func = func._raw_test_func @functools.wraps(func) diff --git a/tests/hypothesis/test_inherited_test.py b/tests/hypothesis/test_inherited_test.py new file mode 100644 index 00000000..86e92efd --- /dev/null +++ b/tests/hypothesis/test_inherited_test.py @@ -0,0 +1,22 @@ +import hypothesis.strategies as st +from hypothesis import given +import pytest + + +class BaseClass: + @pytest.mark.asyncio + @given(value=st.integers()) + async def test_hypothesis(self, value: int) -> None: + assert True + + +class TestOne(BaseClass): + """During the first execution the Hypothesis test is wrapped in a synchronous function.""" + + pass + + +class TestTwo(BaseClass): + """Execute the test a second time to ensure that the test receives a fresh event loop.""" + + pass From 1af571ed83bf86a15884fd8ba4962435a7d53040 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 6 Jan 2022 19:49:04 +0100 Subject: [PATCH 15/39] doc: Updated docstring of pytest_pyfunc_call. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 81ac2e3f..634d1fb7 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -153,8 +153,9 @@ async def setup(): @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): """ - Run asyncio marked test functions in an event loop instead of a normal - function call. + Pytest hook called before a test case is run. + + Wraps marked tests in a synchronous function where the wrapped test coroutine is executed in an event loop. """ if "asyncio" in pyfuncitem.keywords: if getattr(pyfuncitem.obj, "is_hypothesis_test", False): From 430d69af4bb3d3413e9f796e77011843c2299cc7 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 7 Jan 2022 08:38:03 +0100 Subject: [PATCH 16/39] doc: Mentioned additional test dependency on flaky in the changelog. This is an important information for downstream packagers. Signed-off-by: Michael Seifert --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 0faac438..4d264ce2 100644 --- a/README.rst +++ b/README.rst @@ -176,6 +176,7 @@ Changelog - `pytest-asyncio` no longer alters existing event loop policies. `#168 `_, `#188 `_ - Drop support for Python 3.6 - Fixed an issue when pytest-asyncio was used in combination with `flaky` or inherited asynchronous Hypothesis tests. `#178 `_ `#231 `_ +- Added `flaky `_ to test dependencies 0.16.0 (2021-10-16) ~~~~~~~~~~~~~~~~~~~ From d48569eee5703a845ce189ed6cee10d003cb2660 Mon Sep 17 00:00:00 2001 From: "Dominik S. Buse" Date: Fri, 7 Jan 2022 13:25:14 +0100 Subject: [PATCH 17/39] Add unused port helpers for UDP (#99) * Add unused port helpers for UDP Extends the unused_tcp_port and unused_tcp_port_factory mechanisms for UDP ports. * Update pytest_asyncio/plugin.py * Add changenote Co-authored-by: Andrew Svetlov --- README.rst | 8 +++- pytest_asyncio/plugin.py | 36 ++++++++++++--- tests/test_simple.py | 96 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 4d264ce2..7330e1a9 100644 --- a/README.rst +++ b/README.rst @@ -109,6 +109,11 @@ when several unused TCP ports are required in a test. 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 coroutines or asynchronous generators. @@ -166,7 +171,7 @@ Note about unittest ------------------- Test classes subclassing the standard `unittest `__ library are not supported, users -are recommended to use `unitest.IsolatedAsyncioTestCase `__ +are recommended to use `unitest.IsolatedAsyncioTestCase `__ or an async framework such as `asynctest `__. Changelog @@ -177,6 +182,7 @@ Changelog - Drop support for Python 3.6 - Fixed an issue when pytest-asyncio was used in combination with `flaky` or inherited asynchronous Hypothesis tests. `#178 `_ `#231 `_ - Added `flaky `_ to test dependencies +- Added ``unused_udp_port`` and ``unused_udp_port_factory`` fixtures (similar to ``unused_tcp_port`` and ``unused_tcp_port_factory`` counterparts. `#99 `_ 0.16.0 (2021-10-16) ~~~~~~~~~~~~~~~~~~~ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 634d1fb7..dcaf429b 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -224,16 +224,21 @@ def event_loop(request): loop.close() -def _unused_tcp_port(): - """Find an unused localhost TCP port from 1024-65535 and return it.""" - with contextlib.closing(socket.socket()) as sock: +def _unused_port(socket_type): + """Find an unused localhost port from 1024-65535 and return it.""" + with contextlib.closing(socket.socket(type=socket_type)) as sock: sock.bind(("127.0.0.1", 0)) return sock.getsockname()[1] @pytest.fixture def unused_tcp_port(): - return _unused_tcp_port() + return _unused_port(socket.SOCK_STREAM) + + +@pytest.fixture +def unused_udp_port(): + return _unused_port(socket.SOCK_DGRAM) @pytest.fixture(scope="session") @@ -243,10 +248,29 @@ def unused_tcp_port_factory(): def factory(): """Return an unused port.""" - port = _unused_tcp_port() + port = _unused_port(socket.SOCK_STREAM) + + while port in produced: + port = _unused_port(socket.SOCK_STREAM) + + produced.add(port) + + return port + + return factory + + +@pytest.fixture(scope="session") +def unused_udp_port_factory(): + """A factory function, producing different unused UDP ports.""" + produced = set() + + def factory(): + """Return an unused port.""" + port = _unused_port(socket.SOCK_DGRAM) while port in produced: - port = _unused_tcp_port() + port = _unused_port(socket.SOCK_DGRAM) produced.add(port) diff --git a/tests/test_simple.py b/tests/test_simple.py index 854faaf3..42151852 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -51,6 +51,33 @@ async def closer(_, writer): await server1.wait_closed() +@pytest.mark.asyncio +async def test_unused_udp_port_fixture(unused_udp_port, event_loop): + """Test the unused TCP port fixture.""" + + class Closer: + def connection_made(self, transport): + pass + + def connection_lost(self, *arg, **kwd): + pass + + transport1, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", unused_udp_port), + reuse_port=False, + ) + + with pytest.raises(IOError): + await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", unused_udp_port), + reuse_port=False, + ) + + transport1.abort() + + @pytest.mark.asyncio async def test_unused_port_factory_fixture(unused_tcp_port_factory, event_loop): """Test the unused TCP port factory fixture.""" @@ -80,11 +107,57 @@ async def closer(_, writer): await server3.wait_closed() +@pytest.mark.asyncio +async def test_unused_udp_port_factory_fixture(unused_udp_port_factory, event_loop): + """Test the unused UDP port factory fixture.""" + + class Closer: + def connection_made(self, transport): + pass + + def connection_lost(self, *arg, **kwd): + pass + + port1, port2, port3 = ( + unused_udp_port_factory(), + unused_udp_port_factory(), + unused_udp_port_factory(), + ) + + transport1, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port1), + reuse_port=False, + ) + transport2, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port2), + reuse_port=False, + ) + transport3, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port3), + reuse_port=False, + ) + + for port in port1, port2, port3: + with pytest.raises(IOError): + await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port), + reuse_port=False, + ) + + transport1.abort() + transport2.abort() + transport3.abort() + + def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch): """Test correct avoidance of duplicate ports.""" counter = 0 - def mock_unused_tcp_port(): + def mock_unused_tcp_port(_ignored): """Force some duplicate ports.""" nonlocal counter counter += 1 @@ -93,12 +166,31 @@ def mock_unused_tcp_port(): else: return 10000 + counter - monkeypatch.setattr(pytest_asyncio.plugin, "_unused_tcp_port", mock_unused_tcp_port) + monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_tcp_port) assert unused_tcp_port_factory() == 10000 assert unused_tcp_port_factory() > 10000 +def test_unused_udp_port_factory_duplicate(unused_udp_port_factory, monkeypatch): + """Test correct avoidance of duplicate UDP ports.""" + counter = 0 + + def mock_unused_udp_port(_ignored): + """Force some duplicate ports.""" + nonlocal counter + counter += 1 + if counter < 5: + return 10000 + else: + return 10000 + counter + + monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_udp_port) + + assert unused_udp_port_factory() == 10000 + assert unused_udp_port_factory() > 10000 + + class TestMarkerInClassBasedTests: """Test that asyncio marked functions work for methods of test classes.""" From 82261678994cd251d8392508299cb0f75c136951 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sat, 8 Jan 2022 00:10:31 +0200 Subject: [PATCH 18/39] Fix readme, the plugin now provide fixtures for unused udp ports also (#241) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7330e1a9..bfa9785a 100644 --- a/README.rst +++ b/README.rst @@ -35,7 +35,7 @@ Features -------- - fixtures for creating and injecting versions of the asyncio event loop -- fixtures for injecting unused tcp ports +- 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 From f86d9004f14789f24f60a86ecd15e1906e8276c6 Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Sat, 8 Jan 2022 10:41:41 -0700 Subject: [PATCH 19/39] Add mechanism for explicit marking of fixtures which should be run with asyncio (#125) --- README.rst | 82 ++++++++++++++++- pytest_asyncio/__init__.py | 5 ++ pytest_asyncio/plugin.py | 152 ++++++++++++++++++++++++++++++-- setup.cfg | 1 + tests/modes/test_auto_mode.py | 91 +++++++++++++++++++ tests/modes/test_legacy_mode.py | 115 ++++++++++++++++++++++++ tests/modes/test_strict_mode.py | 70 +++++++++++++++ tests/test_asyncio_fixture.py | 39 ++++++++ 8 files changed, 547 insertions(+), 8 deletions(-) create mode 100644 tests/modes/test_auto_mode.py create mode 100644 tests/modes/test_legacy_mode.py create mode 100644 tests/modes/test_strict_mode.py create mode 100644 tests/test_asyncio_fixture.py diff --git a/README.rst b/README.rst index bfa9785a..acaa99b5 100644 --- a/README.rst +++ b/README.rst @@ -39,6 +39,9 @@ Features - 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 ------------ @@ -51,6 +54,70 @@ To install pytest-asyncio, simply: This is enough for pytest to pick up pytest-asyncio. +Modes +----- + +Starting from ``pytest-asyncio>=0.17``, three modes are provided: *auto*, *strict* and +*legacy* (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 overriden 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; plase 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. + + +Legacy mode +~~~~~~~~~~~ + +This mode follows rules used by ``pytest-asyncio<0.17``: tests are not auto-marked but +fixtures are. + +This mode is used by default for the sake of backward compatibility, deprecation +warnings are emitted with suggestion to either switching to ``auto`` mode or using +``strict`` mode with ``@pytest_asyncio.fixture`` decorators. + +In future, the default will be changed. + + Fixtures -------- @@ -116,16 +183,18 @@ 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 coroutines or asynchronous generators. +Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``. .. code-block:: python3 - @pytest.fixture + import pytest_asyncio + + @pytest_asyncio.fixture async def async_gen_fixture(): await asyncio.sleep(0.1) yield 'a value' - @pytest.fixture(scope='module') + @pytest_asyncio.fixture(scope='module') async def async_fixture(): return await asyncio.sleep(0.1) @@ -134,6 +203,9 @@ 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* and *legacy* mode automatically converts async fixtures declared with the +standard ``@pytest.fixture`` decorator to *asyncio-driven* versions. + Markers ------- @@ -164,6 +236,10 @@ Only test coroutines will be affected (by default, coroutines prefixed by """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 diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py index b16159e7..0da62156 100644 --- a/pytest_asyncio/__init__.py +++ b/pytest_asyncio/__init__.py @@ -1,2 +1,7 @@ """The main point for importing pytest-asyncio items.""" __version__ = "0.16.0" + +from .plugin import fixture + + +__all__ = ("fixture",) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index dcaf429b..44165602 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -1,13 +1,88 @@ """pytest-asyncio implementation.""" import asyncio import contextlib +import enum import functools import inspect import socket +import sys +import warnings import pytest -from inspect import isasyncgenfunction + +class Mode(str, enum.Enum): + AUTO = "auto" + STRICT = "strict" + LEGACY = "legacy" + + +LEGACY_MODE = pytest.PytestDeprecationWarning( + "The 'asyncio_mode' default value will change to 'strict' in future, " + "please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' " + "in pytest configuration file." +) + +LEGACY_ASYNCIO_FIXTURE = ( + "'@pytest.fixture' is applied to {name} " + "in 'legacy' mode, " + "please replace it with '@pytest_asyncio.fixture' as a preparation " + "for switching to 'strict' mode (or use 'auto' mode to seamlessly handle " + "all these fixtures as asyncio-driven)." +) + + +ASYNCIO_MODE_HELP = """\ +'auto' - for automatically handling all async functions by the plugin +'strict' - for autoprocessing disabling (useful if different async frameworks \ +should be tested together, e.g. \ +both pytest-asyncio and pytest-trio are used in the same project) +'legacy' - for keeping compatibility with pytest-asyncio<0.17: \ +auto-handling is disabled but pytest_asyncio.fixture usage is not enforced +""" + + +def pytest_addoption(parser, pluginmanager): + group = parser.getgroup("asyncio") + group.addoption( + "--asyncio-mode", + dest="asyncio_mode", + default=None, + metavar="MODE", + help=ASYNCIO_MODE_HELP, + ) + parser.addini( + "asyncio_mode", + help="default value for --asyncio-mode", + type="string", + default="legacy", + ) + + +def fixture(fixture_function=None, **kwargs): + if fixture_function is not None: + _set_explicit_asyncio_mark(fixture_function) + return pytest.fixture(fixture_function, **kwargs) + + else: + + @functools.wraps(fixture) + def inner(fixture_function): + return fixture(fixture_function, **kwargs) + + return inner + + +def _has_explicit_asyncio_mark(obj): + obj = getattr(obj, "__func__", obj) # instance method maybe? + return getattr(obj, "_force_asyncio_fixture", False) + + +def _set_explicit_asyncio_mark(obj): + if hasattr(obj, "__func__"): + # instance method, check the function object + obj = obj.__func__ + obj._force_asyncio_fixture = True def _is_coroutine(obj): @@ -15,6 +90,17 @@ def _is_coroutine(obj): return asyncio.iscoroutinefunction(obj) or inspect.isgeneratorfunction(obj) +def _is_coroutine_or_asyncgen(obj): + return _is_coroutine(obj) or inspect.isasyncgenfunction(obj) + + +def _get_asyncio_mode(config): + val = config.getoption("asyncio_mode") + if val is None: + val = config.getini("asyncio_mode") + return Mode(val) + + def pytest_configure(config): """Inject documentation.""" config.addinivalue_line( @@ -23,6 +109,22 @@ def pytest_configure(config): "mark the test as a coroutine, it will be " "run using an asyncio event loop", ) + if _get_asyncio_mode(config) == Mode.LEGACY: + _issue_warning_captured(LEGACY_MODE, config.hook, stacklevel=1) + + +def _issue_warning_captured(warning, hook, *, stacklevel=1): + # copy-paste of pytest internal _pytest.warnings._issue_warning_captured function + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always", type(warning)) + warnings.warn(LEGACY_MODE, stacklevel=stacklevel) + frame = sys._getframe(stacklevel - 1) + location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + hook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=records[0], when="config", nodeid="", location=location + ) + ) @pytest.mark.tryfirst @@ -32,6 +134,13 @@ def pytest_pycollect_makeitem(collector, name, obj): item = pytest.Function.from_parent(collector, name=name) if "asyncio" in item.keywords: return list(collector._genfunctions(name, obj)) + else: + if _get_asyncio_mode(item.config) == Mode.AUTO: + # implicitly add asyncio marker if asyncio mode is on + ret = list(collector._genfunctions(name, obj)) + for elem in ret: + elem.add_marker("asyncio") + return ret class FixtureStripper: @@ -88,9 +197,42 @@ def pytest_fixture_setup(fixturedef, request): policy.set_event_loop(loop) return - if isasyncgenfunction(fixturedef.func): + func = fixturedef.func + if not _is_coroutine_or_asyncgen(func): + # Nothing to do with a regular fixture function + yield + return + + config = request.node.config + asyncio_mode = _get_asyncio_mode(config) + + if not _has_explicit_asyncio_mark(func): + if asyncio_mode == Mode.AUTO: + # Enforce asyncio mode if 'auto' + _set_explicit_asyncio_mark(func) + elif asyncio_mode == Mode.LEGACY: + _set_explicit_asyncio_mark(func) + try: + code = func.__code__ + except AttributeError: + code = func.__func__.__code__ + name = ( + f"" + ) + warnings.warn( + LEGACY_ASYNCIO_FIXTURE.format(name=name), + pytest.PytestDeprecationWarning, + ) + else: + # asyncio_mode is STRICT, + # don't handle fixtures that are not explicitly marked + yield + return + + if inspect.isasyncgenfunction(func): # This is an async generator function. Wrap it accordingly. - generator = fixturedef.func + generator = func fixture_stripper = FixtureStripper(fixturedef) fixture_stripper.add(FixtureStripper.EVENT_LOOP) @@ -129,8 +271,8 @@ async def async_finalizer(): return loop.run_until_complete(setup()) fixturedef.func = wrapper - elif inspect.iscoroutinefunction(fixturedef.func): - coro = fixturedef.func + elif inspect.iscoroutinefunction(func): + coro = func fixture_stripper = FixtureStripper(fixturedef) fixture_stripper.add(FixtureStripper.EVENT_LOOP) diff --git a/setup.cfg b/setup.cfg index 01610865..fc18e3d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,7 @@ show_missing = true [tool:pytest] addopts = -rsx --tb=short testpaths = tests +asyncio_mode = auto filterwarnings = error [metadata] diff --git a/tests/modes/test_auto_mode.py b/tests/modes/test_auto_mode.py new file mode 100644 index 00000000..980b0b04 --- /dev/null +++ b/tests/modes/test_auto_mode.py @@ -0,0 +1,91 @@ +from textwrap import dedent + +pytest_plugins = "pytester" + + +def test_auto_mode_cmdline(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) + + +def test_auto_mode_cfg(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + pytester.makefile(".ini", pytest="[pytest]\nasyncio_mode = auto\n") + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_auto_mode_async_fixture(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.fixture + async def fixture_a(): + await asyncio.sleep(0) + return 1 + + async def test_a(fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) + + +def test_auto_mode_method_fixture(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + + class TestA: + + @pytest.fixture + async def fixture_a(self): + await asyncio.sleep(0) + return 1 + + async def test_a(self, fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) diff --git a/tests/modes/test_legacy_mode.py b/tests/modes/test_legacy_mode.py new file mode 100644 index 00000000..df9c2cb6 --- /dev/null +++ b/tests/modes/test_legacy_mode.py @@ -0,0 +1,115 @@ +from textwrap import dedent + +pytest_plugins = "pytester" + + +LEGACY_MODE = ( + "The 'asyncio_mode' default value will change to 'strict' in future, " + "please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' " + "in pytest configuration file." +) + +LEGACY_ASYNCIO_FIXTURE = ( + "'@pytest.fixture' is applied to {name} " + "in 'legacy' mode, " + "please replace it with '@pytest_asyncio.fixture' as a preparation " + "for switching to 'strict' mode (or use 'auto' mode to seamlessly handle " + "all these fixtures as asyncio-driven)." +).format(name="*") + + +def test_warning_for_legacy_mode_cmdline(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=legacy") + assert result.parseoutcomes()["warnings"] == 1 + result.stdout.fnmatch_lines(["*" + LEGACY_MODE + "*"]) + + +def test_warning_for_legacy_mode_cfg(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + pytester.makefile(".ini", pytest="[pytest]\nasyncio_mode = legacy\n") + result = pytester.runpytest() + assert result.parseoutcomes()["warnings"] == 1 + result.stdout.fnmatch_lines(["*" + LEGACY_MODE + "*"]) + result.stdout.no_fnmatch_line("*" + LEGACY_ASYNCIO_FIXTURE + "*") + + +def test_warning_for_legacy_fixture(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.fixture + async def fixture_a(): + await asyncio.sleep(0) + return 1 + + @pytest.mark.asyncio + async def test_a(fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=legacy") + assert result.parseoutcomes()["warnings"] == 2 + result.stdout.fnmatch_lines(["*" + LEGACY_ASYNCIO_FIXTURE + "*"]) + + +def test_warning_for_legacy_method_fixture(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + + class TestA: + + @pytest.fixture + async def fixture_a(self): + await asyncio.sleep(0) + return 1 + + @pytest.mark.asyncio + async def test_a(self, fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=legacy") + assert result.parseoutcomes()["warnings"] == 2 + result.stdout.fnmatch_lines(["*" + LEGACY_ASYNCIO_FIXTURE + "*"]) diff --git a/tests/modes/test_strict_mode.py b/tests/modes/test_strict_mode.py new file mode 100644 index 00000000..7b574012 --- /dev/null +++ b/tests/modes/test_strict_mode.py @@ -0,0 +1,70 @@ +from textwrap import dedent + +pytest_plugins = "pytester" + + +def test_strict_mode_cmdline(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_strict_mode_cfg(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + pytester.makefile(".ini", pytest="[pytest]\nasyncio_mode = strict\n") + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_strict_mode_method_fixture(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + import pytest_asyncio + + pytest_plugins = 'pytest_asyncio' + + class TestA: + + @pytest_asyncio.fixture + async def fixture_a(self): + await asyncio.sleep(0) + return 1 + + @pytest.mark.asyncio + async def test_a(self, fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) diff --git a/tests/test_asyncio_fixture.py b/tests/test_asyncio_fixture.py new file mode 100644 index 00000000..824956d8 --- /dev/null +++ b/tests/test_asyncio_fixture.py @@ -0,0 +1,39 @@ +import asyncio +import pytest_asyncio +import pytest + + +@pytest_asyncio.fixture +async def fixture_bare(): + await asyncio.sleep(0) + return 1 + + +@pytest.mark.asyncio +async def test_bare_fixture(fixture_bare): + await asyncio.sleep(0) + assert fixture_bare == 1 + + +@pytest_asyncio.fixture(name="new_fixture_name") +async def fixture_with_name(request): + await asyncio.sleep(0) + return request.fixturename + + +@pytest.mark.asyncio +async def test_fixture_with_name(new_fixture_name): + await asyncio.sleep(0) + assert new_fixture_name == "new_fixture_name" + + +@pytest_asyncio.fixture(params=[2, 4]) +async def fixture_with_params(request): + await asyncio.sleep(0) + return request.param + + +@pytest.mark.asyncio +async def test_fixture_with_params(fixture_with_params): + await asyncio.sleep(0) + assert fixture_with_params % 2 == 0 From 47ae437236630080788f7290c524d8ffd22e0492 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 9 Jan 2022 08:37:52 +0200 Subject: [PATCH 20/39] Ignote .python-version file marker --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 447dbc4d..247e4de7 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,7 @@ target/ .venv* .idea -.vscode \ No newline at end of file +.vscode + +# pyenv +.python-version From b785c98f04d4164739cf8e172488a992a5e9dc6c Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 9 Jan 2022 15:02:20 +0200 Subject: [PATCH 21/39] Update changelog for added plugin modes (#243) --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index acaa99b5..c4c3a5b1 100644 --- a/README.rst +++ b/README.rst @@ -259,6 +259,7 @@ Changelog - Fixed an issue when pytest-asyncio was used in combination with `flaky` or inherited asynchronous Hypothesis tests. `#178 `_ `#231 `_ - Added `flaky `_ to test dependencies - Added ``unused_udp_port`` and ``unused_udp_port_factory`` fixtures (similar to ``unused_tcp_port`` and ``unused_tcp_port_factory`` counterparts. `#99 `_ +- Added the plugin modes: *strict*, *auto*, and *legacy*. See `documentation `_ for details. `#125 `_ 0.16.0 (2021-10-16) ~~~~~~~~~~~~~~~~~~~ From 0a143c4356eb121e7ad44fa1f1fbe1a52e7e3a33 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 9 Jan 2022 15:02:47 +0200 Subject: [PATCH 22/39] Avoid non-stantdard approached for warning emitting (#242) --- pytest_asyncio/plugin.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 44165602..813a638e 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -5,7 +5,6 @@ import functools import inspect import socket -import sys import warnings import pytest @@ -17,7 +16,7 @@ class Mode(str, enum.Enum): LEGACY = "legacy" -LEGACY_MODE = pytest.PytestDeprecationWarning( +LEGACY_MODE = DeprecationWarning( "The 'asyncio_mode' default value will change to 'strict' in future, " "please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' " "in pytest configuration file." @@ -110,21 +109,7 @@ def pytest_configure(config): "run using an asyncio event loop", ) if _get_asyncio_mode(config) == Mode.LEGACY: - _issue_warning_captured(LEGACY_MODE, config.hook, stacklevel=1) - - -def _issue_warning_captured(warning, hook, *, stacklevel=1): - # copy-paste of pytest internal _pytest.warnings._issue_warning_captured function - with warnings.catch_warnings(record=True) as records: - warnings.simplefilter("always", type(warning)) - warnings.warn(LEGACY_MODE, stacklevel=stacklevel) - frame = sys._getframe(stacklevel - 1) - location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name - hook.pytest_warning_recorded.call_historic( - kwargs=dict( - warning_message=records[0], when="config", nodeid="", location=location - ) - ) + config.issue_config_time_warning(LEGACY_MODE, stacklevel=2) @pytest.mark.tryfirst @@ -222,7 +207,7 @@ def pytest_fixture_setup(fixturedef, request): ) warnings.warn( LEGACY_ASYNCIO_FIXTURE.format(name=name), - pytest.PytestDeprecationWarning, + DeprecationWarning, ) else: # asyncio_mode is STRICT, From 5d4954a5743c984af70c4e164c7316974f4cc97d Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 9 Jan 2022 15:03:01 +0200 Subject: [PATCH 23/39] Rewrite flaky integration test (#246) to don't pollute the output with flaky restart warnings --- tests/test_flaky_integration.py | 52 ++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/tests/test_flaky_integration.py b/tests/test_flaky_integration.py index 4628c6a0..2e551aad 100644 --- a/tests/test_flaky_integration.py +++ b/tests/test_flaky_integration.py @@ -1,17 +1,47 @@ """Tests for the Flaky integration, which retries failed tests. """ -import asyncio -import flaky -import pytest -_threshold = -1 +from textwrap import dedent +pytest_plugins = "pytester" -@flaky.flaky(3, 2) -@pytest.mark.asyncio -async def test_asyncio_flaky_thing_that_fails_then_succeeds(): - global _threshold - await asyncio.sleep(0.1) - _threshold += 1 - assert _threshold != 1 + +def test_auto_mode_cmdline(pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import flaky + import pytest + + _threshold = -1 + + @flaky.flaky(3, 2) + @pytest.mark.asyncio + async def test_asyncio_flaky_thing_that_fails_then_succeeds(): + global _threshold + await asyncio.sleep(0.1) + _threshold += 1 + assert _threshold != 1 + """ + ) + ) + # runpytest_subprocess() is required to don't pollute the output + # with flaky restart information + result = pytester.runpytest_subprocess() + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + [ + "===Flaky Test Report===", + "test_asyncio_flaky_thing_that_fails_then_succeeds passed 1 " + "out of the required 2 times. Running test again until it passes 2 times.", + "test_asyncio_flaky_thing_that_fails_then_succeeds failed " + "(1 runs remaining out of 3).", + " ", + " assert 1 != 1", + "test_asyncio_flaky_thing_that_fails_then_succeeds passed 2 " + "out of the required 2 times. Success!", + "===End Flaky Test Report===", + ] + ) From 4c7da65d6fcf9d725eccba28ad1ed2083524ee16 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 9 Jan 2022 15:03:32 +0200 Subject: [PATCH 24/39] Setup pre-commit hooks and reformat code (#245) --- .pre-commit-config.yaml | 39 +++++++++++++++++++ LICENSE | 1 - Makefile | 14 +++++-- README.rst | 14 ++++--- pytest_asyncio/__init__.py | 1 - pytest_asyncio/plugin.py | 6 ++- setup.cfg | 2 +- setup.py | 2 +- .../test_async_fixtures_with_finalizer.py | 3 +- tests/hypothesis/test_base.py | 1 - tests/hypothesis/test_inherited_test.py | 14 +++---- tests/multiloop/conftest.py | 2 - .../test_respects_event_loop_policy.py | 3 +- tests/sessionloop/conftest.py | 2 - tests/test_asyncio_fixture.py | 4 +- tests/test_dependent_fixtures.py | 1 + tests/test_simple.py | 9 +++-- tests/test_subprocess.py | 1 - tox.ini | 3 +- 19 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..cf368171 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v4.1.0' + hooks: + - id: check-merge-conflict + exclude: "rst$" +- repo: https://github.com/asottile/yesqa + rev: v1.3.0 + hooks: + - id: yesqa +- repo: https://github.com/Zac-HD/shed + rev: 0.6.0 # 0.7 does not support Python 3.7 + hooks: + - id: shed + args: + - --refactor + - --py37-plus + types_or: + - python + - markdown + - rst +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: fix-encoding-pragma + args: [--remove] + - id: check-yaml + - id: debug-statements +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + language_version: python3 +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-use-type-annotations diff --git a/LICENSE b/LICENSE index e06d2081..5c304d1a 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,3 @@ Apache License WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/Makefile b/Makefile index 8cf88841..fa6d6c0d 100644 --- a/Makefile +++ b/Makefile @@ -20,9 +20,17 @@ clean-test: ## remove test and coverage artifacts rm -f .coverage rm -fr htmlcov/ -lint: ## check style with flake8 - flake8 pytest_asyncio tests - black --check --verbose pytest_asyncio tests +lint: +# CI env-var is set by GitHub actions +ifdef CI + pre-commit run --all-files --show-diff-on-failure +else + pre-commit run --all-files +endif test: pytest tests + +install: + pip install -U pre-commit + pre-commit install diff --git a/README.rst b/README.rst index c4c3a5b1..0b35000b 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ provides useful fixtures and markers to make testing easier. @pytest.mark.asyncio async def test_some_asyncio_code(): res = await library.do_something() - assert b'expected result' == res + assert b"expected result" == res pytest-asyncio has been strongly influenced by pytest-tornado_. @@ -139,9 +139,9 @@ Use ``pytest.mark.asyncio`` for this purpose. .. code-block:: python def test_http_client(event_loop): - url = 'http://httpbin.org/get' + url = "http://httpbin.org/get" resp = event_loop.run_until_complete(http_client(url)) - assert b'HTTP/1.1 200 OK' in resp + assert b"HTTP/1.1 200 OK" in resp This fixture can be easily overridden in any of the standard pytest locations (e.g. directly in the test file, or in ``conftest.py``) to use a non-default @@ -189,12 +189,14 @@ Asynchronous fixtures are defined just like ordinary pytest fixtures, except the import pytest_asyncio + @pytest_asyncio.fixture async def async_gen_fixture(): await asyncio.sleep(0.1) - yield 'a value' + yield "a value" + - @pytest_asyncio.fixture(scope='module') + @pytest_asyncio.fixture(scope="module") async def async_fixture(): return await asyncio.sleep(0.1) @@ -227,11 +229,13 @@ Only test coroutines will be affected (by default, coroutines prefixed by .. 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) diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py index 0da62156..2a50727f 100644 --- a/pytest_asyncio/__init__.py +++ b/pytest_asyncio/__init__.py @@ -3,5 +3,4 @@ from .plugin import fixture - __all__ = ("fixture",) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 813a638e..13d7c685 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -159,7 +159,8 @@ def pytest_fixture_post_finalizer(fixturedef, request): """Called after fixture teardown""" if fixturedef.argname == "event_loop": policy = asyncio.get_event_loop_policy() - policy.get_event_loop().close() # Clean up existing loop to avoid ResourceWarnings + # Clean up existing loop to avoid ResourceWarnings + policy.get_event_loop().close() new_loop = policy.new_event_loop() # Replace existing event loop # Ensure subsequent calls to get_event_loop() succeed policy.set_event_loop(new_loop) @@ -282,7 +283,8 @@ def pytest_pyfunc_call(pyfuncitem): """ Pytest hook called before a test case is run. - Wraps marked tests in a synchronous function where the wrapped test coroutine is executed in an event loop. + Wraps marked tests in a synchronous function + where the wrapped test coroutine is executed in an event loop. """ if "asyncio" in pyfuncitem.keywords: if getattr(pyfuncitem.obj, "is_hypothesis_test", False): diff --git a/setup.cfg b/setup.cfg index fc18e3d9..e2804822 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,4 +15,4 @@ filterwarnings = error license_file = LICENSE [flake8] -ignore = E203, E501, W503 +max-line-length = 88 diff --git a/setup.py b/setup.py index 1cd1415c..4b331753 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import re from pathlib import Path -from setuptools import setup, find_packages +from setuptools import find_packages, setup def find_version(): diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py index c90a0124..2e72d5de 100644 --- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py +++ b/tests/async_fixtures/test_async_fixtures_with_finalizer.py @@ -46,7 +46,8 @@ def port_finalizer(finalizer): async def port_afinalizer(): # await task using current loop retrieved from the event loop policy # RuntimeError is raised if task is created on a different loop. - # This can happen when pytest_fixture_setup does not set up the loop correctly, + # This can happen when pytest_fixture_setup + # does not set up the loop correctly, # for example when policy.set_event_loop() is called with a wrong argument await finalizer diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py index 39cb6075..e9273d0e 100644 --- a/tests/hypothesis/test_base.py +++ b/tests/hypothesis/test_base.py @@ -4,7 +4,6 @@ import asyncio import pytest - from hypothesis import given, strategies as st diff --git a/tests/hypothesis/test_inherited_test.py b/tests/hypothesis/test_inherited_test.py index 86e92efd..a7762264 100644 --- a/tests/hypothesis/test_inherited_test.py +++ b/tests/hypothesis/test_inherited_test.py @@ -1,22 +1,20 @@ import hypothesis.strategies as st -from hypothesis import given import pytest +from hypothesis import given class BaseClass: @pytest.mark.asyncio @given(value=st.integers()) async def test_hypothesis(self, value: int) -> None: - assert True + pass class TestOne(BaseClass): - """During the first execution the Hypothesis test is wrapped in a synchronous function.""" - - pass + """During the first execution the Hypothesis test + is wrapped in a synchronous function.""" class TestTwo(BaseClass): - """Execute the test a second time to ensure that the test receives a fresh event loop.""" - - pass + """Execute the test a second time to ensure that + the test receives a fresh event loop.""" diff --git a/tests/multiloop/conftest.py b/tests/multiloop/conftest.py index 9c74a509..ebcb627a 100644 --- a/tests/multiloop/conftest.py +++ b/tests/multiloop/conftest.py @@ -6,8 +6,6 @@ class CustomSelectorLoop(asyncio.SelectorEventLoop): """A subclass with no overrides, just to test for presence.""" - pass - @pytest.fixture def event_loop(): diff --git a/tests/respect_event_loop_policy/test_respects_event_loop_policy.py b/tests/respect_event_loop_policy/test_respects_event_loop_policy.py index 2537ca24..610b3388 100644 --- a/tests/respect_event_loop_policy/test_respects_event_loop_policy.py +++ b/tests/respect_event_loop_policy/test_respects_event_loop_policy.py @@ -6,7 +6,8 @@ @pytest.mark.asyncio async def test_uses_loop_provided_by_custom_policy(): - """Asserts that test cases use the event loop provided by the custom event loop policy""" + """Asserts that test cases use the event loop + provided by the custom event loop policy""" assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop" diff --git a/tests/sessionloop/conftest.py b/tests/sessionloop/conftest.py index 6c657688..bb6c1d6c 100644 --- a/tests/sessionloop/conftest.py +++ b/tests/sessionloop/conftest.py @@ -6,8 +6,6 @@ class CustomSelectorLoopSession(asyncio.SelectorEventLoop): """A subclass with no overrides, just to test for presence.""" - pass - loop = CustomSelectorLoopSession() diff --git a/tests/test_asyncio_fixture.py b/tests/test_asyncio_fixture.py index 824956d8..cfe10479 100644 --- a/tests/test_asyncio_fixture.py +++ b/tests/test_asyncio_fixture.py @@ -1,7 +1,9 @@ import asyncio -import pytest_asyncio + import pytest +import pytest_asyncio + @pytest_asyncio.fixture async def fixture_bare(): diff --git a/tests/test_dependent_fixtures.py b/tests/test_dependent_fixtures.py index 2876255b..dc70fe9c 100644 --- a/tests/test_dependent_fixtures.py +++ b/tests/test_dependent_fixtures.py @@ -1,4 +1,5 @@ import asyncio + import pytest diff --git a/tests/test_simple.py b/tests/test_simple.py index 42151852..31204b6c 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -2,6 +2,7 @@ import asyncio import pytest + import pytest_asyncio.plugin @@ -26,7 +27,7 @@ async def test_asyncio_marker(): @pytest.mark.xfail(reason="need a failure", strict=True) @pytest.mark.asyncio def test_asyncio_marker_fail(): - assert False + raise AssertionError @pytest.mark.asyncio @@ -196,13 +197,15 @@ class TestMarkerInClassBasedTests: @pytest.mark.asyncio async def test_asyncio_marker_with_explicit_loop_fixture(self, event_loop): - """Test the "asyncio" marker works on a method in a class-based test with explicit loop fixture.""" + """Test the "asyncio" marker works on a method in + a class-based test with explicit loop fixture.""" ret = await async_coro() assert ret == "ok" @pytest.mark.asyncio async def test_asyncio_marker_with_implicit_loop_fixture(self): - """Test the "asyncio" marker works on a method in a class-based test with implicit loop fixture.""" + """Test the "asyncio" marker works on a method in + a class-based test with implicit loop fixture.""" ret = await async_coro() assert ret == "ok" diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 88ea29ab..311d67d5 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -1,5 +1,4 @@ """Tests for using subprocesses in tests.""" -import asyncio import asyncio.subprocess import sys diff --git a/tox.ini b/tox.ini index edae7dec..7d551eca 100644 --- a/tox.ini +++ b/tox.ini @@ -12,8 +12,7 @@ skip_install = true basepython = python3.9 extras = tests deps = - flake8 - black + pre-commit commands = make lint From 4176c3d0d916c5e4830a423587f0396f7995da54 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 9 Jan 2022 15:48:47 +0200 Subject: [PATCH 25/39] Setup initial codeowners (#244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Andrew Svetlov, Michael Seifert, and Tin Tvrtković are added as reviewers for all pull requests --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..a30293ca --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @asvetlov @seifertm @Tinche From 41420cfc3b0335e256ce5bd7a08dcf03b21b5282 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 11 Jan 2022 12:30:02 +0200 Subject: [PATCH 26/39] Switch to declarative setup (#247) * Switch to declarative setup * Use __main__ guard * Add build-backend * Pin versions --- pyproject.toml | 6 ++++++ setup.cfg | 52 ++++++++++++++++++++++++++++++++++++++++++++++---- setup.py | 48 +++------------------------------------------- 3 files changed, 57 insertions(+), 49 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..31531b9e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=51.0", + "wheel>=0.36", +] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index e2804822..83fdce45 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,51 @@ +[metadata] +name = pytest-asyncio +version = attr: pytest_asyncio.__version__ +url = https://github.com/pytest-dev/pytest-asyncio +project_urls = + GitHub = https://github.com/pytest-dev/pytest-asyncio +description = Pytest support for asyncio +long_description = file: README.rst +long_description_content_type = text/x-rst +author = Tin Tvrtković +author_email = tinchester@gmail.com +license = Apache 2.0 +license_file = LICENSE +classifiers = + Development Status :: 4 - Beta + + Intended Audience :: Developers + + License :: OSI Approved :: Apache Software License + + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + + Topic :: Software Development :: Testing + + Framework :: Asyncio + Framework :: Pytest + +[options] +python_requires = >=3.7 +packages = find: +include_package_data = True + +install_requires = + pytest >= 5.4.0 + +[options.extras_require] +testing = + coverage + hypothesis >= 5.7.1 + flaky >= 3.5.0 + +[options.entry_points] +pytest11 = + asyncio = pytest_asyncio.plugin + [coverage:run] source = pytest_asyncio @@ -10,9 +58,5 @@ testpaths = tests asyncio_mode = auto filterwarnings = error -[metadata] -# ensure LICENSE is included in wheel metadata -license_file = LICENSE - [flake8] max-line-length = 88 diff --git a/setup.py b/setup.py index 4b331753..7f1a1763 100644 --- a/setup.py +++ b/setup.py @@ -1,46 +1,4 @@ -import re -from pathlib import Path +from setuptools import setup -from setuptools import find_packages, setup - - -def find_version(): - version_file = ( - Path(__file__).parent.joinpath("pytest_asyncio", "__init__.py").read_text() - ) - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) - if version_match: - return version_match.group(1) - - raise RuntimeError("Unable to find version string.") - - -setup( - name="pytest-asyncio", - version=find_version(), - packages=find_packages(), - url="https://github.com/pytest-dev/pytest-asyncio", - license="Apache 2.0", - author="Tin Tvrtković", - author_email="tinchester@gmail.com", - description="Pytest support for asyncio.", - long_description=Path(__file__).parent.joinpath("README.rst").read_text(), - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Topic :: Software Development :: Testing", - "Framework :: Asyncio", - "Framework :: Pytest", - ], - python_requires=">= 3.7", - install_requires=["pytest >= 5.4.0"], - extras_require={ - "testing": ["coverage", "hypothesis >= 5.7.1", "flaky >= 3.5.0"], - }, - entry_points={"pytest11": ["asyncio = pytest_asyncio.plugin"]}, -) +if __name__ == "__main__": + setup() From 775da951cc30298a26e4d7db062a18d201021ecc Mon Sep 17 00:00:00 2001 From: kriek Date: Tue, 11 Jan 2022 14:58:23 +0100 Subject: [PATCH 27/39] Fixes pytest-dev/pytest-asyncio#219 (#221) Co-authored-by: Andrew Svetlov --- README.rst | 1 + pytest_asyncio/plugin.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0b35000b..59d2ae9c 100644 --- a/README.rst +++ b/README.rst @@ -264,6 +264,7 @@ Changelog - Added `flaky `_ to test dependencies - Added ``unused_udp_port`` and ``unused_udp_port_factory`` fixtures (similar to ``unused_tcp_port`` and ``unused_tcp_port_factory`` counterparts. `#99 `_ - Added the plugin modes: *strict*, *auto*, and *legacy*. See `documentation `_ for details. `#125 `_ +- Correctly process ``LeyboardInterrupt`` during async fixture setup phase `#219 `_ 0.16.0 (2021-10-16) ~~~~~~~~~~~~~~~~~~~ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 13d7c685..49157de5 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -253,8 +253,9 @@ async def async_finalizer(): loop.run_until_complete(async_finalizer()) + result = loop.run_until_complete(setup()) request.addfinalizer(finalizer) - return loop.run_until_complete(setup()) + return result fixturedef.func = wrapper elif inspect.iscoroutinefunction(func): From f0c20dc485800834bc49a1721ec9d59edbbbc995 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 11 Jan 2022 16:02:01 +0200 Subject: [PATCH 28/39] Fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 59d2ae9c..42f867db 100644 --- a/README.rst +++ b/README.rst @@ -264,7 +264,7 @@ Changelog - Added `flaky `_ to test dependencies - Added ``unused_udp_port`` and ``unused_udp_port_factory`` fixtures (similar to ``unused_tcp_port`` and ``unused_tcp_port_factory`` counterparts. `#99 `_ - Added the plugin modes: *strict*, *auto*, and *legacy*. See `documentation `_ for details. `#125 `_ -- Correctly process ``LeyboardInterrupt`` during async fixture setup phase `#219 `_ +- Correctly process ``KeyboardInterrupt`` during async fixture setup phase `#219 `_ 0.16.0 (2021-10-16) ~~~~~~~~~~~~~~~~~~~ From 37ec756edf0c93b272cfbbcbd485bcb495b0958c Mon Sep 17 00:00:00 2001 From: Imran Hayder Date: Tue, 11 Jan 2022 14:27:47 -0600 Subject: [PATCH 29/39] Switch to setuptools-scm for versioning (#37) * Switch to setuptools-scm for versioning Following other pytest projects like pytest-html, that use setuptool-scm to manage version, it would be nice other projects follow the suit and let setuptools-scm manage version of this project. * Update .gitignore * ci: Install dependency setuptools_scm. Signed-off-by: Michael Seifert * Drop explicit version reference from setup.cfg * Remove setuptools_scm and wheel from `pip install --upgrade` of the test job Co-authored-by: Andrew Svetlov * Replace `setuptools_scm` and `wheel` with `build`in package job Co-authored-by: Andrew Svetlov * Change the packaging command to use `build` Co-authored-by: Andrew Svetlov * build: Added missing dependency declaration on setuptools_scm to setup.cfg. This prevents tools relying on setuptools from picking up the dependency correctly. Signed-off-by: Michael Seifert * build: Added version in metadata. Signed-off-by: Michael Seifert Co-authored-by: maliki Co-authored-by: Bruno Oliveira Co-authored-by: Michael Seifert Co-authored-by: Andrew Svetlov --- .github/workflows/main.yml | 6 +++--- .gitignore | 6 +++++- pyproject.toml | 4 ++++ pytest_asyncio/__init__.py | 3 +-- setup.cfg | 3 +++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b448cd6c..3a3e4e44 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: set -xe python -VV python -m site - python -m pip install --upgrade pip wheel + python -m pip install --upgrade pip python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions - name: "Run tox targets for ${{ matrix.python-version }}" run: "python -m tox" @@ -57,9 +57,9 @@ jobs: python-version: "3.9" - name: "Install poetry, check-wheel-content, and twine" - run: "python -m pip install wheel twine check-wheel-contents" + run: "python -m pip install build check-wheel-contents twine" - name: "Build package" - run: "python setup.py sdist bdist_wheel" + run: "python -m build" - name: "List result" run: "ls -l dist" - name: "Check wheel contents" diff --git a/.gitignore b/.gitignore index 247e4de7..7dd9b771 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,7 @@ htmlcov/ .tox/ .coverage .coverage.* -.cache +.pytest_cache nosetests.xml coverage.xml *,cover @@ -63,3 +63,7 @@ target/ # pyenv .python-version + + +# generated by setuptools_scm +pytest_asyncio/_version.py diff --git a/pyproject.toml b/pyproject.toml index 31531b9e..189ffa1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,5 +2,9 @@ requires = [ "setuptools>=51.0", "wheel>=0.36", + "setuptools_scm>=6.2" ] build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "pytest_asyncio/_version.py" diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py index 2a50727f..1bc2811d 100644 --- a/pytest_asyncio/__init__.py +++ b/pytest_asyncio/__init__.py @@ -1,6 +1,5 @@ """The main point for importing pytest-asyncio items.""" -__version__ = "0.16.0" - +from ._version import version as __version__ # noqa from .plugin import fixture __all__ = ("fixture",) diff --git a/setup.cfg b/setup.cfg index 83fdce45..7c3d8a9c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,9 @@ python_requires = >=3.7 packages = find: include_package_data = True +setup_requires = + setuptools_scm >= 6.2 + install_requires = pytest >= 5.4.0 From 2f523bad98ee1256c26ffe94c78ebcd8a1d03688 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 12 Jan 2022 08:58:58 +0200 Subject: [PATCH 30/39] Configure dependabot version updater (#250) --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..10c63edc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: master +- package-ecosystem: github-actions + directory: / + schedule: + interval: daily + open-pull-requests-limit: 10 From d28b826b8acb329401cceed286ef3b42dc82df05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jan 2022 09:36:15 +0200 Subject: [PATCH 31/39] Bump codecov/codecov-action from 1 to 2.1.0 (#251) * Bump codecov/codecov-action from 1 to 2.1.0 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 2.1.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1...v2.1.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Tune coverage report Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrew Svetlov --- .github/workflows/main.yml | 25 +++++++++++++++---------- Makefile | 4 +++- setup.cfg | 4 +++- tox.ini | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a3e4e44..784ea5e2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: jobs: - tests: + test: name: "Python ${{ matrix.python-version }}" runs-on: "ubuntu-latest" env: @@ -34,17 +34,22 @@ jobs: - name: "Run tox targets for ${{ matrix.python-version }}" run: "python -m tox" - # We always use a modern Python version for combining coverage to prevent - # parsing errors in older versions for modern code. - - uses: "actions/setup-python@v2" - with: - python-version: "3.9" + - name: Prepare coverage artifact + if: ${{ contains(env.USING_COVERAGE, matrix.python-version) }} + uses: aio-libs/prepare-coverage@v21.9.1 - - name: "Upload coverage to Codecov" - if: "contains(env.USING_COVERAGE, matrix.python-version)" - uses: "codecov/codecov-action@v1" + check: + name: Check + if: always() + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 with: - fail_ci_if_error: true + jobs: ${{ toJSON(needs) }} + - name: Upload coverage + uses: aio-libs/upload-coverage@v21.9.4 package: name: "Build & verify package" diff --git a/Makefile b/Makefile index fa6d6c0d..0817a0e7 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,9 @@ else endif test: - pytest tests + coverage run -m pytest tests + coverage xml + coverage report install: pip install -U pre-commit diff --git a/setup.cfg b/setup.cfg index 7c3d8a9c..d78f17d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,7 @@ install_requires = [options.extras_require] testing = - coverage + coverage==6.2 hypothesis >= 5.7.1 flaky >= 3.5.0 @@ -51,6 +51,7 @@ pytest11 = [coverage:run] source = pytest_asyncio +branch = true [coverage:report] show_missing = true @@ -59,6 +60,7 @@ show_missing = true addopts = -rsx --tb=short testpaths = tests asyncio_mode = auto +junit_family=xunit2 filterwarnings = error [flake8] diff --git a/tox.ini b/tox.ini index 7d551eca..dc09b8c5 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skip_missing_interpreters = true [testenv] extras = testing -commands = coverage run -m pytest {posargs} +commands = make test [testenv:lint] skip_install = true From 2eb12a7b0591dfb8578303235d87bb25ddeedf77 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 12 Jan 2022 18:31:16 +0200 Subject: [PATCH 32/39] Setup GitHub Workflows linter and yaml-reformatter (#253) --- .github/dependabot.yml | 23 +++---- .github/workflows/main.yml | 120 ++++++++++++++++++------------------- .pre-commit-config.yaml | 103 +++++++++++++++++++------------ 3 files changed, 137 insertions(+), 109 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 10c63edc..c99eadff 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,14 @@ +--- version: 2 updates: -- package-ecosystem: pip - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 - target-branch: master -- package-ecosystem: github-actions - directory: / - schedule: - interval: daily - open-pull-requests-limit: 10 + - package-ecosystem: pip + directory: / + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: master + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 784ea5e2..12aafa0d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,72 +2,72 @@ name: CI on: - push: - branches: ["master"] - pull_request: - branches: ["master"] - workflow_dispatch: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: jobs: - test: - name: "Python ${{ matrix.python-version }}" - runs-on: "ubuntu-latest" - env: - USING_COVERAGE: "3.7,3.8,3.9,3.10" + test: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + env: + USING_COVERAGE: 3.7,3.8,3.9,3.10 - strategy: - matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + strategy: + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10'] - steps: - - uses: "actions/checkout@v2" - - uses: "actions/setup-python@v2" - with: - python-version: "${{ matrix.python-version }}" - - name: "Install dependencies" - run: | - set -xe - python -VV - python -m site - python -m pip install --upgrade pip - python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions - - name: "Run tox targets for ${{ matrix.python-version }}" - run: "python -m tox" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + set -xe + python -VV + python -m site + python -m pip install --upgrade pip + python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions + - name: Run tox targets for ${{ matrix.python-version }} + run: python -m tox - - name: Prepare coverage artifact - if: ${{ contains(env.USING_COVERAGE, matrix.python-version) }} - uses: aio-libs/prepare-coverage@v21.9.1 + - name: Prepare coverage artifact + if: ${{ contains(env.USING_COVERAGE, matrix.python-version) }} + uses: aio-libs/prepare-coverage@v21.9.1 - check: - name: Check - if: always() - needs: [test] - runs-on: ubuntu-latest - steps: - - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@release/v1 - with: - jobs: ${{ toJSON(needs) }} - - name: Upload coverage - uses: aio-libs/upload-coverage@v21.9.4 + check: + name: Check + if: always() + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + - name: Upload coverage + uses: aio-libs/upload-coverage@v21.9.4 - package: - name: "Build & verify package" - runs-on: "ubuntu-latest" + package: + name: Build & verify package + runs-on: ubuntu-latest - steps: - - uses: "actions/checkout@v2" - - uses: "actions/setup-python@v2" - with: - python-version: "3.9" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.9' - - name: "Install poetry, check-wheel-content, and twine" - run: "python -m pip install build check-wheel-contents twine" - - name: "Build package" - run: "python -m build" - - name: "List result" - run: "ls -l dist" - - name: "Check wheel contents" - run: "check-wheel-contents dist/*.whl" - - name: "Check long_description" - run: "python -m twine check dist/*" + - name: Install poetry, check-wheel-content, and twine + run: python -m pip install build check-wheel-contents twine + - name: Build package + run: python -m build + - name: List result + run: ls -l dist + - name: Check wheel contents + run: check-wheel-contents dist/*.whl + - name: Check long_description + run: python -m twine check dist/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf368171..a085f108 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,39 +1,66 @@ +--- repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: 'v4.1.0' - hooks: - - id: check-merge-conflict - exclude: "rst$" -- repo: https://github.com/asottile/yesqa - rev: v1.3.0 - hooks: - - id: yesqa -- repo: https://github.com/Zac-HD/shed - rev: 0.6.0 # 0.7 does not support Python 3.7 - hooks: - - id: shed - args: - - --refactor - - --py37-plus - types_or: - - python - - markdown - - rst -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: fix-encoding-pragma - args: [--remove] - - id: check-yaml - - id: debug-statements -- repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 - language_version: python3 -- repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 - hooks: - - id: python-use-type-annotations + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-merge-conflict + exclude: rst$ + - repo: https://github.com/asottile/yesqa + rev: v1.3.0 + hooks: + - id: yesqa + - repo: https://github.com/Zac-HD/shed + rev: 0.6.0 # 0.7 does not support Python 3.7 + hooks: + - id: shed + args: + - --refactor + - --py37-plus + types_or: + - python + - markdown + - rst + - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt + rev: 0.1.0 + hooks: + - id: yamlfmt + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: fix-encoding-pragma + args: [--remove] + - id: check-case-conflict + - id: check-json + - id: check-xml + - id: check-yaml + - id: debug-statements + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + language_version: python3 + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-use-type-annotations + - repo: https://github.com/rhysd/actionlint + rev: v1.6.8 + hooks: + - id: actionlint-docker + args: + - -ignore + - 'SC2155:' + - -ignore + - 'SC2086:' + - -ignore + - 'SC1004:' + - repo: https://github.com/sirosen/check-jsonschema + rev: 0.9.1 + hooks: + - id: check-github-actions +ci: + skip: + - actionlint-docker + - check-github-actions From cd8498709d5cf763c7a73cac9b43a0bd2a2d4fb7 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 13 Jan 2022 10:57:59 +0200 Subject: [PATCH 33/39] Release process automation (#252) --- .github/actionlint-matcher.json | 17 +++++++ .github/workflows/main.yml | 88 ++++++++++++++++++++++++++------- tools/get-version.py | 17 +++++++ tox.ini | 18 +++++-- 4 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 .github/actionlint-matcher.json create mode 100644 tools/get-version.py diff --git a/.github/actionlint-matcher.json b/.github/actionlint-matcher.json new file mode 100644 index 00000000..a99709f7 --- /dev/null +++ b/.github/actionlint-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "actionlint", + "pattern": [ + { + "code": 5, + "column": 3, + "file": 1, + "line": 2, + "message": 4, + "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$" + } + ] + } + ] +} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 12aafa0d..186b31b5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,6 +9,43 @@ on: workflow_dispatch: jobs: + lint: + name: Run linters + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + prerelease: ${{ steps.version.outputs.prerelease }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install GitHub matcher for ActionLint checker + run: | + echo "::add-matcher::.github/actionlint-matcher.json" + - name: Install check-wheel-content, and twine + run: python -m pip install build check-wheel-contents tox twine + - name: Build package + run: python -m build + - name: Run tox for linter + run: python -m tox -e lint + - name: List result + run: ls -l dist + - name: Check wheel contents + run: check-wheel-contents dist/*.whl + - name: Check long_description + run: python -m twine check dist/* + - name: Get version info + id: version + run: tox -e version-info + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: dist + path: dist + test: name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest @@ -21,6 +58,8 @@ jobs: steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -41,7 +80,7 @@ jobs: check: name: Check if: always() - needs: [test] + needs: [lint, test] runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed @@ -51,23 +90,36 @@ jobs: - name: Upload coverage uses: aio-libs/upload-coverage@v21.9.4 - package: - name: Build & verify package + deploy: + name: Deploy + environment: release + # Run only on pushing a tag + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + needs: [lint, check] runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - name: Checkout + uses: actions/checkout@v2.4.0 with: - python-version: '3.9' - - - name: Install poetry, check-wheel-content, and twine - run: python -m pip install build check-wheel-contents twine - - name: Build package - run: python -m build - - name: List result - run: ls -l dist - - name: Check wheel contents - run: check-wheel-contents dist/*.whl - - name: Check long_description - run: python -m twine check dist/* + fetch-depth: 0 + - name: Download distributions + uses: actions/download-artifact@v2 + with: + name: dist + path: dist + - name: Collected dists + run: | + tree dist + - name: PyPI upload + uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + packages_dir: dist + password: ${{ secrets.PYPI_API_TOKEN }} + - name: GitHub Release + uses: ncipollo/release-action@v1 + with: + name: pytest-asyncio ${{ needs.lint.outputs.version }} + artifacts: dist + bodyFile: README.rst + prerelease: ${{ needs.lint.outputs.prerelease }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/tools/get-version.py b/tools/get-version.py new file mode 100644 index 00000000..e988a32c --- /dev/null +++ b/tools/get-version.py @@ -0,0 +1,17 @@ +import json +import sys +from importlib import metadata + +from packaging.version import parse as parse_version + + +def main(): + version_string = metadata.version("pytest-asyncio") + version = parse_version(version_string) + print(f"::set-output name=version::{version}") + prerelease = json.dumps(version.is_prerelease) + print(f"::set-output name=prerelease::{prerelease}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tox.ini b/tox.ini index dc09b8c5..0092b03e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,25 @@ [tox] minversion = 3.14.0 -envlist = py37, py38, py39, py310, lint +envlist = py37, py38, py39, py310, lint, version-info skip_missing_interpreters = true +passenv = + CI [testenv] extras = testing commands = make test +allowlist_externals = + make [testenv:lint] skip_install = true basepython = python3.9 -extras = tests deps = - pre-commit + pre-commit == 2.16.0 commands = make lint +allowlist_externals = + make [testenv:coverage-report] deps = coverage @@ -23,6 +28,13 @@ commands = coverage combine coverage report +[testenv:version-info] +basepython = python3.9 +deps = + packaging == 21.3 +commands = + python ./tools/get-version.py + [gh-actions] python = 3.7: py37 From 8ccb30650068f58d6f3e7314d2c1f2d59cdba1e9 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 13 Jan 2022 10:59:27 +0200 Subject: [PATCH 34/39] Build on tag --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 186b31b5..c07b3b40 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,7 @@ name: CI on: push: branches: [master] + tags: [v*] pull_request: branches: [master] workflow_dispatch: From 696cf7d5e0a458825c27ae8c7d621fb538c70827 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 13 Jan 2022 11:19:59 +0200 Subject: [PATCH 35/39] Fix trove classifier for asyncio --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d78f17d5..7be86f36 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ classifiers = Topic :: Software Development :: Testing - Framework :: Asyncio + Framework :: AsyncIO Framework :: Pytest [options] From 141937b89aa6ced9856ed3f997818ef8e3fbea57 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 13 Jan 2022 11:41:15 +0200 Subject: [PATCH 36/39] Fix release artifacts --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c07b3b40..c2c986ec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -120,7 +120,7 @@ jobs: uses: ncipollo/release-action@v1 with: name: pytest-asyncio ${{ needs.lint.outputs.version }} - artifacts: dist + artifacts: dist/* bodyFile: README.rst prerelease: ${{ needs.lint.outputs.prerelease }} token: ${{ secrets.GITHUB_TOKEN }} From d291c666870d2d903fc99543dccd77dca8496d5b Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 13 Jan 2022 12:06:56 +0200 Subject: [PATCH 37/39] Convert README.rst to Markdown for making githun release --- .github/workflows/main.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c2c986ec..7ead9bbb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -99,6 +99,9 @@ jobs: needs: [lint, check] runs-on: ubuntu-latest steps: + - name: Install pandoc + run: | + apt install pandoc - name: Checkout uses: actions/checkout@v2.4.0 with: @@ -111,6 +114,9 @@ jobs: - name: Collected dists run: | tree dist + - name: Convert README.rst to Markdown + run: | + pandoc -s -o README.md README.rst - name: PyPI upload uses: pypa/gh-action-pypi-publish@v1.4.2 with: @@ -121,6 +127,6 @@ jobs: with: name: pytest-asyncio ${{ needs.lint.outputs.version }} artifacts: dist/* - bodyFile: README.rst + bodyFile: README.md prerelease: ${{ needs.lint.outputs.prerelease }} token: ${{ secrets.GITHUB_TOKEN }} From 90436c98dd27e670f0c721b48bb28001ccdcbbda Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 13 Jan 2022 12:28:49 +0200 Subject: [PATCH 38/39] Fix pandoc installation procedure --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ead9bbb..1a0c9031 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -101,7 +101,7 @@ jobs: steps: - name: Install pandoc run: | - apt install pandoc + sudo apt-get install -y pandoc - name: Checkout uses: actions/checkout@v2.4.0 with: From 2e2d5d202ecfea6fd403d92a9dd6ab6734aa0f85 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 13 Jan 2022 12:50:41 +0200 Subject: [PATCH 39/39] Bump to 0.17 release --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 42f867db..c63f72d2 100644 --- a/README.rst +++ b/README.rst @@ -256,7 +256,7 @@ or an async framework such as `asynctest `_, `#188 `_ - Drop support for Python 3.6