diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6851845..12348ae1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ on: workflow_dispatch: env: - PYTHON_LATEST: 3.12 + PYTHON_LATEST: 3.13 jobs: lint: @@ -62,7 +62,7 @@ jobs: strategy: matrix: os: [ubuntu, windows] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', 3.13.0-beta.3] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -70,7 +70,7 @@ jobs: if: "!endsWith(matrix.python-version, '-dev')" with: python-version: ${{ matrix.python-version }} - - uses: deadsnakes/action@v3.1.0 + - uses: deadsnakes/action@v3.2.0 if: endsWith(matrix.python-version, '-dev') with: python-version: ${{ matrix.python-version }} @@ -88,7 +88,8 @@ jobs: if: "!endsWith(matrix.os, 'windows')" with: name: coverage-python-${{ matrix.python-version }} - path: .coverage.* + path: coverage/coverage.* + if-no-files-found: error check: name: Check @@ -112,13 +113,14 @@ jobs: uses: actions/download-artifact@v4 with: pattern: coverage-* + path: coverage merge-multiple: true - name: Combine coverage data and create report run: | coverage combine coverage xml - name: Upload coverage report - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: coverage.xml fail_ci_if_error: true @@ -149,8 +151,9 @@ jobs: run: | pandoc -s -o README.md README.rst - name: PyPI upload - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.12.3 with: + attestations: true packages-dir: dist password: ${{ secrets.PYPI_API_TOKEN }} - name: GitHub Release diff --git a/.gitignore b/.gitignore index 7dd9b771..5a568761 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,7 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.coverage -.coverage.* +coverage/ .pytest_cache nosetests.xml coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ad9e702..c1943f60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,21 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-merge-conflict exclude: rst$ +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.6 + hooks: + - id: ruff + args: [--fix] - repo: https://github.com/asottile/yesqa rev: v1.5.0 hooks: - id: yesqa - repo: https://github.com/Zac-HD/shed - rev: 2024.3.1 + rev: 2024.10.1 hooks: - id: shed args: @@ -25,7 +30,7 @@ repos: - id: yamlfmt args: [--mapping, '2', --sequence, '2', --offset, '0'] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -37,23 +42,18 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 + rev: v1.14.1 hooks: - id: mypy exclude: ^(docs|tests)/.* additional_dependencies: - pytest -- repo: https://github.com/pycqa/flake8 - rev: 7.1.1 - hooks: - - id: flake8 - language_version: python3 - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: python-use-type-annotations - repo: https://github.com/rhysd/actionlint - rev: v1.7.1 + rev: v1.7.6 hooks: - id: actionlint-docker args: @@ -65,9 +65,15 @@ repos: - 'SC1004:' stages: [manual] - repo: https://github.com/sirosen/check-jsonschema - rev: 0.29.1 + rev: 0.30.0 hooks: - id: check-github-actions +- repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.5.0 + hooks: + - id: pyproject-fmt + # https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version + additional_dependencies: [tox>=4.9] ci: skip: - actionlint-docker diff --git a/Makefile b/Makefile index e1ef5d27..83c8ba81 100644 --- a/Makefile +++ b/Makefile @@ -17,11 +17,11 @@ clean-pyc: ## remove Python file artifacts clean-test: ## remove test and coverage artifacts rm -fr .tox/ - rm -f .coverage + rm -fr coverage/ rm -fr htmlcov/ test: - coverage run --parallel-mode --omit */_version.py -m pytest + coverage run -m pytest install: pip install -U pre-commit diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 1221d3b9..816c4639 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,11 +1,11 @@ -attrs==24.2.0 -coverage==7.6.1 +attrs==24.3.0 +coverage==7.6.10 exceptiongroup==1.2.2 -hypothesis==6.111.1 +hypothesis==6.123.4 iniconfig==2.0.0 -packaging==24.1 +packaging==24.2 pluggy==1.5.0 -pytest==8.3.2 +pytest==8.3.4 sortedcontainers==2.4.0 -tomli==2.0.1 +tomli==2.2.1 typing_extensions==4.12.2 diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 6df3c716..607c1632 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -1,18 +1,18 @@ alabaster==0.7.16 Babel==2.16.0 -certifi==2024.7.4 -charset-normalizer==3.3.2 -docutils==0.20.1 -idna==3.7 +certifi==2024.12.14 +charset-normalizer==3.4.1 +docutils==0.21.2 +idna==3.10 imagesize==1.4.1 -Jinja2==3.1.4 -MarkupSafe==2.1.5 -packaging==24.1 -Pygments==2.18.0 +Jinja2==3.1.5 +MarkupSafe==3.0.2 +packaging==24.2 +Pygments==2.19.1 requests==2.32.3 snowballstemmer==2.2.0 -Sphinx==7.4.7 -sphinx-rtd-theme==2.0.0 +Sphinx==8.0.2 +sphinx-rtd-theme==3.0.2 sphinxcontrib-applehelp==2.0.0 sphinxcontrib-devhelp==2.0.0 sphinxcontrib-htmlhelp==2.1.0 @@ -20,4 +20,4 @@ sphinxcontrib-jquery==4.1 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 -urllib3==2.2.2 +urllib3==2.3.0 diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index 77cf3451..a28c9fd3 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -2,6 +2,27 @@ Changelog ========= +0.25.2 (2025-01-08) +=================== + +- Call ``loop.shutdown_asyncgens()`` before closing the event loop to ensure async generators are closed in the same manner as ``asyncio.run`` does `#1034 `_ + +0.25.1 (2025-01-02) +=================== +- Fixes an issue that caused a broken event loop when a function-scoped test was executed in between two tests with wider loop scope `#950 `_ +- Improves test collection speed in auto mode `#1020 `_ +- Corrects the warning that is emitted upon redefining the event_loop fixture + + +0.25.0 (2024-12-13) +=================== +- Deprecated: Added warning when asyncio test requests async ``@pytest.fixture`` in strict mode. This will become an error in a future version of flake8-asyncio. `#979 `_ +- Updates the error message about `pytest.mark.asyncio`'s `scope` keyword argument to say `loop_scope` instead. `#1004 `_ +- Verbose log displays correct parameter name: asyncio_default_fixture_loop_scope `#990 `_ +- Propagates `contextvars` set in async fixtures to other fixtures and tests on Python 3.11 and above. `#1008 `_ + + + 0.24.0 (2024-08-22) =================== - BREAKING: Updated minimum supported pytest version to v8.2.0 @@ -9,6 +30,7 @@ Changelog - Deprecates the optional `scope` keyword argument to `pytest.mark.asyncio` for API consistency with ``pytest_asyncio.fixture``. Users are encouraged to use the `loop_scope` keyword argument, which does exactly the same. - Raises an error when passing `scope` or `loop_scope` as a positional argument to ``@pytest.mark.asyncio``. `#812 `_ - Fixes a bug that caused module-scoped async fixtures to fail when reused in other modules `#862 `_ `#668 `_ +- Added the ``asyncio_default_fixture_loop_scope`` configuration option `c74d1c3 `_ 0.23.8 (2024-07-17) diff --git a/docs/reference/fixtures/index.rst b/docs/reference/fixtures/index.rst index 37eec503..04953783 100644 --- a/docs/reference/fixtures/index.rst +++ b/docs/reference/fixtures/index.rst @@ -4,6 +4,13 @@ Fixtures event_loop ========== +*This fixture is deprecated.* + +*If you want to request an asyncio event loop with a scope other than function +scope, use the "loop_scope" argument to* :ref:`reference/markers/asyncio` *when marking the tests. +If you want to return different types of event loops, use the* :ref:`reference/fixtures/event_loop_policy` +*fixture.* + Creates a new asyncio event loop based on the current event loop policy. The new loop is available as the return value of this fixture for synchronous functions, or via `asyncio.get_running_loop `__ for asynchronous functions. The event loop is closed when the fixture scope ends. @@ -12,7 +19,7 @@ The fixture scope defaults to ``function`` scope. .. include:: event_loop_example.py :code: python -Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The `asyncio <#pytest-mark-asyncio>`__ marker +Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The :ref:`asyncio ` marker is used to mark coroutines that should be treated as test functions. If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture. @@ -20,6 +27,8 @@ If you need to change the type of the event loop, prefer setting a custom event If the ``pytest.mark.asyncio`` decorator is applied to a test function, the ``event_loop`` fixture will be requested automatically by the test function. +.. _reference/fixtures/event_loop_policy: + event_loop_policy ================= Returns the event loop policy used to create asyncio event loops. diff --git a/docs/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py b/docs/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py index 239f3968..6fff0af8 100644 --- a/docs/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py +++ b/docs/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py @@ -9,7 +9,7 @@ class TestClassScopedLoop: loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="class") + @pytest_asyncio.fixture(loop_scope="class") async def my_fixture(self): TestClassScopedLoop.loop = asyncio.get_running_loop() diff --git a/docs/reference/markers/index.rst b/docs/reference/markers/index.rst index 6e9be1ca..e7d700c9 100644 --- a/docs/reference/markers/index.rst +++ b/docs/reference/markers/index.rst @@ -2,6 +2,8 @@ Markers ======= +.. _reference/markers/asyncio: + ``pytest.mark.asyncio`` ======================= A coroutine or async generator with this marker is treated as a test function by pytest. @@ -25,7 +27,7 @@ The following code example provides a shared event loop for all tests in `TestCl .. include:: class_scoped_loop_strict_mode_example.py :code: python -Requesting class scope with the test being part of a class will give a *UsageError*. +If you request class scope for a test that is not part of a class, it will result in a *UsageError*. Similar to class-scoped event loops, a module-scoped loop is provided when setting mark's scope to *module:* .. include:: module_scoped_loop_strict_mode_example.py diff --git a/pyproject.toml b/pyproject.toml index 81540a53..b89c24da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,141 @@ [build-system] +build-backend = "setuptools.build_meta" + requires = [ - "setuptools>=51.0", + "setuptools>=51", + "setuptools-scm[toml]>=6.2", "wheel>=0.36", - "setuptools_scm[toml]>=6.2" ] -build-backend = "setuptools.build_meta" + +[project] +name = "pytest-asyncio" +description = "Pytest support for asyncio" +readme.content-type = "text/x-rst" +readme.file = "README.rst" +license.text = "Apache 2.0" +authors = [ + { name = "Tin Tvrtković ", email = "tinchester@gmail.com" }, +] +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: AsyncIO", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Testing", + "Typing :: Typed", +] +dynamic = [ + "version", +] + +dependencies = [ + "pytest>=8.2,<9", +] +optional-dependencies.docs = [ + "sphinx>=5.3", + "sphinx-rtd-theme>=1", +] +optional-dependencies.testing = [ + "coverage>=6.2", + "hypothesis>=5.7.1", +] +urls."Bug Tracker" = "https://github.com/pytest-dev/pytest-asyncio/issues" +urls.Changelog = "https://pytest-asyncio.readthedocs.io/en/latest/reference/changelog.html" +urls.Documentation = "https://pytest-asyncio.readthedocs.io" +urls.Homepage = "https://github.com/pytest-dev/pytest-asyncio" +urls."Source Code" = "https://github.com/pytest-dev/pytest-asyncio" +entry-points.pytest11.asyncio = "pytest_asyncio.plugin" + +[tool.setuptools] +packages = [ + "pytest_asyncio", +] +include-package-data = true +license-files = [ + "LICENSE", +] [tool.setuptools_scm] write_to = "pytest_asyncio/_version.py" + +[tool.ruff] +line-length = 88 +format.docstring-code-format = true +lint.select = [ + "B", # bugbear + "D", # pydocstyle + "E", # pycodestyle + "F", # pyflakes + "FA100", # add future annotations + "PGH004", # pygrep-hooks - Use specific rule codes when using noqa + "PIE", # flake8-pie + "PLE", # pylint error + "PYI", # flake8-pyi + "RUF", # ruff + "T100", # flake8-debugger + "UP", # pyupgrade + "W", # pycodestyle +] + +lint.ignore = [ + # bugbear ignore + "B028", # No explicit `stacklevel` keyword argument found + # pydocstyle ignore + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in `__init__` + "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible + "D205", # 1 blank line required between summary line and description + "D209", # [*] Multi-line docstring closing quotes should be on a separate line + "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "D402", # First line should not be the function's signature + "D404", # First word of the docstring should not be "This" + "D415", # First line should end with a period, question mark, or exclamation point +] + +[tool.pytest.ini_options] +python_files = [ + "test_*.py", + "*_example.py", +] +addopts = "-rsx --tb=short" +testpaths = [ + "docs", + "tests", +] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +junit_family = "xunit2" +filterwarnings = [ + "error", + "ignore:The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning", +] + +[tool.coverage.run] +source = [ + "pytest_asyncio", +] +branch = true +data_file = "coverage/coverage" +omit = [ + "*/_version.py", +] +parallel = true + +[tool.coverage.report] +show_missing = true diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py index 08dca478..c25c1bf1 100644 --- a/pytest_asyncio/__init__.py +++ b/pytest_asyncio/__init__.py @@ -1,6 +1,8 @@ """The main point for importing pytest-asyncio items.""" -from ._version import version as __version__ # noqa +from __future__ import annotations + +from ._version import version as __version__ # noqa: F401 from .plugin import fixture, is_async_test __all__ = ("fixture", "is_async_test") diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 178fcaa6..2f028ae1 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -1,30 +1,31 @@ """pytest-asyncio implementation.""" +from __future__ import annotations + import asyncio import contextlib +import contextvars import enum import functools import inspect import socket import warnings -from asyncio import AbstractEventLoopPolicy -from textwrap import dedent -from typing import ( - Any, +from asyncio import AbstractEventLoop, AbstractEventLoopPolicy +from collections.abc import ( AsyncIterator, Awaitable, - Callable, - Dict, + Coroutine as AbstractCoroutine, Generator, Iterable, Iterator, - List, - Literal, Mapping, - Optional, Sequence, - Set, - Type, +) +from textwrap import dedent +from typing import ( + Any, + Callable, + Literal, TypeVar, Union, overload, @@ -112,16 +113,16 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None def fixture( fixture_function: FixtureFunction, *, - scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., - loop_scope: Union[_ScopeName, None] = ..., - params: Optional[Iterable[object]] = ..., + scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., + loop_scope: _ScopeName | None = ..., + params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Union[ - Iterable[Union[str, float, int, bool, None]], - Callable[[Any], Optional[object]], - None, - ] = ..., - name: Optional[str] = ..., + ids: ( + Iterable[str | float | int | bool | None] + | Callable[[Any], object | None] + | None + ) = ..., + name: str | None = ..., ) -> FixtureFunction: ... @@ -129,24 +130,24 @@ def fixture( def fixture( fixture_function: None = ..., *, - scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., - loop_scope: Union[_ScopeName, None] = ..., - params: Optional[Iterable[object]] = ..., + scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., + loop_scope: _ScopeName | None = ..., + params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Union[ - Iterable[Union[str, float, int, bool, None]], - Callable[[Any], Optional[object]], - None, - ] = ..., - name: Optional[str] = None, + ids: ( + Iterable[str | float | int | bool | None] + | Callable[[Any], object | None] + | None + ) = ..., + name: str | None = None, ) -> FixtureFunctionMarker: ... def fixture( - fixture_function: Optional[FixtureFunction] = None, - loop_scope: Union[_ScopeName, None] = None, + fixture_function: FixtureFunction | None = None, + loop_scope: _ScopeName | None = None, **kwargs: Any, -) -> Union[FixtureFunction, FixtureFunctionMarker]: +) -> FixtureFunction | FixtureFunctionMarker: if fixture_function is not None: _make_asyncio_fixture_function(fixture_function, loop_scope) return pytest.fixture(fixture_function, **kwargs) @@ -165,9 +166,7 @@ def _is_asyncio_fixture_function(obj: Any) -> bool: return getattr(obj, "_force_asyncio_fixture", False) -def _make_asyncio_fixture_function( - obj: Any, loop_scope: Union[_ScopeName, None] -) -> None: +def _make_asyncio_fixture_function(obj: Any, loop_scope: _ScopeName | None) -> None: if hasattr(obj, "__func__"): # instance method, check the function object obj = obj.__func__ @@ -176,7 +175,7 @@ def _make_asyncio_fixture_function( def _is_coroutine_or_asyncgen(obj: Any) -> bool: - return asyncio.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj) + return inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj) def _get_asyncio_mode(config: Config) -> Mode: @@ -185,11 +184,11 @@ def _get_asyncio_mode(config: Config) -> Mode: val = config.getini("asyncio_mode") try: return Mode(val) - except ValueError: + except ValueError as e: modes = ", ".join(m.value for m in Mode) raise pytest.UsageError( f"{val!r} is not a valid asyncio_mode. Valid modes: {modes}." - ) + ) from e _DEFAULT_FIXTURE_LOOP_SCOPE_UNSET = """\ @@ -215,16 +214,18 @@ def pytest_configure(config: Config) -> None: @pytest.hookimpl(tryfirst=True) -def pytest_report_header(config: Config) -> List[str]: +def pytest_report_header(config: Config) -> list[str]: """Add asyncio config to pytest header.""" mode = _get_asyncio_mode(config) default_loop_scope = config.getini("asyncio_default_fixture_loop_scope") - return [f"asyncio: mode={mode}, default_loop_scope={default_loop_scope}"] + return [ + f"asyncio: mode={mode}, asyncio_default_fixture_loop_scope={default_loop_scope}" + ] def _preprocess_async_fixtures( collector: Collector, - processed_fixturedefs: Set[FixtureDef], + processed_fixturedefs: set[FixtureDef], ) -> None: config = collector.config default_loop_scope = config.getini("asyncio_default_fixture_loop_scope") @@ -238,7 +239,7 @@ def _preprocess_async_fixtures( func ): continue - if not _is_asyncio_fixture_function(func) and asyncio_mode == Mode.STRICT: + if asyncio_mode == Mode.STRICT and not _is_asyncio_fixture_function(func): # Ignore async fixtures without explicit asyncio mark in strict mode # This applies to pytest_trio fixtures, for example continue @@ -268,9 +269,7 @@ def _preprocess_async_fixtures( def _synchronize_async_fixture(fixturedef: FixtureDef) -> None: - """ - Wraps the fixture function of an async fixture in a synchronous function. - """ + """Wraps the fixture function of an async fixture in a synchronous function.""" if inspect.isasyncgenfunction(fixturedef.func): _wrap_asyncgen_fixture(fixturedef) elif inspect.iscoroutinefunction(fixturedef.func): @@ -279,10 +278,10 @@ def _synchronize_async_fixture(fixturedef: FixtureDef) -> None: def _add_kwargs( func: Callable[..., Any], - kwargs: Dict[str, Any], + kwargs: dict[str, Any], event_loop: asyncio.AbstractEventLoop, request: FixtureRequest, -) -> Dict[str, Any]: +) -> dict[str, Any]: sig = inspect.signature(func) ret = kwargs.copy() if "request" in sig.parameters: @@ -292,7 +291,7 @@ def _add_kwargs( return ret -def _perhaps_rebind_fixture_func(func: _T, instance: Optional[Any]) -> _T: +def _perhaps_rebind_fixture_func(func: _T, instance: Any | None) -> _T: if instance is not None: # The fixture needs to be bound to the actual request.instance # so it is bound to the same object as the test method. @@ -325,6 +324,12 @@ async def setup(): res = await gen_obj.__anext__() # type: ignore[union-attr] return res + context = contextvars.copy_context() + setup_task = _create_task_in_context(event_loop, setup(), context) + result = event_loop.run_until_complete(setup_task) + + reset_contextvars = _apply_contextvar_changes(context) + def finalizer() -> None: """Yield again, to finalize.""" @@ -338,9 +343,11 @@ async def async_finalizer() -> None: msg += "Yield only once." raise ValueError(msg) - event_loop.run_until_complete(async_finalizer()) + task = _create_task_in_context(event_loop, async_finalizer(), context) + event_loop.run_until_complete(task) + if reset_contextvars is not None: + reset_contextvars() - result = event_loop.run_until_complete(setup()) request.addfinalizer(finalizer) return result @@ -363,7 +370,23 @@ async def setup(): res = await func(**_add_kwargs(func, kwargs, event_loop, request)) return res - return event_loop.run_until_complete(setup()) + context = contextvars.copy_context() + setup_task = _create_task_in_context(event_loop, setup(), context) + result = event_loop.run_until_complete(setup_task) + + # Copy the context vars modified by the setup task into the current + # context, and (if needed) add a finalizer to reset them. + # + # Note that this is slightly different from the behavior of a non-async + # fixture, which would rely on the fixture author to add a finalizer + # to reset the variables. In this case, the author of the fixture can't + # write such a finalizer because they have no way to capture the Context + # in which the setup function was run, so we need to do it for them. + reset_contextvars = _apply_contextvar_changes(context) + if reset_contextvars is not None: + request.addfinalizer(reset_contextvars) + + return result fixturedef.func = _async_fixture_wrapper # type: ignore[misc] @@ -388,13 +411,66 @@ def _get_event_loop_fixture_id_for_async_fixture( return event_loop_fixture_id +def _create_task_in_context( + loop: asyncio.AbstractEventLoop, + coro: AbstractCoroutine[Any, Any, _T], + context: contextvars.Context, +) -> asyncio.Task[_T]: + """ + Return an asyncio task that runs the coro in the specified context, + if possible. + + This allows fixture setup and teardown to be run as separate asyncio tasks, + while still being able to use context-manager idioms to maintain context + variables and make those variables visible to test functions. + + This is only fully supported on Python 3.11 and newer, as it requires + the API added for https://github.com/python/cpython/issues/91150. + On earlier versions, the returned task will use the default context instead. + """ + try: + return loop.create_task(coro, context=context) + except TypeError: + return loop.create_task(coro) + + +def _apply_contextvar_changes( + context: contextvars.Context, +) -> Callable[[], None] | None: + """ + Copy contextvar changes from the given context to the current context. + + If any contextvars were modified by the fixture, return a finalizer that + will restore them. + """ + context_tokens = [] + for var in context: + try: + if var.get() is context.get(var): + # This variable is not modified, so leave it as-is. + continue + except LookupError: + # This variable isn't yet set in the current context at all. + pass + token = var.set(context.get(var)) + context_tokens.append((var, token)) + + if not context_tokens: + return None + + def restore_contextvars(): + while context_tokens: + (var, token) = context_tokens.pop() + var.reset(token) + + return restore_contextvars + + class PytestAsyncioFunction(Function): """Base class for all test functions managed by pytest-asyncio.""" @classmethod - def item_subclass_for( - cls, item: Function, / - ) -> Union[Type["PytestAsyncioFunction"], None]: + def item_subclass_for(cls, item: Function, /) -> type[PytestAsyncioFunction] | None: """ Returns a subclass of PytestAsyncioFunction if there is a specialized subclass for the specified function item. @@ -447,7 +523,7 @@ class Coroutine(PytestAsyncioFunction): @staticmethod def _can_substitute(item: Function) -> bool: func = item.obj - return asyncio.iscoroutinefunction(func) + return inspect.iscoroutinefunction(func) def runtest(self) -> None: self.obj = wrap_in_sync( @@ -512,7 +588,7 @@ def _can_substitute(item: Function) -> bool: return ( getattr(func, "is_hypothesis_test", False) # type: ignore[return-value] and getattr(func, "hypothesis", None) - and asyncio.iscoroutinefunction(func.hypothesis.inner_test) + and inspect.iscoroutinefunction(func.hypothesis.inner_test) ) def runtest(self) -> None: @@ -522,17 +598,15 @@ def runtest(self) -> None: super().runtest() -_HOLDER: Set[FixtureDef] = set() +_HOLDER: set[FixtureDef] = set() # The function name needs to start with "pytest_" # see https://github.com/pytest-dev/pytest/issues/11307 @pytest.hookimpl(specname="pytest_pycollect_makeitem", tryfirst=True) def pytest_pycollect_makeitem_preprocess_async_fixtures( - collector: Union[pytest.Module, pytest.Class], name: str, obj: object -) -> Union[ - pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]], None -]: + collector: pytest.Module | pytest.Class, name: str, obj: object +) -> pytest.Item | pytest.Collector | list[pytest.Item | pytest.Collector] | None: """A pytest hook to collect asyncio coroutines.""" if not collector.funcnamefilter(name): return None @@ -544,7 +618,7 @@ def pytest_pycollect_makeitem_preprocess_async_fixtures( # see https://github.com/pytest-dev/pytest/issues/11307 @pytest.hookimpl(specname="pytest_pycollect_makeitem", hookwrapper=True) def pytest_pycollect_makeitem_convert_async_functions_to_subclass( - collector: Union[pytest.Module, pytest.Class], name: str, obj: object + collector: pytest.Module | pytest.Class, name: str, obj: object ) -> Generator[None, pluggy.Result, None]: """ Converts coroutines and async generators collected as pytest.Functions @@ -552,12 +626,9 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( """ hook_result = yield try: - node_or_list_of_nodes: Union[ - pytest.Item, - pytest.Collector, - List[Union[pytest.Item, pytest.Collector]], - None, - ] = hook_result.get_result() + node_or_list_of_nodes: ( + pytest.Item | pytest.Collector | list[pytest.Item | pytest.Collector] | None + ) = hook_result.get_result() except BaseException as e: hook_result.force_exception(e) return @@ -585,7 +656,7 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( _event_loop_fixture_id = StashKey[str]() -_fixture_scope_by_collector_type: Mapping[Type[pytest.Collector], _ScopeName] = { +_fixture_scope_by_collector_type: Mapping[type[pytest.Collector], _ScopeName] = { Class: "class", # Package is a subclass of module and the dict is used in isinstance checks # Therefore, the order matters and Package needs to appear before Module @@ -596,7 +667,7 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( # A stack used to push package-scoped loops during collection of a package # and pop those loops during collection of a Module -__package_loop_stack: List[Union[FixtureFunctionMarker, FixtureFunction]] = [] +__package_loop_stack: list[FixtureFunctionMarker | FixtureFunction] = [] @pytest.hookimpl @@ -637,12 +708,12 @@ def scoped_event_loop( event_loop_policy, ) -> Iterator[asyncio.AbstractEventLoop]: new_loop_policy = event_loop_policy - with _temporary_event_loop_policy(new_loop_policy): - loop = asyncio.new_event_loop() - loop.__pytest_asyncio = True # type: ignore[attr-defined] + with ( + _temporary_event_loop_policy(new_loop_policy), + _provide_event_loop() as loop, + ): asyncio.set_event_loop(loop) yield loop - loop.close() # @pytest.fixture does not register the fixture anywhere, so pytest doesn't # know it exists. We work around this by attaching the fixture function to the @@ -691,6 +762,19 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No try: yield finally: + # Try detecting user-created event loops that were left unclosed + # at the end of a test. + try: + current_loop: AbstractEventLoop | None = _get_event_loop_no_warn() + except RuntimeError: + current_loop = None + if current_loop is not None and not current_loop.is_closed(): + warnings.warn( + _UNCLOSED_EVENT_LOOP_WARNING % current_loop, + DeprecationWarning, + ) + current_loop.close() + asyncio.set_event_loop_policy(old_loop_policy) # When a test uses both a scoped event loop and the event_loop fixture, # the "_provide_clean_event_loop" finalizer of the event_loop fixture @@ -711,7 +795,7 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No Replacing the event_loop fixture with a custom implementation is deprecated and will lead to errors in the future. If you want to request an asyncio event loop with a scope other than function - scope, use the "scope" argument to the asyncio mark when marking the tests. + scope, use the "loop_scope" argument to the asyncio mark when marking the tests. If you want to return different types of event loops, use the event_loop_policy fixture. """ @@ -778,7 +862,7 @@ def pytest_fixture_setup( # Weird behavior was observed when checking for an attribute of FixtureDef.func # Instead, we now check for a special attribute of the returned event loop fixture_filename = inspect.getsourcefile(fixturedef.func) - if not getattr(loop, "__original_fixture_loop", False): + if not _is_pytest_asyncio_loop(loop): _, fixture_line_number = inspect.getsourcelines(fixturedef.func) warnings.warn( _REDEFINED_EVENT_LOOP_FIXTURE_WARNING @@ -788,8 +872,7 @@ def pytest_fixture_setup( policy = asyncio.get_event_loop_policy() try: old_loop = _get_event_loop_no_warn(policy) - is_pytest_asyncio_loop = getattr(old_loop, "__pytest_asyncio", False) - if old_loop is not loop and not is_pytest_asyncio_loop: + if old_loop is not loop and not _is_pytest_asyncio_loop(old_loop): old_loop.close() except RuntimeError: # Either the current event loop has been set to None @@ -802,11 +885,20 @@ def pytest_fixture_setup( yield +def _make_pytest_asyncio_loop(loop: AbstractEventLoop) -> AbstractEventLoop: + loop.__pytest_asyncio = True # type: ignore[attr-defined] + return loop + + +def _is_pytest_asyncio_loop(loop: AbstractEventLoop) -> bool: + return getattr(loop, "__pytest_asyncio", False) + + def _add_finalizers(fixturedef: FixtureDef, *finalizers: Callable[[], object]) -> None: """ - Regsiters the specified fixture finalizers in the fixture. + Registers the specified fixture finalizers in the fixture. - Finalizers need to specified in the exact order in which they should be invoked. + Finalizers need to be specified in the exact order in which they should be invoked. :param fixturedef: Fixture definition which finalizers should be added to :param finalizers: Finalizers to be added @@ -835,7 +927,7 @@ def _close_event_loop() -> None: loop = policy.get_event_loop() except RuntimeError: loop = None - if loop is not None: + if loop is not None and not _is_pytest_asyncio_loop(loop): if not loop.is_closed(): warnings.warn( _UNCLOSED_EVENT_LOOP_WARNING % loop, @@ -852,7 +944,7 @@ def _restore_policy(): loop = _get_event_loop_no_warn(previous_policy) except RuntimeError: loop = None - if loop: + if loop and not _is_pytest_asyncio_loop(loop): loop.close() asyncio.set_event_loop_policy(previous_policy) @@ -867,12 +959,17 @@ def _provide_clean_event_loop() -> None: # Note that we cannot set the loop to None, because get_event_loop only creates # a new loop, when set_event_loop has not been called. policy = asyncio.get_event_loop_policy() - new_loop = policy.new_event_loop() - policy.set_event_loop(new_loop) + try: + old_loop = _get_event_loop_no_warn(policy) + except RuntimeError: + old_loop = None + if old_loop is not None and not _is_pytest_asyncio_loop(old_loop): + new_loop = policy.new_event_loop() + policy.set_event_loop(new_loop) def _get_event_loop_no_warn( - policy: Optional[AbstractEventLoopPolicy] = None, + policy: AbstractEventLoopPolicy | None = None, ) -> asyncio.AbstractEventLoop: with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) @@ -883,7 +980,7 @@ def _get_event_loop_no_warn( @pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem: Function) -> Optional[object]: +def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: """ Pytest hook called before a test case is run. @@ -892,7 +989,32 @@ def pytest_pyfunc_call(pyfuncitem: Function) -> Optional[object]: """ if pyfuncitem.get_closest_marker("asyncio") is not None: if isinstance(pyfuncitem, PytestAsyncioFunction): - pass + asyncio_mode = _get_asyncio_mode(pyfuncitem.config) + for fixname, fixtures in pyfuncitem._fixtureinfo.name2fixturedefs.items(): + # name2fixturedefs is a dict between fixture name and a list of matching + # fixturedefs. The last entry in the list is closest and the one used. + func = fixtures[-1].func + if ( + asyncio_mode == Mode.STRICT + and _is_coroutine_or_asyncgen(func) + and not _is_asyncio_fixture_function(func) + ): + warnings.warn( + PytestDeprecationWarning( + f"asyncio test {pyfuncitem.name!r} requested async " + "@pytest.fixture " + f"{fixname!r} in strict mode. " + "You might want to use @pytest_asyncio.fixture or switch " + "to auto mode. " + "This will become an error in future versions of " + "flake8-asyncio." + ), + stacklevel=1, + ) + # no stacklevel points at the users code, so we set stacklevel=1 + # so it at least indicates that it's the plugin complaining. + # Pytest gives the test file & name in the warnings summary at least + else: pyfuncitem.warn( pytest.PytestWarning( @@ -910,9 +1032,10 @@ def pytest_pyfunc_call(pyfuncitem: Function) -> Optional[object]: def wrap_in_sync( func: Callable[..., Awaitable[Any]], ): - """Return a sync wrapper around an async function executing it in the - current event 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 @@ -990,7 +1113,7 @@ def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName: if asyncio_marker.args or ( asyncio_marker.kwargs and set(asyncio_marker.kwargs) - {"loop_scope", "scope"} ): - raise ValueError("mark.asyncio accepts only a keyword argument 'scope'.") + raise ValueError("mark.asyncio accepts only a keyword argument 'loop_scope'.") if "scope" in asyncio_marker.kwargs: if "loop_scope" in asyncio_marker.kwargs: raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR) @@ -1002,7 +1125,7 @@ def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName: return scope -def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector: +def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector: node_type_by_scope = { "class": Class, "module": Module, @@ -1025,16 +1148,26 @@ def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector: def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: """Create an instance of the default event loop for each test case.""" new_loop_policy = request.getfixturevalue(event_loop_policy.__name__) - asyncio.set_event_loop_policy(new_loop_policy) + with _temporary_event_loop_policy(new_loop_policy), _provide_event_loop() as loop: + yield loop + + +@contextlib.contextmanager +def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]: loop = asyncio.get_event_loop_policy().new_event_loop() # Add a magic value to the event loop, so pytest-asyncio can determine if the # event_loop fixture was overridden. Other implementations of event_loop don't # set this value. # The magic value must be set as part of the function definition, because pytest # seems to have multiple instances of the same FixtureDef or fixture function - loop.__original_fixture_loop = True # type: ignore[attr-defined] - yield loop - loop.close() + loop = _make_pytest_asyncio_loop(loop) + try: + yield loop + finally: + try: + loop.run_until_complete(loop.shutdown_asyncgens()) + finally: + loop.close() @pytest.fixture(scope="session") @@ -1042,12 +1175,9 @@ def _session_event_loop( request: FixtureRequest, event_loop_policy: AbstractEventLoopPolicy ) -> Iterator[asyncio.AbstractEventLoop]: new_loop_policy = event_loop_policy - with _temporary_event_loop_policy(new_loop_policy): - loop = asyncio.new_event_loop() - loop.__pytest_asyncio = True # type: ignore[attr-defined] + with _temporary_event_loop_policy(new_loop_policy), _provide_event_loop() as loop: asyncio.set_event_loop(loop) yield loop - loop.close() @pytest.fixture(scope="session", autouse=True) diff --git a/setup.cfg b/setup.cfg index ac2f2adc..f68f54b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,76 +1,18 @@ [metadata] -name = pytest-asyncio +# Not everything is in in pyproject.toml because of this issue: +; Traceback (most recent call last): +; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 69, in __getattr__ +; return next( +; ^^^^^ +;StopIteration +; +;The above exception was the direct cause of the following exception: +; +;Traceback (most recent call last): +; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 183, in read_attr +; return getattr(StaticModule(module_name, spec), attr_name) +; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +; File "/tmp/build-env-rud8b5r6/lib/python3.12/site-packages/setuptools/config/expand.py", line 75, in __getattr__ +; raise AttributeError(f"{self.name} has no attribute {attr}") from e +;AttributeError: pytest_asyncio has no attribute __version__ version = attr: pytest_asyncio.__version__ -url = https://github.com/pytest-dev/pytest-asyncio -project_urls = - Documentation = https://pytest-asyncio.readthedocs.io - Changelog = https://pytest-asyncio.readthedocs.io/en/latest/reference/changelog.html - Source Code = https://github.com/pytest-dev/pytest-asyncio - Bug Tracker = https://github.com/pytest-dev/pytest-asyncio/issues -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_files = LICENSE -classifiers = - Development Status :: 4 - Beta - - Intended Audience :: Developers - - License :: OSI Approved :: Apache Software License - - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - - Topic :: Software Development :: Testing - - Framework :: AsyncIO - Framework :: Pytest - Typing :: Typed - -[options] -python_requires = >=3.8 -packages = pytest_asyncio -include_package_data = True - -# Always adjust requirements.txt and pytest-min-requirements.txt when changing runtime dependencies -install_requires = - pytest >= 8.2,<9 - -[options.extras_require] -testing = - coverage >= 6.2 - hypothesis >= 5.7.1 -docs = - sphinx >= 5.3 - sphinx-rtd-theme >= 1.0 - -[options.entry_points] -pytest11 = - asyncio = pytest_asyncio.plugin - -[coverage:run] -source = pytest_asyncio -branch = true - -[coverage:report] -show_missing = true - -[tool:pytest] -python_files = test_*.py *_example.py -addopts = -rsx --tb=short -testpaths = docs tests -asyncio_mode = auto -junit_family=xunit2 -filterwarnings = - error - ignore:The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning - -[flake8] -max-line-length = 88 diff --git a/tests/async_fixtures/test_async_fixtures.py b/tests/async_fixtures/test_async_fixtures.py index 40012962..16478539 100644 --- a/tests/async_fixtures/test_async_fixtures.py +++ b/tests/async_fixtures/test_async_fixtures.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import unittest.mock diff --git a/tests/async_fixtures/test_async_fixtures_contextvars.py b/tests/async_fixtures/test_async_fixtures_contextvars.py new file mode 100644 index 00000000..ff79e17e --- /dev/null +++ b/tests/async_fixtures/test_async_fixtures_contextvars.py @@ -0,0 +1,247 @@ +""" +Regression test for https://github.com/pytest-dev/pytest-asyncio/issues/127: +contextvars were not properly maintained among fixtures and tests. +""" + +from __future__ import annotations + +import sys +from textwrap import dedent + +import pytest +from pytest import Pytester + +_prelude = dedent( + """ + import pytest + import pytest_asyncio + from contextlib import contextmanager + from contextvars import ContextVar + + _context_var = ContextVar("context_var") + + @contextmanager + def context_var_manager(value): + token = _context_var.set(value) + try: + yield + finally: + _context_var.reset(token) +""" +) + + +def test_var_from_sync_generator_propagates_to_async(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + _prelude + + dedent( + """ + @pytest.fixture + def var_fixture(): + with context_var_manager("value"): + yield + + @pytest_asyncio.fixture + async def check_var_fixture(var_fixture): + assert _context_var.get() == "value" + + @pytest.mark.asyncio + async def test(check_var_fixture): + assert _context_var.get() == "value" + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +@pytest.mark.xfail( + sys.version_info < (3, 11), + reason="requires asyncio Task context support", + strict=True, +) +def test_var_from_async_generator_propagates_to_sync(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + _prelude + + dedent( + """ + @pytest_asyncio.fixture + async def var_fixture(): + with context_var_manager("value"): + yield + + @pytest.fixture + def check_var_fixture(var_fixture): + assert _context_var.get() == "value" + + @pytest.mark.asyncio + async def test(check_var_fixture): + assert _context_var.get() == "value" + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +@pytest.mark.xfail( + sys.version_info < (3, 11), + reason="requires asyncio Task context support", + strict=True, +) +def test_var_from_async_fixture_propagates_to_sync(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + _prelude + + dedent( + """ + @pytest_asyncio.fixture + async def var_fixture(): + _context_var.set("value") + # Rely on async fixture teardown to reset the context var. + + @pytest.fixture + def check_var_fixture(var_fixture): + assert _context_var.get() == "value" + + def test(check_var_fixture): + assert _context_var.get() == "value" + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +@pytest.mark.xfail( + sys.version_info < (3, 11), + reason="requires asyncio Task context support", + strict=True, +) +def test_var_from_generator_reset_before_previous_fixture_cleanup(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + _prelude + + dedent( + """ + @pytest_asyncio.fixture + async def no_var_fixture(): + with pytest.raises(LookupError): + _context_var.get() + yield + with pytest.raises(LookupError): + _context_var.get() + + @pytest_asyncio.fixture + async def var_fixture(no_var_fixture): + with context_var_manager("value"): + yield + + @pytest.mark.asyncio + async def test(var_fixture): + assert _context_var.get() == "value" + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +@pytest.mark.xfail( + sys.version_info < (3, 11), + reason="requires asyncio Task context support", + strict=True, +) +def test_var_from_fixture_reset_before_previous_fixture_cleanup(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + _prelude + + dedent( + """ + @pytest_asyncio.fixture + async def no_var_fixture(): + with pytest.raises(LookupError): + _context_var.get() + yield + with pytest.raises(LookupError): + _context_var.get() + + @pytest_asyncio.fixture + async def var_fixture(no_var_fixture): + _context_var.set("value") + # Rely on async fixture teardown to reset the context var. + + @pytest.mark.asyncio + async def test(var_fixture): + assert _context_var.get() == "value" + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +@pytest.mark.xfail( + sys.version_info < (3, 11), + reason="requires asyncio Task context support", + strict=True, +) +def test_var_previous_value_restored_after_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + _prelude + + dedent( + """ + @pytest_asyncio.fixture + async def var_fixture_1(): + with context_var_manager("value1"): + yield + assert _context_var.get() == "value1" + + @pytest_asyncio.fixture + async def var_fixture_2(var_fixture_1): + with context_var_manager("value2"): + yield + assert _context_var.get() == "value2" + + @pytest.mark.asyncio + async def test(var_fixture_2): + assert _context_var.get() == "value2" + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +@pytest.mark.xfail( + sys.version_info < (3, 11), + reason="requires asyncio Task context support", + strict=True, +) +def test_var_set_to_existing_value_ok(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + _prelude + + dedent( + """ + @pytest_asyncio.fixture + async def var_fixture(): + with context_var_manager("value"): + yield + + @pytest_asyncio.fixture + async def same_var_fixture(var_fixture): + with context_var_manager(_context_var.get()): + yield + + @pytest.mark.asyncio + async def test(same_var_fixture): + assert _context_var.get() == "value" + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) diff --git a/tests/async_fixtures/test_async_fixtures_scope.py b/tests/async_fixtures/test_async_fixtures_scope.py index a25934a8..7fbed781 100644 --- a/tests/async_fixtures/test_async_fixtures_scope.py +++ b/tests/async_fixtures/test_async_fixtures_scope.py @@ -3,6 +3,8 @@ module-scoped too. """ +from __future__ import annotations + import asyncio import pytest diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py index bc6826bb..199ecbca 100644 --- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py +++ b/tests/async_fixtures/test_async_fixtures_with_finalizer.py @@ -1,8 +1,12 @@ +from __future__ import annotations + import asyncio import functools import pytest +import pytest_asyncio + @pytest.mark.asyncio(loop_scope="module") async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer): @@ -25,7 +29,7 @@ def event_loop(): loop.close() -@pytest.fixture(scope="module") +@pytest_asyncio.fixture(loop_scope="module", scope="module") async def port_with_event_loop_finalizer(request): def port_finalizer(finalizer): async def port_afinalizer(): @@ -40,7 +44,7 @@ async def port_afinalizer(): return True -@pytest.fixture(scope="module") +@pytest_asyncio.fixture(loop_scope="module", scope="module") async def port_with_get_event_loop_finalizer(request): def port_finalizer(finalizer): async def port_afinalizer(): diff --git a/tests/async_fixtures/test_async_gen_fixtures.py b/tests/async_fixtures/test_async_gen_fixtures.py index 2b198f2b..ddc2f5be 100644 --- a/tests/async_fixtures/test_async_gen_fixtures.py +++ b/tests/async_fixtures/test_async_gen_fixtures.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import unittest.mock import pytest diff --git a/tests/async_fixtures/test_nested.py b/tests/async_fixtures/test_nested.py index da7ee3a1..72b5129a 100644 --- a/tests/async_fixtures/test_nested.py +++ b/tests/async_fixtures/test_nested.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import pytest diff --git a/tests/async_fixtures/test_parametrized_loop.py b/tests/async_fixtures/test_parametrized_loop.py index 2bdbe5e8..ca2cb5c7 100644 --- a/tests/async_fixtures/test_parametrized_loop.py +++ b/tests/async_fixtures/test_parametrized_loop.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester diff --git a/tests/async_fixtures/test_shared_module_fixture.py b/tests/async_fixtures/test_shared_module_fixture.py index ff1cb62b..3295c83a 100644 --- a/tests/async_fixtures/test_shared_module_fixture.py +++ b/tests/async_fixtures/test_shared_module_fixture.py @@ -1,15 +1,18 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester def test_asyncio_mark_provides_package_scoped_loop_strict_mode(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", conftest=dedent( """\ import pytest_asyncio - @pytest_asyncio.fixture(scope="module") + @pytest_asyncio.fixture(loop_scope="module", scope="module") async def async_shared_module_fixture(): return True """ diff --git a/tests/conftest.py b/tests/conftest.py index 4aa8c89a..76e2026f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import pytest diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py index 2d2171bd..4b185f62 100644 --- a/tests/hypothesis/test_base.py +++ b/tests/hypothesis/test_base.py @@ -1,7 +1,10 @@ -"""Tests for the Hypothesis integration, which wraps async functions in a +""" +Tests for the Hypothesis integration, which wraps async functions in a sync shim for Hypothesis. """ +from __future__ import annotations + from textwrap import dedent import pytest @@ -10,6 +13,7 @@ def test_hypothesis_given_decorator_before_asyncio_mark(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -42,6 +46,7 @@ async def test_mark_and_parametrize(x, y): def test_can_use_explicit_event_loop_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = module") pytester.makepyfile( dedent( """\ @@ -78,6 +83,7 @@ async def test_explicit_fixture_request(event_loop, n): def test_async_auto_marked(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -100,6 +106,7 @@ async def test_hypothesis(n: int): def test_sync_not_auto_marked(pytester: Pytester): """Assert that synchronous Hypothesis functions are not marked with asyncio""" + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ diff --git a/tests/loop_fixture_scope/conftest.py b/tests/loop_fixture_scope/conftest.py index 6b9a7649..4e8b06de 100644 --- a/tests/loop_fixture_scope/conftest.py +++ b/tests/loop_fixture_scope/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import pytest diff --git a/tests/loop_fixture_scope/test_loop_fixture_scope.py b/tests/loop_fixture_scope/test_loop_fixture_scope.py index eb4be8c9..eb1bae58 100644 --- a/tests/loop_fixture_scope/test_loop_fixture_scope.py +++ b/tests/loop_fixture_scope/test_loop_fixture_scope.py @@ -1,5 +1,7 @@ """Unit tests for overriding the event loop with a larger scoped one.""" +from __future__ import annotations + import asyncio import pytest diff --git a/tests/markers/test_class_scope.py b/tests/markers/test_class_scope.py index baac5869..4bddb4b8 100644 --- a/tests/markers/test_class_scope.py +++ b/tests/markers/test_class_scope.py @@ -1,5 +1,7 @@ """Test if pytestmark works when defined on a class.""" +from __future__ import annotations + import asyncio from textwrap import dedent @@ -30,6 +32,7 @@ def sample_fixture(): def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_functions( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -56,6 +59,7 @@ async def test_this_runs_in_same_loop(self): def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_class( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -81,6 +85,7 @@ async def test_this_runs_in_same_loop(self): def test_asyncio_mark_raises_when_class_scoped_is_request_without_class( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -101,6 +106,7 @@ async def test_has_no_surrounding_class(): def test_asyncio_mark_is_inherited_to_subclasses(pytester: pytest.Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -129,6 +135,7 @@ async def test_this_runs_in_same_loop(self): def test_asyncio_mark_respects_the_loop_policy( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -166,6 +173,7 @@ async def test_does_not_use_custom_event_loop_policy(): def test_asyncio_mark_respects_parametrized_loop_policies( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -197,6 +205,7 @@ async def test_parametrized_loop(self, request): def test_asyncio_mark_provides_class_scoped_loop_to_fixtures( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -226,6 +235,7 @@ async def test_runs_is_same_loop_as_fixture(self, my_fixture): def test_asyncio_mark_allows_combining_class_scoped_fixture_with_function_scoped_test( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -237,7 +247,7 @@ def test_asyncio_mark_allows_combining_class_scoped_fixture_with_function_scoped loop: asyncio.AbstractEventLoop class TestMixedScopes: - @pytest_asyncio.fixture(scope="class") + @pytest_asyncio.fixture(loop_scope="class", scope="class") async def async_fixture(self): global loop loop = asyncio.get_running_loop() @@ -257,6 +267,7 @@ async def test_runs_in_different_loop_as_fixture(self, async_fixture): def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -292,6 +303,7 @@ async def test_does_not_fail(self, sets_event_loop_to_none, n): def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( pytester: pytest.Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index 81260006..c17a6225 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester def test_asyncio_mark_provides_function_scoped_loop_strict_mode(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -29,6 +32,7 @@ async def test_does_not_run_in_same_loop(): def test_loop_scope_function_provides_function_scoped_event_loop(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -54,6 +58,7 @@ async def test_does_not_run_in_same_loop(): def test_raises_when_scope_and_loop_scope_arguments_are_present(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -70,6 +75,7 @@ async def test_raises(): def test_warns_when_scope_argument_is_present(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -89,6 +95,7 @@ async def test_warns(): def test_function_scope_supports_explicit_event_loop_fixture_request( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -111,6 +118,7 @@ async def test_remember_loop(event_loop): def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -141,6 +149,7 @@ async def test_uses_custom_event_loop_policy(): def test_asyncio_mark_respects_parametrized_loop_policies( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -178,6 +187,7 @@ async def test_parametrized_loop(): def test_asyncio_mark_provides_function_scoped_loop_to_fixtures( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -208,6 +218,7 @@ async def test_runs_is_same_loop_as_fixture(my_fixture): def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -242,6 +253,7 @@ async def test_does_not_fail(sets_event_loop_to_none, n): def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -260,6 +272,7 @@ async def test_anything(): def test_asyncio_mark_does_not_duplicate_other_marks_in_auto_mode( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makeconftest( dedent( """\ diff --git a/tests/markers/test_invalid_arguments.py b/tests/markers/test_invalid_arguments.py index a4f4b070..2d5c3552 100644 --- a/tests/markers/test_invalid_arguments.py +++ b/tests/markers/test_invalid_arguments.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent import pytest @@ -60,7 +62,7 @@ async def test_anything(): result = pytester.runpytest_subprocess() result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( - ["*ValueError: mark.asyncio accepts only a keyword argument 'scope'*"] + ["*ValueError: mark.asyncio accepts only a keyword argument 'loop_scope'*"] ) diff --git a/tests/markers/test_mixed_scope.py b/tests/markers/test_mixed_scope.py new file mode 100644 index 00000000..40eaaa35 --- /dev/null +++ b/tests/markers/test_mixed_scope.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from textwrap import dedent + +from pytest import Pytester + + +def test_function_scoped_loop_restores_previous_loop_scope(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + + module_loop: asyncio.AbstractEventLoop + + @pytest.mark.asyncio(loop_scope="module") + async def test_remember_loop(): + global module_loop + module_loop = asyncio.get_running_loop() + + @pytest.mark.asyncio(loop_scope="function") + async def test_with_function_scoped_loop(): + pass + + @pytest.mark.asyncio(loop_scope="module") + async def test_runs_in_same_loop(): + global module_loop + assert asyncio.get_running_loop() is module_loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=3) diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index 5280ed7e..7dbdbb7f 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester def test_asyncio_mark_works_on_module_level(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -56,6 +59,7 @@ def sample_fixture(): def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -88,6 +92,7 @@ async def test_this_runs_in_same_loop(self): def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -109,6 +114,7 @@ async def test_remember_loop(event_loop): def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", custom_policy=dedent( @@ -163,6 +169,7 @@ async def test_does_not_use_custom_event_loop_policy(): def test_asyncio_mark_respects_parametrized_loop_policies( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -194,6 +201,7 @@ async def test_parametrized_loop(): def test_asyncio_mark_provides_module_scoped_loop_to_fixtures( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -206,7 +214,7 @@ def test_asyncio_mark_provides_module_scoped_loop_to_fixtures( loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="module") + @pytest_asyncio.fixture(loop_scope="module", scope="module") async def my_fixture(): global loop loop = asyncio.get_running_loop() @@ -224,6 +232,7 @@ async def test_runs_is_same_loop_as_fixture(my_fixture): def test_asyncio_mark_allows_combining_module_scoped_fixture_with_class_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -234,7 +243,7 @@ def test_asyncio_mark_allows_combining_module_scoped_fixture_with_class_scoped_t loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="module") + @pytest_asyncio.fixture(loop_scope="module", scope="module") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -255,6 +264,7 @@ async def test_runs_in_different_loop_as_fixture(self, async_fixture): def test_asyncio_mark_allows_combining_module_scoped_fixture_with_function_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -266,7 +276,7 @@ def test_asyncio_mark_allows_combining_module_scoped_fixture_with_function_scope loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="module") + @pytest_asyncio.fixture(loop_scope="module", scope="module") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -285,6 +295,7 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): def test_allows_combining_module_scoped_asyncgen_fixture_with_function_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -295,7 +306,7 @@ def test_allows_combining_module_scoped_asyncgen_fixture_with_function_scoped_te loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="module") + @pytest_asyncio.fixture(loop_scope="module", scope="module") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -315,6 +326,7 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -349,6 +361,7 @@ async def test_does_not_fail(sets_event_loop_to_none, n): def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ diff --git a/tests/markers/test_package_scope.py b/tests/markers/test_package_scope.py index 849967e8..204238a4 100644 --- a/tests/markers/test_package_scope.py +++ b/tests/markers/test_package_scope.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester @@ -6,6 +8,7 @@ def test_asyncio_mark_provides_package_scoped_loop_strict_mode(pytester: Pytester): package_name = pytester.path.name subpackage_name = "subpkg" + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", shared_module=dedent( @@ -69,6 +72,7 @@ async def test_subpackage_runs_in_different_loop(): def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_raises=dedent( @@ -90,6 +94,7 @@ async def test_remember_loop(event_loop): def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", conftest=dedent( @@ -151,6 +156,7 @@ async def test_also_uses_custom_event_loop_policy(): def test_asyncio_mark_respects_parametrized_loop_policies( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_parametrization=dedent( @@ -183,6 +189,7 @@ async def test_parametrized_loop(): def test_asyncio_mark_provides_package_scoped_loop_to_fixtures( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") package_name = pytester.path.name pytester.makepyfile( __init__="", @@ -194,7 +201,7 @@ def test_asyncio_mark_provides_package_scoped_loop_to_fixtures( from {package_name} import shared_module - @pytest_asyncio.fixture(scope="package") + @pytest_asyncio.fixture(loop_scope="package", scope="package") async def my_fixture(): shared_module.loop = asyncio.get_running_loop() """ @@ -229,6 +236,7 @@ async def test_runs_in_same_loop_as_fixture(my_fixture): def test_asyncio_mark_allows_combining_package_scoped_fixture_with_module_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -240,7 +248,7 @@ def test_asyncio_mark_allows_combining_package_scoped_fixture_with_module_scoped loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="package") + @pytest_asyncio.fixture(loop_scope="package", scope="package") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -259,6 +267,7 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): def test_asyncio_mark_allows_combining_package_scoped_fixture_with_class_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -270,7 +279,7 @@ def test_asyncio_mark_allows_combining_package_scoped_fixture_with_class_scoped_ loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="package") + @pytest_asyncio.fixture(loop_scope="package", scope="package") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -290,6 +299,7 @@ async def test_runs_in_different_loop_as_fixture(self, async_fixture): def test_asyncio_mark_allows_combining_package_scoped_fixture_with_function_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -301,7 +311,7 @@ def test_asyncio_mark_allows_combining_package_scoped_fixture_with_function_scop loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="package") + @pytest_asyncio.fixture(loop_scope="package", scope="package") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -320,6 +330,7 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_loop_is_none=dedent( @@ -355,6 +366,7 @@ async def test_does_not_fail(sets_event_loop_to_none, n): def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_module=dedent( diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py index 7900ef48..70e191b2 100644 --- a/tests/markers/test_session_scope.py +++ b/tests/markers/test_session_scope.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester @@ -5,6 +7,7 @@ def test_asyncio_mark_provides_session_scoped_loop_strict_mode(pytester: Pytester): package_name = pytester.path.name + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", shared_module=dedent( @@ -70,6 +73,7 @@ async def test_subpackage_runs_in_same_loop(): def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_raises=dedent( @@ -91,6 +95,7 @@ async def test_remember_loop(event_loop): def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", conftest=dedent( @@ -152,6 +157,7 @@ async def test_also_uses_custom_event_loop_policy(): def test_asyncio_mark_respects_parametrized_loop_policies( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_parametrization=dedent( @@ -185,6 +191,7 @@ def test_asyncio_mark_provides_session_scoped_loop_to_fixtures( pytester: Pytester, ): package_name = pytester.path.name + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", conftest=dedent( @@ -195,7 +202,7 @@ def test_asyncio_mark_provides_session_scoped_loop_to_fixtures( from {package_name} import shared_module - @pytest_asyncio.fixture(scope="session") + @pytest_asyncio.fixture(loop_scope="session", scope="session") async def my_fixture(): shared_module.loop = asyncio.get_running_loop() """ @@ -234,6 +241,7 @@ async def test_runs_in_same_loop_as_fixture(my_fixture): def test_asyncio_mark_allows_combining_session_scoped_fixture_with_package_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -245,7 +253,7 @@ def test_asyncio_mark_allows_combining_session_scoped_fixture_with_package_scope loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="session") + @pytest_asyncio.fixture(loop_scope="session", scope="session") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -264,6 +272,7 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): def test_asyncio_mark_allows_combining_session_scoped_fixture_with_module_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -275,7 +284,7 @@ def test_asyncio_mark_allows_combining_session_scoped_fixture_with_module_scoped loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="session") + @pytest_asyncio.fixture(loop_scope="session", scope="session") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -294,6 +303,7 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): def test_asyncio_mark_allows_combining_session_scoped_fixture_with_class_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -305,7 +315,7 @@ def test_asyncio_mark_allows_combining_session_scoped_fixture_with_class_scoped_ loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="session") + @pytest_asyncio.fixture(loop_scope="session", scope="session") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -325,6 +335,7 @@ async def test_runs_in_different_loop_as_fixture(self, async_fixture): def test_asyncio_mark_allows_combining_session_scoped_fixture_with_function_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -336,7 +347,7 @@ def test_asyncio_mark_allows_combining_session_scoped_fixture_with_function_scop loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="session") + @pytest_asyncio.fixture(loop_scope="session", scope="session") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -355,6 +366,7 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): def test_allows_combining_session_scoped_asyncgen_fixture_with_function_scoped_test( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_mixed_scopes=dedent( @@ -366,7 +378,7 @@ def test_allows_combining_session_scoped_asyncgen_fixture_with_function_scoped_t loop: asyncio.AbstractEventLoop - @pytest_asyncio.fixture(scope="session") + @pytest_asyncio.fixture(loop_scope="session", scope="session") async def async_fixture(): global loop loop = asyncio.get_running_loop() @@ -386,6 +398,7 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -420,6 +433,7 @@ async def test_does_not_fail(sets_event_loop_to_none, n): def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ diff --git a/tests/modes/test_auto_mode.py b/tests/modes/test_auto_mode.py index fc4d2df0..21c48d87 100644 --- a/tests/modes/test_auto_mode.py +++ b/tests/modes/test_auto_mode.py @@ -1,8 +1,13 @@ +from __future__ import annotations + from textwrap import dedent +from pytest import Pytester + -def test_auto_mode_cmdline(testdir): - testdir.makepyfile( +def test_auto_mode_cmdline(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import asyncio @@ -15,12 +20,21 @@ async def test_a(): """ ) ) - result = testdir.runpytest("--asyncio-mode=auto") + result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) -def test_auto_mode_cfg(testdir): - testdir.makepyfile( +def test_auto_mode_cfg(pytester: Pytester): + pytester.makeini( + dedent( + """\ + [pytest] + asyncio_default_fixture_loop_scope = function + asyncio_mode = auto + """ + ) + ) + pytester.makepyfile( dedent( """\ import asyncio @@ -33,13 +47,13 @@ async def test_a(): """ ) ) - testdir.makefile(".ini", pytest="[pytest]\nasyncio_mode = auto\n") - result = testdir.runpytest() + result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) -def test_auto_mode_async_fixture(testdir): - testdir.makepyfile( +def test_auto_mode_async_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import asyncio @@ -58,12 +72,13 @@ async def test_a(fixture_a): """ ) ) - result = testdir.runpytest("--asyncio-mode=auto") + result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) -def test_auto_mode_method_fixture(testdir): - testdir.makepyfile( +def test_auto_mode_method_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import asyncio @@ -85,12 +100,13 @@ async def test_a(self, fixture_a): """ ) ) - result = testdir.runpytest("--asyncio-mode=auto") + result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) -def test_auto_mode_static_method(testdir): - testdir.makepyfile( +def test_auto_mode_static_method(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import asyncio @@ -106,12 +122,13 @@ async def test_a(): """ ) ) - result = testdir.runpytest("--asyncio-mode=auto") + result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) -def test_auto_mode_static_method_fixture(testdir): - testdir.makepyfile( +def test_auto_mode_static_method_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import asyncio @@ -135,5 +152,5 @@ async def test_a(fixture_a): """ ) ) - result = testdir.runpytest("--asyncio-mode=auto") + result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) diff --git a/tests/modes/test_strict_mode.py b/tests/modes/test_strict_mode.py index 220410be..52cbb251 100644 --- a/tests/modes/test_strict_mode.py +++ b/tests/modes/test_strict_mode.py @@ -1,8 +1,13 @@ +from __future__ import annotations + from textwrap import dedent +from pytest import Pytester + -def test_strict_mode_cmdline(testdir): - testdir.makepyfile( +def test_strict_mode_cmdline(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import asyncio @@ -16,12 +21,21 @@ async def test_a(): """ ) ) - result = testdir.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=1) -def test_strict_mode_cfg(testdir): - testdir.makepyfile( +def test_strict_mode_cfg(pytester: Pytester): + pytester.makeini( + dedent( + """\ + [pytest] + asyncio_default_fixture_loop_scope = function + asyncio_mode = strict + """ + ) + ) + pytester.makepyfile( dedent( """\ import asyncio @@ -35,13 +49,13 @@ async def test_a(): """ ) ) - testdir.makefile(".ini", pytest="[pytest]\nasyncio_mode = strict\n") - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) -def test_strict_mode_method_fixture(testdir): - testdir.makepyfile( +def test_strict_mode_method_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import asyncio @@ -64,12 +78,13 @@ async def test_a(self, fixture_a): """ ) ) - result = testdir.runpytest("--asyncio-mode=auto") + result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) -def test_strict_mode_ignores_unmarked_coroutine(testdir): - testdir.makepyfile( +def test_strict_mode_ignores_unmarked_coroutine(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import pytest @@ -79,13 +94,14 @@ async def test_anything(): """ ) ) - result = testdir.runpytest_subprocess("--asyncio-mode=strict", "-W default") + result = pytester.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*"]) -def test_strict_mode_ignores_unmarked_fixture(testdir): - testdir.makepyfile( +def test_strict_mode_ignores_unmarked_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import pytest @@ -100,7 +116,7 @@ async def test_anything(any_fixture): """ ) ) - result = testdir.runpytest_subprocess("--asyncio-mode=strict", "-W default") + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") result.assert_outcomes(skipped=1, warnings=2) result.stdout.fnmatch_lines( [ @@ -108,3 +124,90 @@ async def test_anything(any_fixture): "*coroutine 'any_fixture' was never awaited*", ], ) + + +def test_strict_mode_marked_test_unmarked_fixture_warning(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import pytest + + # Not using pytest_asyncio.fixture + @pytest.fixture() + async def any_fixture(): + pass + + @pytest.mark.asyncio + async def test_anything(any_fixture): + # suppress unawaited coroutine warning + try: + any_fixture.send(None) + except StopIteration: + pass + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") + result.assert_outcomes(passed=1, failed=0, skipped=0, warnings=1) + result.stdout.fnmatch_lines( + [ + "*warnings summary*", + ( + "test_strict_mode_marked_test_unmarked_fixture_warning.py::" + "test_anything" + ), + ( + "*/pytest_asyncio/plugin.py:*: PytestDeprecationWarning: " + "asyncio test 'test_anything' requested async " + "@pytest.fixture 'any_fixture' in strict mode. " + "You might want to use @pytest_asyncio.fixture or switch to " + "auto mode. " + "This will become an error in future versions of flake8-asyncio." + ), + ], + ) + + +# autouse is not handled in any special way currently +def test_strict_mode_marked_test_unmarked_autouse_fixture_warning(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import pytest + + # Not using pytest_asyncio.fixture + @pytest.fixture(autouse=True) + async def any_fixture(): + pass + + @pytest.mark.asyncio + async def test_anything(any_fixture): + # suppress unawaited coroutine warning + try: + any_fixture.send(None) + except StopIteration: + pass + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + [ + "*warnings summary*", + ( + "test_strict_mode_marked_test_unmarked_autouse_fixture_warning.py::" + "test_anything" + ), + ( + "*/pytest_asyncio/plugin.py:*: PytestDeprecationWarning: " + "*asyncio test 'test_anything' requested async " + "@pytest.fixture 'any_fixture' in strict mode. " + "You might want to use @pytest_asyncio.fixture or switch to " + "auto mode. " + "This will become an error in future versions of flake8-asyncio." + ), + ], + ) diff --git a/tests/test_asyncio_fixture.py b/tests/test_asyncio_fixture.py index 2577cba0..91e5d8d4 100644 --- a/tests/test_asyncio_fixture.py +++ b/tests/test_asyncio_fixture.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import asyncio from textwrap import dedent import pytest +from pytest import Pytester import pytest_asyncio @@ -43,8 +46,9 @@ async def test_fixture_with_params(fixture_with_params): @pytest.mark.parametrize("mode", ("auto", "strict")) -def test_sync_function_uses_async_fixture(testdir, mode): - testdir.makepyfile( +def test_sync_function_uses_async_fixture(pytester: Pytester, mode): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ import pytest_asyncio @@ -60,5 +64,5 @@ def test_sync_function_uses_async_fixture(always_true): """ ) ) - result = testdir.runpytest(f"--asyncio-mode={mode}") + result = pytester.runpytest(f"--asyncio-mode={mode}") result.assert_outcomes(passed=1) diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py index 20ac173d..e22be989 100644 --- a/tests/test_asyncio_mark.py +++ b/tests/test_asyncio_mark.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester diff --git a/tests/test_dependent_fixtures.py b/tests/test_dependent_fixtures.py index dc70fe9c..2e53700a 100644 --- a/tests/test_dependent_fixtures.py +++ b/tests/test_dependent_fixtures.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import pytest diff --git a/tests/test_doctest.py b/tests/test_doctest.py index 5b79619a..d175789e 100644 --- a/tests/test_doctest.py +++ b/tests/test_doctest.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester def test_plugin_does_not_interfere_with_doctest_collection(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( '''\ @@ -20,6 +23,7 @@ def any_function(): def test_plugin_does_not_interfere_with_doctest_textfile_collection(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makefile(".txt", "") # collected as DoctestTextfile pytester.makepyfile( __init__="", diff --git a/tests/test_event_loop_fixture.py b/tests/test_event_loop_fixture.py index aaf591c9..447d15d5 100644 --- a/tests/test_event_loop_fixture.py +++ b/tests/test_event_loop_fixture.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester @@ -51,3 +53,30 @@ async def test_custom_policy_is_not_overwritten(): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=2) + + +def test_event_loop_fixture_handles_unclosed_async_gen( + pytester: Pytester, +): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + async def test_something(): + async def generator_fn(): + yield + yield + + gen = generator_fn() + await gen.__anext__() + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") + result.assert_outcomes(passed=1, warnings=0) diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py index ae260261..1e378643 100644 --- a/tests/test_event_loop_fixture_finalizer.py +++ b/tests/test_event_loop_fixture_finalizer.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester def test_event_loop_fixture_finalizer_returns_fresh_loop_after_test(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -11,7 +14,8 @@ def test_event_loop_fixture_finalizer_returns_fresh_loop_after_test(pytester: Py import pytest - loop = asyncio.get_event_loop_policy().get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) @pytest.mark.asyncio async def test_1(): @@ -36,6 +40,7 @@ def test_2(): def test_event_loop_fixture_finalizer_handles_loop_set_to_none_sync( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -53,6 +58,7 @@ def test_sync(event_loop): def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_without_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -72,6 +78,7 @@ async def test_async_without_explicit_fixture_request(): def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_with_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -94,6 +101,7 @@ async def test_async_with_explicit_fixture_request(event_loop): def test_event_loop_fixture_finalizer_raises_warning_when_fixture_leaves_loop_unclosed( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -121,6 +129,7 @@ async def test_ends_with_unclosed_loop(): def test_event_loop_fixture_finalizer_raises_warning_when_test_leaves_loop_unclosed( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py index 683f0963..04859ef7 100644 --- a/tests/test_event_loop_fixture_override_deprecation.py +++ b/tests/test_event_loop_fixture_override_deprecation.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester diff --git a/tests/test_explicit_event_loop_fixture_request.py b/tests/test_explicit_event_loop_fixture_request.py index e09893fa..c685ad84 100644 --- a/tests/test_explicit_event_loop_fixture_request.py +++ b/tests/test_explicit_event_loop_fixture_request.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester @@ -6,6 +8,7 @@ def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -27,6 +30,7 @@ async def test_coroutine_emits_warning(event_loop): def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_method( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -49,6 +53,7 @@ async def test_coroutine_emits_warning(self, event_loop): def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_staticmethod( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -72,6 +77,7 @@ async def test_coroutine_emits_warning(event_loop): def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -98,6 +104,7 @@ async def test_uses_fixture(emits_warning): def test_emit_warning_when_event_loop_is_explicitly_requested_in_async_gen_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -124,6 +131,7 @@ async def test_uses_fixture(emits_warning): def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_function( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -141,6 +149,7 @@ def test_uses_fixture(event_loop): def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_fixture( pytester: Pytester, ): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index f0271e59..a9ce4b35 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent import pytest diff --git a/tests/test_import.py b/tests/test_import.py index 9912ae0c..2272704a 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester def test_import_warning_does_not_cause_internal_error(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -19,6 +22,7 @@ async def test_errors_out(): def test_import_warning_in_package_does_not_cause_internal_error(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__=dedent( """\ @@ -37,6 +41,7 @@ async def test_errors_out(): def test_does_not_import_unrelated_packages(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pkg_dir = pytester.mkpydir("mypkg") pkg_dir.joinpath("__init__.py").write_text( dedent( diff --git a/tests/test_is_async_test.py b/tests/test_is_async_test.py index e0df54de..f99dc0d9 100644 --- a/tests/test_is_async_test.py +++ b/tests/test_is_async_test.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester def test_returns_false_for_sync_item(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -28,6 +31,7 @@ def pytest_collection_modifyitems(items): def test_returns_true_for_marked_coroutine_item_in_strict_mode(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -53,6 +57,7 @@ def pytest_collection_modifyitems(items): def test_returns_false_for_unmarked_coroutine_item_in_strict_mode(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -77,6 +82,7 @@ def pytest_collection_modifyitems(items): def test_returns_true_for_unmarked_coroutine_item_in_auto_mode(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py index c3713cc9..e6c852b9 100644 --- a/tests/test_multiloop.py +++ b/tests/test_multiloop.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester diff --git a/tests/test_port_factories.py b/tests/test_port_factories.py index cbbd47b4..713d747e 100644 --- a/tests/test_port_factories.py +++ b/tests/test_port_factories.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester diff --git a/tests/test_simple.py b/tests/test_simple.py index f5f52a8d..b8a34fb2 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -1,5 +1,7 @@ """Quick'n'dirty unit tests for provided fixtures and markers.""" +from __future__ import annotations + import asyncio from textwrap import dedent @@ -26,6 +28,7 @@ async def test_asyncio_marker(): def test_asyncio_marker_compatibility_with_xfail(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -45,6 +48,7 @@ async def test_asyncio_marker_fail(): def test_asyncio_auto_mode_compatibility_with_xfail(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -73,8 +77,10 @@ class TestMarkerInClassBasedTests: @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" @@ -101,8 +107,9 @@ async def test_event_loop_before_fixture(self, loop): assert await loop.run_in_executor(None, self.foo) == 1 -def test_invalid_asyncio_mode(testdir): - result = testdir.runpytest("-o", "asyncio_mode=True") +def test_invalid_asyncio_mode(pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + result = pytester.runpytest("-o", "asyncio_mode=True") result.stderr.no_fnmatch_line("INTERNALERROR> *") result.stderr.fnmatch_lines( "ERROR: 'True' is not a valid asyncio_mode. Valid modes: auto, strict." diff --git a/tests/test_skips.py b/tests/test_skips.py index 5d7aa303..d32273cd 100644 --- a/tests/test_skips.py +++ b/tests/test_skips.py @@ -1,9 +1,12 @@ +from __future__ import annotations + from textwrap import dedent from pytest import Pytester def test_asyncio_strict_mode_skip(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -22,6 +25,7 @@ async def test_no_warning_on_skip(): def test_asyncio_auto_mode_skip(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -39,6 +43,7 @@ async def test_no_warning_on_skip(): def test_asyncio_strict_mode_module_level_skip(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -57,6 +62,7 @@ async def test_is_skipped(): def test_asyncio_auto_mode_module_level_skip(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -74,6 +80,7 @@ async def test_is_skipped(): def test_asyncio_auto_mode_wrong_skip_usage(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -91,6 +98,7 @@ async def test_is_skipped(): def test_unittest_skiptest_compatibility(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -108,6 +116,7 @@ async def test_is_skipped(): def test_skip_in_module_does_not_skip_package(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( __init__="", test_skip=dedent( diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 3d91e7b1..c32ba964 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -1,5 +1,7 @@ """Tests for using subprocesses in tests.""" +from __future__ import annotations + import asyncio.subprocess import sys diff --git a/tools/get-version.py b/tools/get-version.py index c29081b9..9d24b6a5 100644 --- a/tools/get-version.py +++ b/tools/get-version.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import sys from importlib import metadata diff --git a/tox.ini b/tox.ini index 79e96fa6..9a0cf93b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -minversion = 3.14.0 -envlist = py38, py39, py310, py311, py312, py13, pytest-min, docs +minversion = 4.9.0 +envlist = py39, py310, py311, py312, py313, pytest-min, docs isolated_build = true passenv = CI @@ -71,8 +71,7 @@ skip_install = false [gh-actions] python = - 3.8: py38, pytest-min - 3.9: py39 + 3.9: py39, pytest-min 3.10: py310 3.11: py311 3.12: py312