diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f435f11..620a65cc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,7 +62,7 @@ jobs: strategy: matrix: os: [ubuntu, windows] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', 3.13.0-beta.3] steps: - uses: actions/checkout@v4 @@ -149,7 +149,7 @@ jobs: run: | pandoc -s -o README.md README.rst - name: PyPI upload - uses: pypa/gh-action-pypi-publish@v1.8.14 + uses: pypa/gh-action-pypi-publish@v1.9.0 with: packages-dir: dist password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee649b19..f83159c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,22 +52,10 @@ repos: rev: v1.9.0 hooks: - id: python-use-type-annotations -- repo: https://github.com/rhysd/actionlint - rev: v1.6.22 - hooks: - - id: actionlint-docker - args: - - -ignore - - 'SC2155:' - - -ignore - - 'SC2086:' - - -ignore - - 'SC1004:' - repo: https://github.com/sirosen/check-jsonschema rev: 0.19.2 hooks: - id: check-github-actions ci: skip: - - actionlint-docker - check-github-actions diff --git a/SECURITY.rst b/SECURITY.rst new file mode 100644 index 00000000..cebe85ad --- /dev/null +++ b/SECURITY.rst @@ -0,0 +1,4 @@ +Security contact information +============================ + +To report a security vulnerability, please use the `Tidelift security contact. `__ Tidelift will coordinate the fix and disclosure. diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 209bbcd2..2ccf269e 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,11 +1,11 @@ attrs==23.2.0 -coverage==7.5.1 -exceptiongroup==1.2.1 -hypothesis==6.102.0 +coverage==7.6.0 +exceptiongroup==1.2.2 +hypothesis==6.108.2 iniconfig==2.0.0 -packaging==24.0 +packaging==24.1 pluggy==1.5.0 -pytest==8.2.0 +pytest==8.2.2 sortedcontainers==2.4.0 tomli==2.0.1 -typing_extensions==4.11.0 +typing_extensions==4.12.2 diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 966b8034..4c187a1e 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -1,15 +1,15 @@ alabaster==0.7.16 Babel==2.15.0 -certifi==2024.2.2 +certifi==2024.7.4 charset-normalizer==3.3.2 docutils==0.18.1 idna==3.7 imagesize==1.4.1 Jinja2==3.1.4 MarkupSafe==2.1.5 -packaging==24.0 +packaging==24.1 Pygments==2.18.0 -requests==2.31.0 +requests==2.32.3 snowballstemmer==2.2.0 Sphinx==7.3.7 sphinx-rtd-theme==2.0.0 @@ -20,4 +20,4 @@ sphinxcontrib-jquery==4.1 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 -urllib3==2.2.1 +urllib3==2.2.2 diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index bdec69db..b62e5114 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,16 @@ Changelog ========= +0.23.8 (2024-07-17) +=================== +- Fixes a bug that caused duplicate markers in async tests `#813 `_ +- Declare support for Python 3.13 + +Known issues +------------ +As of v0.23, pytest-asyncio attaches an asyncio event loop to each item of the test suite (i.e. session, packages, modules, classes, functions) and allows tests to be run in those loops when marked accordingly. Pytest-asyncio currently assumes that async fixture scope is correlated with the new event loop scope. This prevents fixtures from being evaluated independently from the event loop scope and breaks some existing test suites (see `#706`_). For example, a test suite may require all fixtures and tests to run in the same event loop, but have async fixtures that are set up and torn down for each module. If you're affected by this issue, please continue using the v0.21 release, until it is resolved. + + 0.23.7 (2024-05-19) =================== - Silence deprecation warnings about unclosed event loops that occurred with certain CPython patch releases `#817 `_ @@ -84,6 +94,10 @@ This release has been yanked from PyPI due to fundamental issues with the _async - Remove support for Python 3.7 - Declare support for Python 3.12 +0.21.2 (2024-04-29) +=================== +- Fix compatibility with pytest 8.2. Backport of `#800 `_ to pytest-asyncio v0.21 for users who are unable to upgrade to a more recent version (see `#706`_) + 0.21.1 (2023-07-12) =================== - Output a proper error message when an invalid ``asyncio_mode`` is selected. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 7dbb238d..d3d1fcf7 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -61,8 +61,11 @@ FixtureFunction = Union[SimpleFixtureFunction, FactoryFixtureFunction] FixtureFunctionMarker = Callable[[FixtureFunction], FixtureFunction] -# https://github.com/pytest-dev/pytest/pull/9510 -FixtureDef = Any +# https://github.com/pytest-dev/pytest/commit/fb55615d5e999dd44306596f340036c195428ef1 +if pytest.version_tuple < (8, 0): + FixtureDef = Any +else: + from pytest import FixtureDef class PytestAsyncioError(Exception): @@ -315,7 +318,7 @@ def _wrap_asyncgen_fixture(fixturedef: FixtureDef, event_loop_fixture_id: str) - @functools.wraps(fixture) def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any): - unittest = False if pytest.version_tuple >= (8, 2) else fixturedef.unittest + unittest = fixturedef.unittest if hasattr(fixturedef, "unittest") else False func = _perhaps_rebind_fixture_func(fixture, request.instance, unittest) event_loop = kwargs.pop(event_loop_fixture_id) gen_obj = func( @@ -402,7 +405,8 @@ def _from_function(cls, function: Function, /) -> Function: keywords=function.keywords, originalname=function.originalname, ) - subclass_instance.own_markers.extend(function.own_markers) + subclass_instance.own_markers = function.own_markers + assert subclass_instance.own_markers == function.own_markers subclassed_function_signature = inspect.signature(subclass_instance.obj) if "event_loop" in subclassed_function_signature.parameters: subclass_instance.warn( diff --git a/setup.cfg b/setup.cfg index 9fba05ae..9947cbe3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Topic :: Software Development :: Testing diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py index fa12f2b3..2d2171bd 100644 --- a/tests/hypothesis/test_base.py +++ b/tests/hypothesis/test_base.py @@ -23,7 +23,7 @@ async def test_mark_inner(n): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1) @@ -67,7 +67,7 @@ async def test_explicit_fixture_request(event_loop, n): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=2) result.stdout.fnmatch_lines( [ diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index 7a5f8533..eded4552 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -43,7 +43,7 @@ async def test_remember_loop(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( '*is asynchronous and explicitly requests the "event_loop" fixture*' @@ -197,3 +197,34 @@ async def test_anything(): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(warnings=0, passed=1) + + +def test_asyncio_mark_does_not_duplicate_other_marks_in_auto_mode( + pytester: Pytester, +): + pytester.makeconftest( + dedent( + """\ + def pytest_configure(config): + config.addinivalue_line( + "markers", "dummy_marker: mark used for testing purposes" + ) + """ + ) + ) + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.dummy_marker + async def test_markers_not_duplicated(request): + markers = [] + for node, marker in request.node.iter_markers_with_node(): + markers.append(marker) + assert len(markers) == 2 + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=auto") + result.assert_outcomes(warnings=0, passed=1) diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index 01fdc324..5cc6a2a7 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -48,7 +48,7 @@ def sample_fixture(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=2, warnings=2) result.stdout.fnmatch_lines( '*is asynchronous and explicitly requests the "event_loop" fixture*' diff --git a/tests/modes/test_strict_mode.py b/tests/modes/test_strict_mode.py index 3afc9f5b..220410be 100644 --- a/tests/modes/test_strict_mode.py +++ b/tests/modes/test_strict_mode.py @@ -79,7 +79,7 @@ async def test_anything(): """ ) ) - result = testdir.runpytest("--asyncio-mode=strict", "-W default") + result = testdir.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(skipped=1, warnings=1) result.stdout.fnmatch_lines(["*async def functions are not natively supported*"]) @@ -100,7 +100,7 @@ async def test_anything(any_fixture): """ ) ) - result = testdir.runpytest("--asyncio-mode=strict", "-W default") + result = testdir.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(skipped=1, warnings=2) result.stdout.fnmatch_lines( [ diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py index b514cbcd..20ac173d 100644 --- a/tests/test_asyncio_mark.py +++ b/tests/test_asyncio_mark.py @@ -15,7 +15,7 @@ def test_a(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1) result.stdout.fnmatch_lines( ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] @@ -36,7 +36,7 @@ async def test_a(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -54,7 +54,7 @@ async def test_a(): """ ) ) - result = pytester.runpytest("--asyncio-mode=auto", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -76,7 +76,7 @@ async def test_a(self): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -96,7 +96,7 @@ async def test_a(): """ ) ) - result = pytester.runpytest("--asyncio-mode=auto", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -119,7 +119,7 @@ async def test_a(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -139,7 +139,7 @@ async def test_a(): """ ) ) - result = pytester.runpytest("--asyncio-mode=auto", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py index eabb54a3..ae260261 100644 --- a/tests/test_event_loop_fixture_finalizer.py +++ b/tests/test_event_loop_fixture_finalizer.py @@ -84,7 +84,7 @@ async def test_async_with_explicit_fixture_request(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( '*is asynchronous and explicitly requests the "event_loop" fixture*' @@ -113,7 +113,7 @@ async def test_ends_with_unclosed_loop(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W", "default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") result.assert_outcomes(passed=1, warnings=2) result.stdout.fnmatch_lines("*unclosed event loop*") @@ -135,6 +135,6 @@ async def test_ends_with_unclosed_loop(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W", "default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines("*unclosed event loop*") diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py index 45afc542..683f0963 100644 --- a/tests/test_event_loop_fixture_override_deprecation.py +++ b/tests/test_event_loop_fixture_override_deprecation.py @@ -22,7 +22,7 @@ async def test_emits_warning(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ["*event_loop fixture provided by pytest-asyncio has been redefined*"] @@ -50,7 +50,7 @@ async def test_emits_warning_when_requested_explicitly(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=2) result.stdout.fnmatch_lines( ["*event_loop fixture provided by pytest-asyncio has been redefined*"] @@ -80,7 +80,7 @@ def test_emits_no_warning(): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=1, warnings=0) @@ -107,5 +107,5 @@ def test_emits_warning(uses_event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) diff --git a/tests/test_explicit_event_loop_fixture_request.py b/tests/test_explicit_event_loop_fixture_request.py index 4cac85f7..e09893fa 100644 --- a/tests/test_explicit_event_loop_fixture_request.py +++ b/tests/test_explicit_event_loop_fixture_request.py @@ -17,7 +17,7 @@ async def test_coroutine_emits_warning(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] @@ -39,7 +39,7 @@ async def test_coroutine_emits_warning(self, event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] @@ -62,7 +62,7 @@ async def test_coroutine_emits_warning(event_loop): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] @@ -88,7 +88,7 @@ async def test_uses_fixture(emits_warning): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] @@ -114,7 +114,7 @@ async def test_uses_fixture(emits_warning): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( ['*is asynchronous and explicitly requests the "event_loop" fixture*'] diff --git a/tox.ini b/tox.ini index 7bab7350..665c2fff 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.14.0 -envlist = py38, py39, py310, py311, py312, pytest-min, docs +envlist = py38, py39, py310, py311, py312, py13, pytest-min, docs isolated_build = true passenv = CI @@ -42,4 +42,5 @@ python = 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 pypy3: pypy3