diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..a30293ca --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @asvetlov @seifertm @Tinche diff --git a/.github/actionlint-matcher.json b/.github/actionlint-matcher.json new file mode 100644 index 00000000..a99709f7 --- /dev/null +++ b/.github/actionlint-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "actionlint", + "pattern": [ + { + "code": 5, + "column": 3, + "file": 1, + "line": 2, + "message": 4, + "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$" + } + ] + } + ] +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..28b158af --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +--- +version: 2 +updates: +- package-ecosystem: pip + directory: /dependencies/default + schedule: + interval: weekly + open-pull-requests-limit: 10 + target-branch: master +- package-ecosystem: github-actions + directory: / + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..5c5876b8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,132 @@ +--- +name: CI + +on: + push: + branches: [master] + tags: [v*] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + lint: + name: Run linters + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + prerelease: ${{ steps.version.outputs.prerelease }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install GitHub matcher for ActionLint checker + run: | + echo "::add-matcher::.github/actionlint-matcher.json" + - name: Install check-wheel-content, and twine + run: python -m pip install build check-wheel-contents tox twine + - name: Build package + run: python -m build + - name: Run tox for linter + run: python -m tox -e lint + - name: List result + run: ls -l dist + - name: Check wheel contents + run: check-wheel-contents dist/*.whl + - name: Check long_description + run: python -m twine check dist/* + - name: Get version info + id: version + run: tox -e version-info + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist + + test: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + env: + USING_COVERAGE: 3.7,3.8,3.9,3.10,3.11 + + strategy: + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10', 3.11-dev] + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + set -xe + python -VV + python -m site + python -m pip install --upgrade pip + python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions + - name: Run tox targets for ${{ matrix.python-version }} + run: python -m tox + + - name: Prepare coverage artifact + if: ${{ contains(env.USING_COVERAGE, matrix.python-version) }} + uses: aio-libs/prepare-coverage@v21.9.1 + + check: + name: Check + if: always() + needs: [lint, test] + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + - name: Upload coverage + uses: aio-libs/upload-coverage@v21.9.4 + + deploy: + name: Deploy + environment: release + # Run only on pushing a tag + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + needs: [lint, check] + runs-on: ubuntu-latest + steps: + - name: Install pandoc + run: | + sudo apt-get install -y pandoc + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Download distributions + uses: actions/download-artifact@v3 + with: + name: dist + path: dist + - name: Collected dists + run: | + tree dist + - name: Convert README.rst to Markdown + run: | + pandoc -s -o README.md README.rst + - name: PyPI upload + uses: pypa/gh-action-pypi-publish@v1.5.1 + with: + packages_dir: dist + password: ${{ secrets.PYPI_API_TOKEN }} + - name: GitHub Release + uses: ncipollo/release-action@v1 + with: + name: pytest-asyncio ${{ needs.lint.outputs.version }} + artifacts: dist/* + bodyFile: README.md + prerelease: ${{ needs.lint.outputs.prerelease }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 09758085..7dd9b771 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,7 @@ htmlcov/ .tox/ .coverage .coverage.* -.cache +.pytest_cache nosetests.xml coverage.xml *,cover @@ -58,4 +58,12 @@ docs/_build/ target/ .venv* -.idea \ No newline at end of file +.idea +.vscode + +# pyenv +.python-version + + +# generated by setuptools_scm +pytest_asyncio/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..8c15003b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,67 @@ +--- +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-merge-conflict + exclude: rst$ +- repo: https://github.com/asottile/yesqa + rev: v1.3.0 + hooks: + - id: yesqa +- repo: https://github.com/Zac-HD/shed + rev: 0.6.0 # 0.7 does not support Python 3.7 + hooks: + - id: shed + args: + - --refactor + - --py37-plus + types_or: + - python + - markdown + - rst +- repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt + rev: 0.1.1 + hooks: + - id: yamlfmt + args: [--mapping, '2', --sequence, '2', --offset, '0'] +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: fix-encoding-pragma + args: [--remove] + - id: check-case-conflict + - id: check-json + - id: check-xml + - id: check-yaml + - id: debug-statements +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + language_version: python3 +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-use-type-annotations +- repo: https://github.com/rhysd/actionlint + rev: v1.6.8 + hooks: + - id: actionlint-docker + args: + - -ignore + - 'SC2155:' + - -ignore + - 'SC2086:' + - -ignore + - 'SC1004:' +- repo: https://github.com/sirosen/check-jsonschema + rev: 0.9.1 + hooks: + - id: check-github-actions +ci: + skip: + - actionlint-docker + - check-github-actions diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b93377ad..00000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: python - -matrix: - include: - - python: 3.5 - env: TOX_ENV=py35 - - python: 3.6 - env: TOX_ENV=py36 - - python: 3.7 - env: TOX_ENV=py37 - - python: 3.8 - env: TOX_ENV=py38 - -install: pip install tox-travis coveralls - -script: tox -e $TOX_ENV - -after_success: - - tox -e coverage-report - - coveralls diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 00000000..5c84b46e --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,179 @@ +========= +Changelog +========= + +0.20.1 (22-10-21) +================= +- Fixes an issue that warned about using an old version of pytest, even though the most recent version was installed. `#430 `_ + +0.20.0 (22-10-21) +================= +- BREAKING: Removed *legacy* mode. If you're upgrading from v0.19 and you haven't configured ``asyncio_mode = legacy``, you can upgrade without taking any additional action. If you're upgrading from an earlier version or you have explicitly enabled *legacy* mode, you need to switch to *auto* or *strict* mode before upgrading to this version. +- Deprecate use of pytest v6. +- Fixed an issue which prevented fixture setup from being cached. `#404 `_ + +0.19.0 (22-07-13) +================= +- BREAKING: The default ``asyncio_mode`` is now *strict*. `#293 `_ +- Removes `setup.py` since all relevant configuration is present `setup.cfg`. Users requiring an editable installation of pytest-asyncio need to use pip v21.1 or newer. `#283 `_ +- Declare support for Python 3.11. + +0.18.3 (22-03-25) +================= +- Adds `pytest-trio `_ to the test dependencies +- Fixes a bug that caused pytest-asyncio to try to set up async pytest_trio fixtures in strict mode. `#298 `_ + +0.18.2 (22-03-03) +================= +- Fix asyncio auto mode not marking static methods. `#295 `_ +- Fix a compatibility issue with Hypothesis 6.39.0. `#302 `_ + +0.18.1 (22-02-10) +================= +- Fixes a regression that prevented async fixtures from working in synchronous tests. `#286 `_ + +0.18.0 (22-02-07) +================= + +- Raise a warning if @pytest.mark.asyncio is applied to non-async function. `#275 `_ +- Support parametrized ``event_loop`` fixture. `#278 `_ + +0.17.2 (22-01-17) +================= + +- Require ``typing-extensions`` on Python<3.8 only. `#269 `_ +- Fix a regression in tests collection introduced by 0.17.1, the plugin works fine with non-python tests again. `#267 `_ + + +0.17.1 (22-01-16) +================= +- Fixes a bug that prevents async Hypothesis tests from working without explicit ``asyncio`` marker when ``--asyncio-mode=auto`` is set. `#258 `_ +- Fixed a bug that closes the default event loop if the loop doesn't exist `#257 `_ +- Added type annotations. `#198 `_ +- Show asyncio mode in pytest report headers. `#266 `_ +- Relax ``asyncio_mode`` type definition; it allows to support pytest 6.1+. `#262 `_ + +0.17.0 (22-01-13) +================= +- `pytest-asyncio` no longer alters existing event loop policies. `#168 `_, `#188 `_ +- Drop support for Python 3.6 +- Fixed an issue when pytest-asyncio was used in combination with `flaky` or inherited asynchronous Hypothesis tests. `#178 `_ `#231 `_ +- Added `flaky `_ to test dependencies +- Added ``unused_udp_port`` and ``unused_udp_port_factory`` fixtures (similar to ``unused_tcp_port`` and ``unused_tcp_port_factory`` counterparts. `#99 `_ +- Added the plugin modes: *strict*, *auto*, and *legacy*. See `documentation `_ for details. `#125 `_ +- Correctly process ``KeyboardInterrupt`` during async fixture setup phase `#219 `_ + +0.16.0 (2021-10-16) +=================== +- Add support for Python 3.10 + +0.15.1 (2021-04-22) +=================== +- Hotfix for errors while closing event loops while replacing them. + `#209 `_ + `#210 `_ + +0.15.0 (2021-04-19) +=================== +- Add support for Python 3.9 +- Abandon support for Python 3.5. If you still require support for Python 3.5, please use pytest-asyncio v0.14 or earlier. +- Set ``unused_tcp_port_factory`` fixture scope to 'session'. + `#163 `_ +- Properly close event loops when replacing them. + `#208 `_ + +0.14.0 (2020-06-24) +=================== +- Fix `#162 `_, and ``event_loop`` fixture behavior now is coherent on all scopes. + `#164 `_ + +0.12.0 (2020-05-04) +=================== +- Run the event loop fixture as soon as possible. This helps with fixtures that have an implicit dependency on the event loop. + `#156 `_ + +0.11.0 (2020-04-20) +=================== +- Test on 3.8, drop 3.3 and 3.4. Stick to 0.10 for these versions. + `#152 `_ +- Use the new Pytest 5.4.0 Function API. We therefore depend on pytest >= 5.4.0. + `#142 `_ +- Better ``pytest.skip`` support. + `#126 `_ + +0.10.0 (2019-01-08) +==================== +- ``pytest-asyncio`` integrates with `Hypothesis `_ + to support ``@given`` on async test functions using ``asyncio``. + `#102 `_ +- Pytest 4.1 support. + `#105 `_ + +0.9.0 (2018-07-28) +================== +- Python 3.7 support. +- Remove ``event_loop_process_pool`` fixture and + ``pytest.mark.asyncio_process_pool`` marker (see + https://bugs.python.org/issue34075 for deprecation and removal details) + +0.8.0 (2017-09-23) +================== +- Improve integration with other packages (like aiohttp) with more careful event loop handling. + `#64 `_ + +0.7.0 (2017-09-08) +================== +- Python versions pre-3.6 can use the async_generator library for async fixtures. + `#62 ` + +0.6.0 (2017-05-28) +================== +- Support for Python versions pre-3.5 has been dropped. +- ``pytestmark`` now works on both module and class level. +- The ``forbid_global_loop`` parameter has been removed. +- Support for async and async gen fixtures has been added. + `#45 `_ +- The deprecation warning regarding ``asyncio.async()`` has been fixed. + `#51 `_ + +0.5.0 (2016-09-07) +================== +- Introduced a changelog. + `#31 `_ +- The ``event_loop`` fixture is again responsible for closing itself. + This makes the fixture slightly harder to correctly override, but enables + other fixtures to depend on it correctly. + `#30 `_ +- Deal with the event loop policy by wrapping a special pytest hook, + ``pytest_fixture_setup``. This allows setting the policy before fixtures + dependent on the ``event_loop`` fixture run, thus allowing them to take + advantage of the ``forbid_global_loop`` parameter. As a consequence of this, + we now depend on pytest 3.0. + `#29 `_ + +0.4.1 (2016-06-01) +================== +- Fix a bug preventing the propagation of exceptions from the plugin. + `#25 `_ + +0.4.0 (2016-05-30) +================== +- Make ``event_loop`` fixtures simpler to override by closing them in the + plugin, instead of directly in the fixture. + `#21 `_ +- Introduce the ``forbid_global_loop`` parameter. + `#21 `_ + +0.3.0 (2015-12-19) +================== +- Support for Python 3.5 ``async``/``await`` syntax. + `#17 `_ + +0.2.0 (2015-08-01) +================== +- ``unused_tcp_port_factory`` fixture. + `#10 `_ + +0.1.1 (2015-04-23) +================== +Initial release. diff --git a/LICENSE b/LICENSE index e06d2081..5c304d1a 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,3 @@ Apache License WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..fdf813e9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include CHANGELOG.rst + +recursive-exclude .github * +exclude .gitignore +exclude .pre-commit-config.yaml diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..2b0216f9 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +.PHONY: clean clean-build clean-pyc clean-test lint test + +clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + +lint: +# CI env-var is set by GitHub actions +ifdef CI + python -m pre_commit run --all-files --show-diff-on-failure +else + python -m pre_commit run --all-files +endif + python -m mypy pytest_asyncio --show-error-codes + +test: + coverage run -m pytest tests + coverage xml + coverage report + +install: + pip install -U pre-commit + pre-commit install diff --git a/README.rst b/README.rst index 6ea6014c..d62087bb 100644 --- a/README.rst +++ b/README.rst @@ -3,13 +3,15 @@ pytest-asyncio: pytest support for asyncio .. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg :target: https://pypi.python.org/pypi/pytest-asyncio -.. image:: https://travis-ci.org/pytest-dev/pytest-asyncio.svg?branch=master - :target: https://travis-ci.org/pytest-dev/pytest-asyncio -.. image:: https://coveralls.io/repos/pytest-dev/pytest-asyncio/badge.svg - :target: https://coveralls.io/r/pytest-dev/pytest-asyncio +.. image:: https://github.com/pytest-dev/pytest-asyncio/workflows/CI/badge.svg + :target: https://github.com/pytest-dev/pytest-asyncio/actions?workflow=CI +.. image:: https://codecov.io/gh/pytest-dev/pytest-asyncio/branch/master/graph/badge.svg + :target: https://codecov.io/gh/pytest-dev/pytest-asyncio .. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg :target: https://github.com/pytest-dev/pytest-asyncio :alt: Supported Python versions +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black pytest-asyncio is an Apache2 licensed library, written in Python, for testing asyncio code with pytest. @@ -23,7 +25,7 @@ provides useful fixtures and markers to make testing easier. @pytest.mark.asyncio async def test_some_asyncio_code(): res = await library.do_something() - assert b'expected result' == res + assert b"expected result" == res pytest-asyncio has been strongly influenced by pytest-tornado_. @@ -33,10 +35,13 @@ Features -------- - fixtures for creating and injecting versions of the asyncio event loop -- fixtures for injecting unused tcp ports +- fixtures for injecting unused tcp/udp ports - pytest markers for treating tests as asyncio coroutines - easy testing with non-default event loops - support for `async def` fixtures and async generator fixtures +- support *auto* mode to handle all async fixtures and tests automatically by asyncio; + provide *strict* mode if a test suite should work with different async frameworks + simultaneously, e.g. ``asyncio`` and ``trio``. Installation ------------ @@ -49,47 +54,97 @@ To install pytest-asyncio, simply: This is enough for pytest to pick up pytest-asyncio. +Modes +----- + +Pytest-asyncio provides two modes: *auto* and *strict* with *strict* mode being the default. + +The mode can be set by ``asyncio_mode`` configuration option in `configuration file +`_: + +.. code-block:: ini + + # pytest.ini + [pytest] + asyncio_mode = auto + +The value can be overridden by command-line option for ``pytest`` invocation: + +.. code-block:: bash + + $ pytest tests --asyncio-mode=strict + +Auto mode +~~~~~~~~~ + +When the mode is auto, all discovered *async* tests are considered *asyncio-driven* even +if they have no ``@pytest.mark.asyncio`` marker. + +All async fixtures are considered *asyncio-driven* as well, even if they are decorated +with a regular ``@pytest.fixture`` decorator instead of dedicated +``@pytest_asyncio.fixture`` counterpart. + +*asyncio-driven* means that tests and fixtures are executed by ``pytest-asyncio`` +plugin. + +This mode requires the simplest tests and fixtures configuration and is +recommended for default usage *unless* the same project and its test suite should +execute tests from different async frameworks, e.g. ``asyncio`` and ``trio``. In this +case, auto-handling can break tests designed for other framework; please use *strict* +mode instead. + +Strict mode +~~~~~~~~~~~ + +Strict mode enforces ``@pytest.mark.asyncio`` and ``@pytest_asyncio.fixture`` usage. +Without these markers, tests and fixtures are not considered as *asyncio-driven*, other +pytest plugin can handle them. + +Please use this mode if multiple async frameworks should be combined in the same test +suite. + +This mode is used by default for the sake of project inter-compatibility. + + Fixtures -------- ``event_loop`` ~~~~~~~~~~~~~~ -Creates and injects a new instance of the default asyncio event loop. By -default, the loop will be closed at the end of the test (i.e. the default -fixture scope is ``function``). +Creates a new asyncio event loop based on the current event loop policy. The new loop +is available as the return value of this fixture or via `asyncio.get_running_loop `__. +The event loop is closed when the fixture scope ends. The fixture scope defaults +to ``function`` scope. Note that just using the ``event_loop`` fixture won't make your test function a coroutine. You'll need to interact with the event loop directly, using methods like ``event_loop.run_until_complete``. See the ``pytest.mark.asyncio`` marker for treating test functions like coroutines. -Simply using this fixture will not set the generated event loop as the -default asyncio event loop, or change the asyncio event loop policy in any way. -Use ``pytest.mark.asyncio`` for this purpose. - .. code-block:: python def test_http_client(event_loop): - url = 'http://httpbin.org/get' + url = "http://httpbin.org/get" resp = event_loop.run_until_complete(http_client(url)) - assert b'HTTP/1.1 200 OK' in resp + assert b"HTTP/1.1 200 OK" in resp -This fixture can be easily overridden in any of the standard pytest locations -(e.g. directly in the test file, or in ``conftest.py``) to use a non-default -event loop. This will take effect even if you're using the -``pytest.mark.asyncio`` marker and not the ``event_loop`` fixture directly. +The ``event_loop`` fixture can be overridden in any of the standard pytest locations, +e.g. directly in the test file, or in ``conftest.py``. This allows redefining the +fixture scope, for example: .. code-block:: python - @pytest.fixture + @pytest.fixture(scope="session") def event_loop(): - loop = MyCustomLoop() + policy = asyncio.get_event_loop_policy() + loop = policy.new_event_loop() yield loop loop.close() -If the ``pytest.mark.asyncio`` marker is applied, a pytest hook will -ensure the produced loop is set as the default global loop. -Fixtures depending on the ``event_loop`` fixture can expect the policy to be properly modified when they run. +If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture. + +If the ``pytest.mark.asyncio`` marker is applied to a test function, the ``event_loop`` +fixture will be requested automatically by the test function. ``unused_tcp_port`` ~~~~~~~~~~~~~~~~~~~ @@ -107,18 +162,27 @@ when several unused TCP ports are required in a test. port1, port2 = unused_tcp_port_factory(), unused_tcp_port_factory() ... +``unused_udp_port`` and ``unused_udp_port_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Work just like their TCP counterparts but return unused UDP ports. + + Async fixtures ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be coroutines or asynchronous generators. +Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``. .. code-block:: python3 - @pytest.fixture + import pytest_asyncio + + + @pytest_asyncio.fixture async def async_gen_fixture(): await asyncio.sleep(0.1) - yield 'a value' + yield "a value" + - @pytest.fixture(scope='module') + @pytest_asyncio.fixture(scope="module") async def async_fixture(): return await asyncio.sleep(0.1) @@ -127,18 +191,8 @@ to redefine the ``event_loop`` fixture to have the same or broader scope. Async fixtures need the event loop, and so must have the same or narrower scope than the ``event_loop`` fixture. -If you want to do this with Python 3.5, the ``yield`` statement must be replaced with ``await yield_()`` and the coroutine -function must be decorated with ``@async_generator``, like so: - -.. code-block:: python3 - - from async_generator import yield_, async_generator - - @pytest.fixture - @async_generator - async def async_gen_fixture(): - await asyncio.sleep(0.1) - await yield_('a value') +*auto* mode automatically converts async fixtures declared with the +standard ``@pytest.fixture`` decorator to *asyncio-driven* versions. Markers @@ -150,7 +204,7 @@ Mark your test coroutine with this marker and pytest will execute it as an asyncio task using the event loop provided by the ``event_loop`` fixture. See the introductory section for an example. -The event loop used can be overriden by overriding the ``event_loop`` fixture +The event loop used can be overridden by overriding the ``event_loop`` fixture (see above). In order to make your test code a little more concise, the pytest |pytestmark|_ @@ -161,119 +215,30 @@ Only test coroutines will be affected (by default, coroutines prefixed by .. code-block:: python import asyncio + import pytest # All test coroutines will be treated as marked. pytestmark = pytest.mark.asyncio + async def test_example(event_loop): """No marker!""" await asyncio.sleep(0, loop=event_loop) -.. |pytestmark| replace:: ``pytestmark`` -.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules +In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted, the marker is added +automatically to *async* test functions. -Changelog ---------- -0.13.0 (2020-06-24) -~~~~~~~~~~~~~~~~~~~ -- Fix `#162 `_, and ``event_loop`` fixture behavior now is coherent on all scopes. - `#164 `_ -0.12.0 (2020-05-04) -~~~~~~~~~~~~~~~~~~~ -- Run the event loop fixture as soon as possible. This helps with fixtures that have an implicit dependency on the event loop. - `#156 `_ +.. |pytestmark| replace:: ``pytestmark`` +.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules -0.11.0 (2020-04-20) -~~~~~~~~~~~~~~~~~~~ -- Test on 3.8, drop 3.3 and 3.4. Stick to 0.10 for these versions. - `#152 `_ -- Use the new Pytest 5.4.0 Function API. We therefore depend on pytest >= 5.4.0. - `#142 `_ -- Better ``pytest.skip`` support. - `#126 `_ - -0.10.0 (2019-01-08) -~~~~~~~~~~~~~~~~~~~~ -- ``pytest-asyncio`` integrates with `Hypothesis `_ - to support ``@given`` on async test functions using ``asyncio``. - `#102 `_ -- Pytest 4.1 support. - `#105 `_ - -0.9.0 (2018-07-28) -~~~~~~~~~~~~~~~~~~ -- Python 3.7 support. -- Remove ``event_loop_process_pool`` fixture and - ``pytest.mark.asyncio_process_pool`` marker (see - https://bugs.python.org/issue34075 for deprecation and removal details) - -0.8.0 (2017-09-23) -~~~~~~~~~~~~~~~~~~ -- Improve integration with other packages (like aiohttp) with more careful event loop handling. - `#64 `_ - -0.7.0 (2017-09-08) -~~~~~~~~~~~~~~~~~~ -- Python versions pre-3.6 can use the async_generator library for async fixtures. - `#62 ` - - -0.6.0 (2017-05-28) -~~~~~~~~~~~~~~~~~~ -- Support for Python versions pre-3.5 has been dropped. -- ``pytestmark`` now works on both module and class level. -- The ``forbid_global_loop`` parameter has been removed. -- Support for async and async gen fixtures has been added. - `#45 `_ -- The deprecation warning regarding ``asyncio.async()`` has been fixed. - `#51 `_ - -0.5.0 (2016-09-07) -~~~~~~~~~~~~~~~~~~ -- Introduced a changelog. - `#31 `_ -- The ``event_loop`` fixture is again responsible for closing itself. - This makes the fixture slightly harder to correctly override, but enables - other fixtures to depend on it correctly. - `#30 `_ -- Deal with the event loop policy by wrapping a special pytest hook, - ``pytest_fixture_setup``. This allows setting the policy before fixtures - dependent on the ``event_loop`` fixture run, thus allowing them to take - advantage of the ``forbid_global_loop`` parameter. As a consequence of this, - we now depend on pytest 3.0. - `#29 `_ - - -0.4.1 (2016-06-01) -~~~~~~~~~~~~~~~~~~ -- Fix a bug preventing the propagation of exceptions from the plugin. - `#25 `_ - -0.4.0 (2016-05-30) -~~~~~~~~~~~~~~~~~~ -- Make ``event_loop`` fixtures simpler to override by closing them in the - plugin, instead of directly in the fixture. - `#21 `_ -- Introduce the ``forbid_global_loop`` parameter. - `#21 `_ - -0.3.0 (2015-12-19) -~~~~~~~~~~~~~~~~~~ -- Support for Python 3.5 ``async``/``await`` syntax. - `#17 `_ - -0.2.0 (2015-08-01) -~~~~~~~~~~~~~~~~~~ -- ``unused_tcp_port_factory`` fixture. - `#10 `_ - - -0.1.1 (2015-04-23) -~~~~~~~~~~~~~~~~~~ -Initial release. +Note about unittest +------------------- +Test classes subclassing the standard `unittest `__ library are not supported, users +are recommended to use `unittest.IsolatedAsyncioTestCase `__ +or an async framework such as `asynctest `__. Contributing ------------ diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt new file mode 100644 index 00000000..cdaa6a6f --- /dev/null +++ b/dependencies/default/constraints.txt @@ -0,0 +1,24 @@ +async-generator==1.10 +attrs==22.1.0 +coverage==6.5.0 +flaky==3.7.0 +hypothesis==6.56.3 +idna==3.4 +importlib-metadata==5.0.0 +iniconfig==1.1.1 +mypy==0.982 +mypy-extensions==0.4.3 +outcome==1.2.0 +packaging==21.3 +pluggy==1.0.0 +py==1.11.0 +pyparsing==3.0.9 +pytest==7.1.3 +pytest-trio==0.7.0 +sniffio==1.3.0 +sortedcontainers==2.4.0 +tomli==2.0.1 +trio==0.22.0 +typed-ast==1.5.4 +typing_extensions==4.4.0 +zipp==3.9.0 diff --git a/dependencies/default/requirements.txt b/dependencies/default/requirements.txt new file mode 100644 index 00000000..01b2484e --- /dev/null +++ b/dependencies/default/requirements.txt @@ -0,0 +1,4 @@ +# Always adjust install_requires in setup.cfg and pytest-min-requirements.txt +# when changing runtime dependencies +pytest >= 6.1.0 +typing-extensions >= 3.7.2; python_version < "3.8" diff --git a/dependencies/pytest-min/constraints.txt b/dependencies/pytest-min/constraints.txt new file mode 100644 index 00000000..33f7948f --- /dev/null +++ b/dependencies/pytest-min/constraints.txt @@ -0,0 +1,22 @@ +async-generator==1.10 +attrs==21.4.0 +coverage==6.3.2 +flaky==3.7.0 +hypothesis==6.43.3 +idna==3.3 +iniconfig==1.1.1 +mypy==0.942 +mypy-extensions==0.4.3 +outcome==1.1.0 +packaging==21.3 +pluggy==0.13.1 +py==1.11.0 +pyparsing==3.0.8 +pytest==6.1.0 +pytest-trio==0.7.0 +sniffio==1.2.0 +sortedcontainers==2.4.0 +toml==0.10.2 +tomli==2.0.1 +trio==0.20.0 +typing_extensions==4.2.0 diff --git a/dependencies/pytest-min/requirements.txt b/dependencies/pytest-min/requirements.txt new file mode 100644 index 00000000..4fc6ef2f --- /dev/null +++ b/dependencies/pytest-min/requirements.txt @@ -0,0 +1,4 @@ +# Always adjust install_requires in setup.cfg and requirements.txt +# when changing minimum version dependencies +pytest == 6.1.0 +typing-extensions >= 3.7.2; python_version < "3.8" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..81540a53 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = [ + "setuptools>=51.0", + "wheel>=0.36", + "setuptools_scm[toml]>=6.2" +] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "pytest_asyncio/_version.py" diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py index 61c5f43b..1bc2811d 100644 --- a/pytest_asyncio/__init__.py +++ b/pytest_asyncio/__init__.py @@ -1,2 +1,5 @@ """The main point for importing pytest-asyncio items.""" -__version__ = "0.14.0" +from ._version import version as __version__ # noqa +from .plugin import fixture + +__all__ = ("fixture",) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2fdc5f4e..3b7b2304 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -1,240 +1,541 @@ """pytest-asyncio implementation.""" import asyncio import contextlib +import enum import functools import inspect import socket +import sys +import warnings +from typing import ( + Any, + AsyncIterator, + Awaitable, + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Set, + TypeVar, + Union, + cast, + overload, +) import pytest -try: - from _pytest.python import transfer_markers -except ImportError: # Pytest 4.1.0 removes the transfer_marker api (#104) - def transfer_markers(*args, **kwargs): # noqa - """Noop when over pytest 4.1.0""" - pass +from pytest import Function, Session, Item + +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + +_R = TypeVar("_R") + +_ScopeName = Literal["session", "package", "module", "class", "function"] +_T = TypeVar("_T") + +SimpleFixtureFunction = TypeVar( + "SimpleFixtureFunction", bound=Callable[..., Awaitable[_R]] +) +FactoryFixtureFunction = TypeVar( + "FactoryFixtureFunction", bound=Callable[..., AsyncIterator[_R]] +) +FixtureFunction = Union[SimpleFixtureFunction, FactoryFixtureFunction] +FixtureFunctionMarker = Callable[[FixtureFunction], FixtureFunction] + +Config = Any # pytest < 7.0 +PytestPluginManager = Any # pytest < 7.0 +FixtureDef = Any # pytest < 7.0 +Parser = Any # pytest < 7.0 +SubRequest = Any # pytest < 7.0 + + +class Mode(str, enum.Enum): + AUTO = "auto" + STRICT = "strict" + + +ASYNCIO_MODE_HELP = """\ +'auto' - for automatically handling all async functions by the plugin +'strict' - for autoprocessing disabling (useful if different async frameworks \ +should be tested together, e.g. \ +both pytest-asyncio and pytest-trio are used in the same project) +""" + + +def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None: + group = parser.getgroup("asyncio") + group.addoption( + "--asyncio-mode", + dest="asyncio_mode", + default=None, + metavar="MODE", + help=ASYNCIO_MODE_HELP, + ) + parser.addini( + "asyncio_mode", + help="default value for --asyncio-mode", + default="strict", + ) + + +@overload +def fixture( + fixture_function: FixtureFunction, + *, + scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., + params: Optional[Iterable[object]] = ..., + autouse: bool = ..., + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[Any], Optional[object]], + ] + ] = ..., + name: Optional[str] = ..., +) -> FixtureFunction: + ... + + +@overload +def fixture( + fixture_function: None = ..., + *, + scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., + params: Optional[Iterable[object]] = ..., + autouse: bool = ..., + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[Any], Optional[object]], + ] + ] = ..., + name: Optional[str] = None, +) -> FixtureFunctionMarker: + ... + + +def fixture( + fixture_function: Optional[FixtureFunction] = None, **kwargs: Any +) -> Union[FixtureFunction, FixtureFunctionMarker]: + if fixture_function is not None: + _make_asyncio_fixture_function(fixture_function) + return pytest.fixture(fixture_function, **kwargs) + + else: + + @functools.wraps(fixture) + def inner(fixture_function: FixtureFunction) -> FixtureFunction: + return fixture(fixture_function, **kwargs) + + return inner + + +def _is_asyncio_fixture_function(obj: Any) -> bool: + obj = getattr(obj, "__func__", obj) # instance method maybe? + return getattr(obj, "_force_asyncio_fixture", False) + + +def _make_asyncio_fixture_function(obj: Any) -> None: + if hasattr(obj, "__func__"): + # instance method, check the function object + obj = obj.__func__ + obj._force_asyncio_fixture = True + + +def _is_coroutine(obj: Any) -> bool: + """Check to see if an object is really an asyncio coroutine.""" + return asyncio.iscoroutinefunction(obj) -try: - from async_generator import isasyncgenfunction -except ImportError: - from inspect import isasyncgenfunction +def _is_coroutine_or_asyncgen(obj: Any) -> bool: + return _is_coroutine(obj) or inspect.isasyncgenfunction(obj) -def _is_coroutine(obj): - """Check to see if an object is really an asyncio coroutine.""" - return asyncio.iscoroutinefunction(obj) or inspect.isgeneratorfunction(obj) + +def _get_asyncio_mode(config: Config) -> Mode: + val = config.getoption("asyncio_mode") + if val is None: + val = config.getini("asyncio_mode") + return Mode(val) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: """Inject documentation.""" - config.addinivalue_line("markers", - "asyncio: " - "mark the test as a coroutine, it will be " - "run using an asyncio event loop") + config.addinivalue_line( + "markers", + "asyncio: " + "mark the test as a coroutine, it will be " + "run using an asyncio event loop", + ) + + if getattr(pytest, "version_tuple", (0, 0, 0)) < (7,): + warnings.warn( + "You're using an outdated version of pytest. Newer releases of " + "pytest-asyncio will not be compatible with this pytest version. " + "Please update pytest to version 7 or later.", + DeprecationWarning, + ) @pytest.mark.tryfirst -def pytest_pycollect_makeitem(collector, name, obj): - """A pytest hook to collect asyncio coroutines.""" - if collector.funcnamefilter(name) and _is_coroutine(obj): - item = pytest.Function.from_parent(collector, name=name) - - # Due to how pytest test collection works, module-level pytestmarks - # are applied after the collection step. Since this is the collection - # step, we look ourselves. - transfer_markers(obj, item.cls, item.module) - item = pytest.Function.from_parent(collector, name=name) # To reload keywords. +def pytest_report_header(config: Config) -> List[str]: + """Add asyncio config to pytest header.""" + mode = _get_asyncio_mode(config) + return [f"asyncio: mode={mode}"] + + +def _preprocess_async_fixtures( + config: Config, + processed_fixturedefs: Set[FixtureDef], +) -> None: + asyncio_mode = _get_asyncio_mode(config) + fixturemanager = config.pluginmanager.get_plugin("funcmanage") + for fixtures in fixturemanager._arg2fixturedefs.values(): + for fixturedef in fixtures: + func = fixturedef.func + if fixturedef in processed_fixturedefs or not _is_coroutine_or_asyncgen( + func + ): + continue + if not _is_asyncio_fixture_function(func) and asyncio_mode == Mode.STRICT: + # Ignore async fixtures without explicit asyncio mark in strict mode + # This applies to pytest_trio fixtures, for example + continue + _make_asyncio_fixture_function(func) + _inject_fixture_argnames(fixturedef) + _synchronize_async_fixture(fixturedef) + assert _is_asyncio_fixture_function(fixturedef.func) + processed_fixturedefs.add(fixturedef) + + +def _inject_fixture_argnames(fixturedef: FixtureDef) -> None: + """ + Ensures that `request` and `event_loop` are arguments of the specified fixture. + """ + to_add = [] + for name in ("request", "event_loop"): + if name not in fixturedef.argnames: + to_add.append(name) + if to_add: + fixturedef.argnames += tuple(to_add) - if 'asyncio' in item.keywords: - return list(collector._genfunctions(name, obj)) +def _synchronize_async_fixture(fixturedef: FixtureDef) -> None: + """ + Wraps the fixture function of an async fixture in a synchronous function. + """ + func = fixturedef.func + if inspect.isasyncgenfunction(func): + fixturedef.func = _wrap_asyncgen(func) + elif inspect.iscoroutinefunction(func): + fixturedef.func = _wrap_async(func) + + +def _add_kwargs( + func: Callable[..., Any], + kwargs: Dict[str, Any], + event_loop: asyncio.AbstractEventLoop, + request: SubRequest, +) -> Dict[str, Any]: + sig = inspect.signature(func) + ret = kwargs.copy() + if "request" in sig.parameters: + ret["request"] = request + if "event_loop" in sig.parameters: + ret["event_loop"] = event_loop + return ret + + +def _wrap_asyncgen(func: Callable[..., AsyncIterator[_R]]) -> Callable[..., _R]: + @functools.wraps(func) + def _asyncgen_fixture_wrapper( + event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any + ) -> _R: + gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request)) + + async def setup() -> _R: + res = await gen_obj.__anext__() + return res + + def finalizer() -> None: + """Yield again, to finalize.""" + + async def async_finalizer() -> None: + try: + await gen_obj.__anext__() + except StopAsyncIteration: + pass + else: + msg = "Async generator fixture didn't stop." + msg += "Yield only once." + raise ValueError(msg) + + event_loop.run_until_complete(async_finalizer()) + + result = event_loop.run_until_complete(setup()) + request.addfinalizer(finalizer) + return result -class FixtureStripper: - """Include additional Fixture, and then strip them""" - REQUEST = "request" - EVENT_LOOP = "event_loop" + return _asyncgen_fixture_wrapper - def __init__(self, fixturedef): - self.fixturedef = fixturedef - self.to_strip = set() - def add(self, name): - """Add fixture name to fixturedef - and record in to_strip list (If not previously included)""" - if name in self.fixturedef.argnames: - return - self.fixturedef.argnames += (name, ) - self.to_strip.add(name) - - def get_and_strip_from(self, name, data_dict): - """Strip name from data, and return value""" - result = data_dict[name] - if name in self.to_strip: - del data_dict[name] - return result +def _wrap_async(func: Callable[..., Awaitable[_R]]) -> Callable[..., _R]: + @functools.wraps(func) + def _async_fixture_wrapper( + event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any + ) -> _R: + async def setup() -> _R: + res = await func(**_add_kwargs(func, kwargs, event_loop, request)) + return res -@pytest.hookimpl(trylast=True) -def pytest_fixture_post_finalizer(fixturedef, request): - """Called after fixture teardown""" - if fixturedef.argname == "event_loop": - # Set empty loop policy, so that subsequent get_event_loop() provides a new loop - asyncio.set_event_loop_policy(None) + return event_loop.run_until_complete(setup()) + return _async_fixture_wrapper -@pytest.hookimpl(hookwrapper=True) -def pytest_fixture_setup(fixturedef, request): - """Adjust the event loop policy when an event loop is produced.""" - if fixturedef.argname == "event_loop": - outcome = yield - loop = outcome.get_result() - policy = asyncio.get_event_loop_policy() - policy.set_event_loop(loop) - return +_HOLDER: Set[FixtureDef] = set() - if isasyncgenfunction(fixturedef.func): - # This is an async generator function. Wrap it accordingly. - generator = fixturedef.func - fixture_stripper = FixtureStripper(fixturedef) - fixture_stripper.add(FixtureStripper.EVENT_LOOP) - fixture_stripper.add(FixtureStripper.REQUEST) +@pytest.mark.tryfirst +def pytest_pycollect_makeitem( + collector: Union[pytest.Module, pytest.Class], name: str, obj: object +) -> Union[ + None, pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]] +]: + """A pytest hook to collect asyncio coroutines.""" + if not collector.funcnamefilter(name): + return None + _preprocess_async_fixtures(collector.config, _HOLDER) + return None - def wrapper(*args, **kwargs): - loop = fixture_stripper.get_and_strip_from(FixtureStripper.EVENT_LOOP, kwargs) - request = fixture_stripper.get_and_strip_from(FixtureStripper.REQUEST, kwargs) +def pytest_collection_modifyitems( + session: Session, config: Config, items: List[Item] +) -> None: + """ + Marks collected async test items as `asyncio` tests. - gen_obj = generator(*args, **kwargs) + The mark is only applied in `AUTO` mode. It is applied to: - async def setup(): - res = await gen_obj.__anext__() - return res + - coroutines + - staticmethods wrapping coroutines + - Hypothesis tests wrapping coroutines - def finalizer(): - """Yield again, to finalize.""" - async def async_finalizer(): - try: - await gen_obj.__anext__() - except StopAsyncIteration: - pass - else: - msg = "Async generator fixture didn't stop." - msg += "Yield only once." - raise ValueError(msg) - loop.run_until_complete(async_finalizer()) + """ + if _get_asyncio_mode(config) != Mode.AUTO: + return + function_items = (item for item in items if isinstance(item, Function)) + for function_item in function_items: + function = function_item.obj + if isinstance(function, staticmethod): + # staticmethods need to be unwrapped. + function = function.__func__ + if ( + _is_coroutine(function) + or _is_hypothesis_test(function) + and _hypothesis_test_wraps_coroutine(function) + ): + function_item.add_marker("asyncio") - request.addfinalizer(finalizer) - return loop.run_until_complete(setup()) - fixturedef.func = wrapper - elif inspect.iscoroutinefunction(fixturedef.func): - coro = fixturedef.func +def _hypothesis_test_wraps_coroutine(function: Any) -> bool: + return _is_coroutine(function.hypothesis.inner_test) - fixture_stripper = FixtureStripper(fixturedef) - fixture_stripper.add(FixtureStripper.EVENT_LOOP) - def wrapper(*args, **kwargs): - loop = fixture_stripper.get_and_strip_from(FixtureStripper.EVENT_LOOP, kwargs) +@pytest.hookimpl(trylast=True) +def pytest_fixture_post_finalizer(fixturedef: FixtureDef, request: SubRequest) -> None: + """Called after fixture teardown""" + if fixturedef.argname == "event_loop": + policy = asyncio.get_event_loop_policy() + try: + loop = policy.get_event_loop() + except RuntimeError: + loop = None + if loop is not None: + # Clean up existing loop to avoid ResourceWarnings + loop.close() + new_loop = policy.new_event_loop() # Replace existing event loop + # Ensure subsequent calls to get_event_loop() succeed + policy.set_event_loop(new_loop) - async def setup(): - res = await coro(*args, **kwargs) - return res - return loop.run_until_complete(setup()) +@pytest.hookimpl(hookwrapper=True) +def pytest_fixture_setup( + fixturedef: FixtureDef, request: SubRequest +) -> Optional[object]: + """Adjust the event loop policy when an event loop is produced.""" + if fixturedef.argname == "event_loop": + outcome = yield + loop = outcome.get_result() + policy = asyncio.get_event_loop_policy() + try: + old_loop = policy.get_event_loop() + if old_loop is not loop: + old_loop.close() + except RuntimeError: + # Swallow this, since it's probably bad event loop hygiene. + pass + policy.set_event_loop(loop) + return - fixturedef.func = wrapper yield @pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem): +def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]: """ - Run asyncio marked test functions in an event loop instead of a normal - function call. + Pytest hook called before a test case is run. + + Wraps marked tests in a synchronous function + where the wrapped test coroutine is executed in an event loop. """ - if 'asyncio' in pyfuncitem.keywords: - if getattr(pyfuncitem.obj, 'is_hypothesis_test', False): + marker = pyfuncitem.get_closest_marker("asyncio") + if marker is not None: + funcargs: Dict[str, object] = pyfuncitem.funcargs # type: ignore[name-defined] + loop = cast(asyncio.AbstractEventLoop, funcargs["event_loop"]) + if _is_hypothesis_test(pyfuncitem.obj): pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync( + pyfuncitem, pyfuncitem.obj.hypothesis.inner_test, - _loop=pyfuncitem.funcargs['event_loop'] + _loop=loop, ) else: pyfuncitem.obj = wrap_in_sync( + pyfuncitem, pyfuncitem.obj, - _loop=pyfuncitem.funcargs['event_loop'] + _loop=loop, ) yield -def wrap_in_sync(func, _loop): +def _is_hypothesis_test(function: Any) -> bool: + return getattr(function, "is_hypothesis_test", False) + + +def wrap_in_sync( + pyfuncitem: pytest.Function, + func: Callable[..., Awaitable[Any]], + _loop: asyncio.AbstractEventLoop, +): """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 + raw_func = getattr(func, "_raw_test_func", None) + if raw_func is not None: + func = raw_func + @functools.wraps(func) - def inner(**kwargs): - coro = func(**kwargs) - if coro is not None: - task = asyncio.ensure_future(coro, loop=_loop) - try: - _loop.run_until_complete(task) - except BaseException: - # run_until_complete doesn't get the result from exceptions - # that are not subclasses of `Exception`. Consume all - # exceptions to prevent asyncio's warning from logging. - if task.done() and not task.cancelled(): - task.exception() - raise + def inner(*args, **kwargs): + coro = func(*args, **kwargs) + if not inspect.isawaitable(coro): + pyfuncitem.warn( + pytest.PytestWarning( + f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' " + "but it is not an async function. " + "Please remove asyncio marker. " + "If the test is not marked explicitly, " + "check for global markers applied via 'pytestmark'." + ) + ) + return + task = asyncio.ensure_future(coro, loop=_loop) + try: + _loop.run_until_complete(task) + except BaseException: + # run_until_complete doesn't get the result from exceptions + # that are not subclasses of `Exception`. Consume all + # exceptions to prevent asyncio's warning from logging. + if task.done() and not task.cancelled(): + task.exception() + raise + + inner._raw_test_func = func # type: ignore[attr-defined] return inner -def pytest_runtest_setup(item): - if 'asyncio' in item.keywords: - # inject an event loop fixture for all async tests - if 'event_loop' in item.fixturenames: - item.fixturenames.remove('event_loop') - item.fixturenames.insert(0, 'event_loop') - if item.get_closest_marker("asyncio") is not None \ - and not getattr(item.obj, 'hypothesis', False) \ - and getattr(item.obj, 'is_hypothesis_test', False): - pytest.fail( - 'test function `%r` is using Hypothesis, but pytest-asyncio ' - 'only works with Hypothesis 3.64.0 or later.' % item - ) +def pytest_runtest_setup(item: pytest.Item) -> None: + marker = item.get_closest_marker("asyncio") + if marker is None: + return + fixturenames = item.fixturenames # type: ignore[attr-defined] + # inject an event loop fixture for all async tests + if "event_loop" in fixturenames: + fixturenames.remove("event_loop") + fixturenames.insert(0, "event_loop") + obj = getattr(item, "obj", None) + if not getattr(obj, "hypothesis", False) and getattr( + obj, "is_hypothesis_test", False + ): + pytest.fail( + "test function `%r` is using Hypothesis, but pytest-asyncio " + "only works with Hypothesis 3.64.0 or later." % item + ) @pytest.fixture -def event_loop(request): +def event_loop(request: "pytest.FixtureRequest") -> Iterator[asyncio.AbstractEventLoop]: """Create an instance of the default event loop for each test case.""" loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() -def _unused_tcp_port(): - """Find an unused localhost TCP port from 1024-65535 and return it.""" - with contextlib.closing(socket.socket()) as sock: - sock.bind(('127.0.0.1', 0)) +def _unused_port(socket_type: int) -> int: + """Find an unused localhost port from 1024-65535 and return it.""" + with contextlib.closing(socket.socket(type=socket_type)) as sock: + sock.bind(("127.0.0.1", 0)) return sock.getsockname()[1] @pytest.fixture -def unused_tcp_port(): - return _unused_tcp_port() +def unused_tcp_port() -> int: + return _unused_port(socket.SOCK_STREAM) @pytest.fixture -def unused_tcp_port_factory(): +def unused_udp_port() -> int: + return _unused_port(socket.SOCK_DGRAM) + + +@pytest.fixture(scope="session") +def unused_tcp_port_factory() -> Callable[[], int]: """A factory function, producing different unused TCP ports.""" produced = set() def factory(): """Return an unused port.""" - port = _unused_tcp_port() + port = _unused_port(socket.SOCK_STREAM) + + while port in produced: + port = _unused_port(socket.SOCK_STREAM) + + produced.add(port) + + return port + + return factory + + +@pytest.fixture(scope="session") +def unused_udp_port_factory() -> Callable[[], int]: + """A factory function, producing different unused UDP ports.""" + produced = set() + + def factory(): + """Return an unused port.""" + port = _unused_port(socket.SOCK_DGRAM) while port in produced: - port = _unused_tcp_port() + port = _unused_port(socket.SOCK_DGRAM) produced.add(port) return port + return factory diff --git a/pytest_asyncio/py.typed b/pytest_asyncio/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/setup.cfg b/setup.cfg index 4a0e3005..b1f1e82d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,60 @@ +[metadata] +name = pytest-asyncio +version = attr: pytest_asyncio.__version__ +url = https://github.com/pytest-dev/pytest-asyncio +project_urls = + GitHub = https://github.com/pytest-dev/pytest-asyncio +description = Pytest support for asyncio +long_description = file: README.rst +long_description_content_type = text/x-rst +author = Tin Tvrtković +author_email = tinchester@gmail.com +license = Apache 2.0 +license_file = LICENSE +classifiers = + Development Status :: 4 - Beta + + Intended Audience :: Developers + + License :: OSI Approved :: Apache Software License + + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + + Topic :: Software Development :: Testing + + Framework :: AsyncIO + Framework :: Pytest + Typing :: Typed + +[options] +python_requires = >=3.7 +packages = find: +include_package_data = True + +# Always adjust requirements.txt and pytest-min-requirements.txt when changing runtime dependencies +install_requires = + pytest >= 6.1.0 + typing-extensions >= 3.7.2; python_version < "3.8" + +[options.extras_require] +testing = + coverage >= 6.2 + hypothesis >= 5.7.1 + flaky >= 3.5.0 + mypy >= 0.931 + pytest-trio >= 0.7.0 + +[options.entry_points] +pytest11 = + asyncio = pytest_asyncio.plugin + [coverage:run] source = pytest_asyncio +branch = true [coverage:report] show_missing = true @@ -7,8 +62,9 @@ show_missing = true [tool:pytest] addopts = -rsx --tb=short testpaths = tests +asyncio_mode = auto +junit_family=xunit2 filterwarnings = error -[metadata] -# ensure LICENSE is included in wheel metadata -license_file = LICENSE +[flake8] +max-line-length = 88 diff --git a/setup.py b/setup.py deleted file mode 100644 index 61757113..00000000 --- a/setup.py +++ /dev/null @@ -1,54 +0,0 @@ -import re -from pathlib import Path - -from setuptools import setup, find_packages - - -def find_version(): - version_file = ( - Path(__file__) - .parent.joinpath("pytest_asyncio", "__init__.py") - .read_text() - ) - version_match = re.search( - r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M - ) - if version_match: - return version_match.group(1) - - raise RuntimeError("Unable to find version string.") - - -setup( - name="pytest-asyncio", - version=find_version(), - packages=find_packages(), - url="https://github.com/pytest-dev/pytest-asyncio", - license="Apache 2.0", - author="Tin Tvrtković", - author_email="tinchester@gmail.com", - description="Pytest support for asyncio.", - long_description=Path(__file__).parent.joinpath("README.rst").read_text(), - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Topic :: Software Development :: Testing", - "Framework :: Pytest", - ], - python_requires=">= 3.5", - install_requires=["pytest >= 5.4.0"], - extras_require={ - ':python_version == "3.5"': "async_generator >= 1.3", - "testing": [ - "coverage", - "async_generator >= 1.3", - "hypothesis >= 5.7.1", - ], - }, - entry_points={"pytest11": ["asyncio = pytest_asyncio.plugin"]}, -) diff --git a/tests/async_fixtures/test_async_fixtures_35.py b/tests/async_fixtures/test_async_fixtures.py similarity index 100% rename from tests/async_fixtures/test_async_fixtures_35.py rename to tests/async_fixtures/test_async_fixtures.py diff --git a/tests/async_fixtures/test_async_fixtures_scope.py b/tests/async_fixtures/test_async_fixtures_scope.py index 0d8682cc..b150f8a8 100644 --- a/tests/async_fixtures/test_async_fixtures_scope.py +++ b/tests/async_fixtures/test_async_fixtures_scope.py @@ -3,16 +3,17 @@ module-scoped too. """ import asyncio + import pytest -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def event_loop(): """A module-scoped event loop.""" return asyncio.new_event_loop() -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") async def async_fixture(): await asyncio.sleep(0.1) return 1 diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py index 44b5bbe4..2e72d5de 100644 --- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py +++ b/tests/async_fixtures/test_async_fixtures_with_finalizer.py @@ -1,5 +1,6 @@ import asyncio import functools + import pytest @@ -8,11 +9,13 @@ async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer): await asyncio.sleep(0.01) assert port_with_event_loop_finalizer + @pytest.mark.asyncio async def test_module_with_get_event_loop_finalizer(port_with_get_event_loop_finalizer): await asyncio.sleep(0.01) assert port_with_get_event_loop_finalizer + @pytest.fixture(scope="module") def event_loop(): """Change event_loop fixture to module level.""" @@ -29,6 +32,7 @@ async def port_afinalizer(): # await task using loop provided by event_loop fixture # RuntimeError is raised if task is created on a different loop await finalizer + event_loop.run_until_complete(port_afinalizer()) worker = asyncio.ensure_future(asyncio.sleep(0.2)) @@ -40,10 +44,15 @@ async def port_afinalizer(): async def port_with_get_event_loop_finalizer(request, event_loop): def port_finalizer(finalizer): async def port_afinalizer(): - # await task using loop provided by asyncio.get_event_loop() - # RuntimeError is raised if task is created on a different loop + # await task using current loop retrieved from the event loop policy + # RuntimeError is raised if task is created on a different loop. + # This can happen when pytest_fixture_setup + # does not set up the loop correctly, + # for example when policy.set_event_loop() is called with a wrong argument await finalizer - asyncio.get_event_loop().run_until_complete(port_afinalizer()) + + current_loop = asyncio.get_event_loop_policy().get_event_loop() + current_loop.run_until_complete(port_afinalizer()) worker = asyncio.ensure_future(asyncio.sleep(0.2)) request.addfinalizer(functools.partial(port_finalizer, worker)) diff --git a/tests/async_fixtures/test_async_gen_fixtures_36.py b/tests/async_fixtures/test_async_gen_fixtures.py similarity index 93% rename from tests/async_fixtures/test_async_gen_fixtures_36.py rename to tests/async_fixtures/test_async_gen_fixtures.py index 81b21949..0bea7458 100644 --- a/tests/async_fixtures/test_async_gen_fixtures_36.py +++ b/tests/async_fixtures/test_async_gen_fixtures.py @@ -1,4 +1,3 @@ -import asyncio import unittest.mock import pytest @@ -8,7 +7,7 @@ RETVAL = object() -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def mock(): return unittest.mock.Mock(return_value=RETVAL) diff --git a/tests/async_fixtures/test_async_gen_fixtures_35.py b/tests/async_fixtures/test_async_gen_fixtures_35.py deleted file mode 100644 index 3b552fab..00000000 --- a/tests/async_fixtures/test_async_gen_fixtures_35.py +++ /dev/null @@ -1,40 +0,0 @@ -import unittest.mock - -import pytest -from async_generator import yield_, async_generator - -START = object() -END = object() -RETVAL = object() - - -@pytest.fixture(scope='module') -def mock(): - return unittest.mock.Mock(return_value=RETVAL) - - -@pytest.fixture -@async_generator -async def async_gen_fixture(mock): - try: - await yield_(mock(START)) - except Exception as e: - mock(e) - else: - mock(END) - - -@pytest.mark.asyncio -async def test_async_gen_fixture(async_gen_fixture, mock): - assert mock.called - assert mock.call_args_list[-1] == unittest.mock.call(START) - assert async_gen_fixture is RETVAL - - -@pytest.mark.asyncio -async def test_async_gen_fixture_finalized(mock): - try: - assert mock.called - assert mock.call_args_list[-1] == unittest.mock.call(END) - finally: - mock.reset_mock() diff --git a/tests/async_fixtures/test_coroutine_fixtures.py b/tests/async_fixtures/test_coroutine_fixtures.py deleted file mode 100644 index 77f203c9..00000000 --- a/tests/async_fixtures/test_coroutine_fixtures.py +++ /dev/null @@ -1,27 +0,0 @@ -import asyncio -import unittest.mock - -import pytest - -START = object() -END = object() -RETVAL = object() - -pytestmark = pytest.mark.skip(reason='@asyncio.coroutine fixtures are not supported yet') - - -@pytest.fixture -def mock(): - return unittest.mock.Mock(return_value=RETVAL) - - -@pytest.fixture -async def coroutine_fixture(mock): - await asyncio.sleep(0.1, result=mock(START)) - - -@pytest.mark.asyncio -async def test_coroutine_fixture(coroutine_fixture, mock): - assert mock.call_count == 1 - assert mock.call_args_list[-1] == unittest.mock.call(START) - assert coroutine_fixture is RETVAL diff --git a/tests/async_fixtures/test_nested_36.py b/tests/async_fixtures/test_nested.py similarity index 75% rename from tests/async_fixtures/test_nested_36.py rename to tests/async_fixtures/test_nested.py index 86e45b6b..e81e7824 100644 --- a/tests/async_fixtures/test_nested_36.py +++ b/tests/async_fixtures/test_nested.py @@ -1,25 +1,26 @@ import asyncio + import pytest @pytest.fixture() async def async_inner_fixture(): await asyncio.sleep(0.01) - print('inner start') + print("inner start") yield True - print('inner stop') + print("inner stop") @pytest.fixture() async def async_fixture_outer(async_inner_fixture, event_loop): await asyncio.sleep(0.01) - print('outer start') + print("outer start") assert async_inner_fixture is True yield True - print('outer stop') + print("outer stop") @pytest.mark.asyncio async def test_async_fixture(async_fixture_outer): assert async_fixture_outer is True - print('test_async_fixture') + print("test_async_fixture") diff --git a/tests/async_fixtures/test_parametrized_loop.py b/tests/async_fixtures/test_parametrized_loop.py new file mode 100644 index 00000000..2fb8befa --- /dev/null +++ b/tests/async_fixtures/test_parametrized_loop.py @@ -0,0 +1,31 @@ +import asyncio + +import pytest + +TESTS_COUNT = 0 + + +def teardown_module(): + # parametrized 2 * 2 times: 2 for 'event_loop' and 2 for 'fix' + assert TESTS_COUNT == 4 + + +@pytest.fixture(scope="module", params=[1, 2]) +def event_loop(request): + request.param + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +@pytest.fixture(params=["a", "b"]) +async def fix(request): + await asyncio.sleep(0) + return request.param + + +@pytest.mark.asyncio +async def test_parametrized_loop(fix): + await asyncio.sleep(0) + global TESTS_COUNT + TESTS_COUNT += 1 diff --git a/tests/conftest.py b/tests/conftest.py index cc2ec163..4aa8c89a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,8 @@ import asyncio -import sys import pytest -collect_ignore = [] -if sys.version_info[:2] < (3, 6): - collect_ignore.append("async_fixtures/test_async_gen_fixtures_36.py") - collect_ignore.append("async_fixtures/test_nested_36.py") +pytest_plugins = "pytester" @pytest.fixture @@ -26,3 +22,11 @@ async def just_a_sleep(): event_loop.run_until_complete(just_a_sleep()) assert counter == 2 + + +@pytest.fixture(scope="session", name="factory_involving_factories") +def factory_involving_factories_fixture(unused_tcp_port_factory): + def factory(): + return unused_tcp_port_factory() + + return factory diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py new file mode 100644 index 00000000..e6da3427 --- /dev/null +++ b/tests/hypothesis/test_base.py @@ -0,0 +1,88 @@ +"""Tests for the Hypothesis integration, which wraps async functions in a +sync shim for Hypothesis. +""" +import asyncio +from textwrap import dedent + +import pytest +from hypothesis import given, strategies as st + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + +@given(st.integers()) +@pytest.mark.asyncio +async def test_mark_inner(n): + assert isinstance(n, int) + + +@pytest.mark.asyncio +@given(st.integers()) +async def test_mark_outer(n): + assert isinstance(n, int) + + +@pytest.mark.parametrize("y", [1, 2]) +@given(x=st.none()) +@pytest.mark.asyncio +async def test_mark_and_parametrize(x, y): + assert x is None + assert y in (1, 2) + + +@given(st.integers()) +@pytest.mark.asyncio +async def test_can_use_fixture_provided_event_loop(event_loop, n): + semaphore = asyncio.Semaphore(value=0) + event_loop.call_soon(semaphore.release) + await semaphore.acquire() + + +def test_async_auto_marked(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + from hypothesis import given + import hypothesis.strategies as st + + pytest_plugins = 'pytest_asyncio' + + @given(n=st.integers()) + async def test_hypothesis(n: int): + assert isinstance(n, int) + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) + + +def test_sync_not_auto_marked(testdir): + """Assert that synchronous Hypothesis functions are not marked with asyncio""" + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + from hypothesis import given + import hypothesis.strategies as st + + pytest_plugins = 'pytest_asyncio' + + @given(n=st.integers()) + def test_hypothesis(request, n: int): + markers = [marker.name for marker in request.node.own_markers] + assert "asyncio" not in markers + assert isinstance(n, int) + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) diff --git a/tests/hypothesis/test_inherited_test.py b/tests/hypothesis/test_inherited_test.py new file mode 100644 index 00000000..a7762264 --- /dev/null +++ b/tests/hypothesis/test_inherited_test.py @@ -0,0 +1,20 @@ +import hypothesis.strategies as st +import pytest +from hypothesis import given + + +class BaseClass: + @pytest.mark.asyncio + @given(value=st.integers()) + async def test_hypothesis(self, value: int) -> None: + pass + + +class TestOne(BaseClass): + """During the first execution the Hypothesis test + is wrapped in a synchronous function.""" + + +class TestTwo(BaseClass): + """Execute the test a second time to ensure that + the test receives a fresh event loop.""" diff --git a/tests/loop_fixture_scope/conftest.py b/tests/loop_fixture_scope/conftest.py new file mode 100644 index 00000000..223160c2 --- /dev/null +++ b/tests/loop_fixture_scope/conftest.py @@ -0,0 +1,17 @@ +import asyncio + +import pytest + + +class CustomSelectorLoop(asyncio.SelectorEventLoop): + """A subclass with no overrides, just to test for presence.""" + + +loop = CustomSelectorLoop() + + +@pytest.fixture(scope="module") +def event_loop(): + """Create an instance of the default event loop for each test case.""" + yield loop + loop.close() diff --git a/tests/loop_fixture_scope/test_loop_fixture_scope.py b/tests/loop_fixture_scope/test_loop_fixture_scope.py new file mode 100644 index 00000000..679ab48f --- /dev/null +++ b/tests/loop_fixture_scope/test_loop_fixture_scope.py @@ -0,0 +1,16 @@ +"""Unit tests for overriding the event loop with a larger scoped one.""" +import asyncio + +import pytest + + +@pytest.mark.asyncio +async def test_for_custom_loop(): + """This test should be executed using the custom loop.""" + await asyncio.sleep(0.01) + assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop" + + +@pytest.mark.asyncio +async def test_dependent_fixture(dependent_fixture): + await asyncio.sleep(0.1) diff --git a/tests/markers/test_class_marker_35.py b/tests/markers/test_class_marker.py similarity index 99% rename from tests/markers/test_class_marker_35.py rename to tests/markers/test_class_marker.py index df137e7c..d46c3af7 100644 --- a/tests/markers/test_class_marker_35.py +++ b/tests/markers/test_class_marker.py @@ -1,5 +1,6 @@ """Test if pytestmark works when defined on a class.""" import asyncio + import pytest @@ -14,6 +15,7 @@ async def inc(): nonlocal counter counter += 1 await asyncio.sleep(0) + await asyncio.ensure_future(inc()) assert counter == 2 diff --git a/tests/markers/test_module_marker_35.py b/tests/markers/test_module_marker.py similarity index 99% rename from tests/markers/test_module_marker_35.py rename to tests/markers/test_module_marker.py index c5ce0b04..2f69dbc9 100644 --- a/tests/markers/test_module_marker_35.py +++ b/tests/markers/test_module_marker.py @@ -24,10 +24,12 @@ async def inc(): async def test_is_asyncio(event_loop, sample_fixture): assert asyncio.get_event_loop() counter = 1 + async def inc(): nonlocal counter counter += 1 await asyncio.sleep(0) + await asyncio.ensure_future(inc()) assert counter == 2 diff --git a/tests/modes/test_auto_mode.py b/tests/modes/test_auto_mode.py new file mode 100644 index 00000000..fc4d2df0 --- /dev/null +++ b/tests/modes/test_auto_mode.py @@ -0,0 +1,139 @@ +from textwrap import dedent + + +def test_auto_mode_cmdline(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) + + +def test_auto_mode_cfg(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + testdir.makefile(".ini", pytest="[pytest]\nasyncio_mode = auto\n") + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + +def test_auto_mode_async_fixture(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.fixture + async def fixture_a(): + await asyncio.sleep(0) + return 1 + + async def test_a(fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) + + +def test_auto_mode_method_fixture(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + + class TestA: + + @pytest.fixture + async def fixture_a(self): + await asyncio.sleep(0) + return 1 + + async def test_a(self, fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) + + +def test_auto_mode_static_method(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + + pytest_plugins = 'pytest_asyncio' + + + class TestA: + + @staticmethod + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) + + +def test_auto_mode_static_method_fixture(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + + class TestA: + + @staticmethod + @pytest.fixture + async def fixture_a(): + await asyncio.sleep(0) + return 1 + + @staticmethod + async def test_a(fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = testdir.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 new file mode 100644 index 00000000..3b6487c7 --- /dev/null +++ b/tests/modes/test_strict_mode.py @@ -0,0 +1,68 @@ +from textwrap import dedent + + +def test_strict_mode_cmdline(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_strict_mode_cfg(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + async def test_a(): + await asyncio.sleep(0) + """ + ) + ) + testdir.makefile(".ini", pytest="[pytest]\nasyncio_mode = strict\n") + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + +def test_strict_mode_method_fixture(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import pytest + import pytest_asyncio + + pytest_plugins = 'pytest_asyncio' + + class TestA: + + @pytest_asyncio.fixture + async def fixture_a(self): + await asyncio.sleep(0) + return 1 + + @pytest.mark.asyncio + async def test_a(self, fixture_a): + await asyncio.sleep(0) + assert fixture_a == 1 + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) diff --git a/tests/multiloop/conftest.py b/tests/multiloop/conftest.py index 1b62a1d0..ebcb627a 100644 --- a/tests/multiloop/conftest.py +++ b/tests/multiloop/conftest.py @@ -5,7 +5,6 @@ class CustomSelectorLoop(asyncio.SelectorEventLoop): """A subclass with no overrides, just to test for presence.""" - pass @pytest.fixture diff --git a/tests/respect_event_loop_policy/conftest.py b/tests/respect_event_loop_policy/conftest.py new file mode 100644 index 00000000..2c5cef24 --- /dev/null +++ b/tests/respect_event_loop_policy/conftest.py @@ -0,0 +1,16 @@ +"""Defines and sets a custom event loop policy""" +import asyncio +from asyncio import DefaultEventLoopPolicy, SelectorEventLoop + + +class TestEventLoop(SelectorEventLoop): + pass + + +class TestEventLoopPolicy(DefaultEventLoopPolicy): + def new_event_loop(self): + return TestEventLoop() + + +# This statement represents a code which sets a custom event loop policy +asyncio.set_event_loop_policy(TestEventLoopPolicy()) diff --git a/tests/respect_event_loop_policy/test_respects_event_loop_policy.py b/tests/respect_event_loop_policy/test_respects_event_loop_policy.py new file mode 100644 index 00000000..610b3388 --- /dev/null +++ b/tests/respect_event_loop_policy/test_respects_event_loop_policy.py @@ -0,0 +1,17 @@ +"""Tests that any externally provided event loop policy remains unaltered.""" +import asyncio + +import pytest + + +@pytest.mark.asyncio +async def test_uses_loop_provided_by_custom_policy(): + """Asserts that test cases use the event loop + provided by the custom event loop policy""" + assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop" + + +@pytest.mark.asyncio +async def test_custom_policy_is_not_overwritten(): + """Asserts that any custom event loop policy stays the same across test cases""" + assert type(asyncio.get_event_loop()).__name__ == "TestEventLoop" diff --git a/tests/test_asyncio_fixture.py b/tests/test_asyncio_fixture.py new file mode 100644 index 00000000..2577cba0 --- /dev/null +++ b/tests/test_asyncio_fixture.py @@ -0,0 +1,64 @@ +import asyncio +from textwrap import dedent + +import pytest + +import pytest_asyncio + + +@pytest_asyncio.fixture +async def fixture_bare(): + await asyncio.sleep(0) + return 1 + + +@pytest.mark.asyncio +async def test_bare_fixture(fixture_bare): + await asyncio.sleep(0) + assert fixture_bare == 1 + + +@pytest_asyncio.fixture(name="new_fixture_name") +async def fixture_with_name(request): + await asyncio.sleep(0) + return request.fixturename + + +@pytest.mark.asyncio +async def test_fixture_with_name(new_fixture_name): + await asyncio.sleep(0) + assert new_fixture_name == "new_fixture_name" + + +@pytest_asyncio.fixture(params=[2, 4]) +async def fixture_with_params(request): + await asyncio.sleep(0) + return request.param + + +@pytest.mark.asyncio +async def test_fixture_with_params(fixture_with_params): + await asyncio.sleep(0) + assert fixture_with_params % 2 == 0 + + +@pytest.mark.parametrize("mode", ("auto", "strict")) +def test_sync_function_uses_async_fixture(testdir, mode): + testdir.makepyfile( + dedent( + """\ + import pytest_asyncio + + pytest_plugins = 'pytest_asyncio' + + @pytest_asyncio.fixture + async def always_true(): + return True + + def test_sync_function_uses_async_fixture(always_true): + assert always_true is True + """ + ) + ) + result = testdir.runpytest(f"--asyncio-mode={mode}") + result.assert_outcomes(passed=1) diff --git a/tests/test_dependent_fixtures.py b/tests/test_dependent_fixtures.py index db2252b2..dc70fe9c 100644 --- a/tests/test_dependent_fixtures.py +++ b/tests/test_dependent_fixtures.py @@ -1,4 +1,5 @@ import asyncio + import pytest @@ -6,3 +7,8 @@ async def test_dependent_fixture(dependent_fixture): """Test a dependent fixture.""" await asyncio.sleep(0.1) + + +@pytest.mark.asyncio +async def test_factory_involving_factories(factory_involving_factories): + factory_involving_factories() diff --git a/tests/test_event_loop_scope.py b/tests/test_event_loop_scope.py new file mode 100644 index 00000000..21fd6415 --- /dev/null +++ b/tests/test_event_loop_scope.py @@ -0,0 +1,37 @@ +"""Test the event loop fixture provides a separate loop for each test. + +These tests need to be run together. +""" +import asyncio + +import pytest + +loop: asyncio.AbstractEventLoop + + +def test_1(): + global loop + # The main thread should have a default event loop. + loop = asyncio.get_event_loop_policy().get_event_loop() + + +@pytest.mark.asyncio +async def test_2(): + global loop + running_loop = asyncio.get_event_loop_policy().get_event_loop() + # Make sure this test case received a different loop + assert running_loop is not loop + loop = running_loop # Store the loop reference for later + + +def test_3(): + global loop + current_loop = asyncio.get_event_loop_policy().get_event_loop() + # Now the event loop from test_2 should have been cleaned up + assert loop is not current_loop + + +def test_4(event_loop): + # If a test sets the loop to None -- pytest_fixture_post_finalizer() + # still should work + asyncio.get_event_loop_policy().set_event_loop(None) diff --git a/tests/test_event_loop_scope_35.py b/tests/test_event_loop_scope_35.py deleted file mode 100644 index ed07d98d..00000000 --- a/tests/test_event_loop_scope_35.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Test the event loop fixture is properly disposed of. - -These tests need to be run together. -""" -import asyncio -import pytest - - -def test_1(): - loop = asyncio.get_event_loop() - assert not loop.is_closed() - - -@pytest.mark.asyncio -async def test_2(): - pass - - -def test_3(): - loop = asyncio.get_event_loop() - assert not loop.is_closed() diff --git a/tests/test_flaky_integration.py b/tests/test_flaky_integration.py new file mode 100644 index 00000000..54c9d2ea --- /dev/null +++ b/tests/test_flaky_integration.py @@ -0,0 +1,43 @@ +"""Tests for the Flaky integration, which retries failed tests. +""" +from textwrap import dedent + + +def test_auto_mode_cmdline(testdir): + testdir.makepyfile( + dedent( + """\ + import asyncio + import flaky + import pytest + + _threshold = -1 + + @flaky.flaky(3, 2) + @pytest.mark.asyncio + async def test_asyncio_flaky_thing_that_fails_then_succeeds(): + global _threshold + await asyncio.sleep(0.1) + _threshold += 1 + assert _threshold != 1 + """ + ) + ) + # runpytest_subprocess() is required to don't pollute the output + # with flaky restart information + result = testdir.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + [ + "===Flaky Test Report===", + "test_asyncio_flaky_thing_that_fails_then_succeeds passed 1 " + "out of the required 2 times. Running test again until it passes 2 times.", + "test_asyncio_flaky_thing_that_fails_then_succeeds failed " + "(1 runs remaining out of 3).", + " ", + " assert 1 != 1", + "test_asyncio_flaky_thing_that_fails_then_succeeds passed 2 " + "out of the required 2 times. Success!", + "===End Flaky Test Report===", + ] + ) diff --git a/tests/test_hypothesis_integration.py b/tests/test_hypothesis_integration.py deleted file mode 100644 index 9c97e06c..00000000 --- a/tests/test_hypothesis_integration.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Tests for the Hypothesis integration, which wraps async functions in a -sync shim for Hypothesis. -""" -import asyncio - -import pytest - -from hypothesis import given, strategies as st - - -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - -@given(st.integers()) -@pytest.mark.asyncio -async def test_mark_inner(n): - assert isinstance(n, int) - - -@pytest.mark.asyncio -@given(st.integers()) -async def test_mark_outer(n): - assert isinstance(n, int) - - -@pytest.mark.parametrize("y", [1, 2]) -@given(x=st.none()) -@pytest.mark.asyncio -async def test_mark_and_parametrize(x, y): - assert x is None - assert y in (1, 2) - - -@given(st.integers()) -@pytest.mark.asyncio -async def test_can_use_fixture_provided_event_loop(event_loop, n): - semaphore = asyncio.Semaphore(value=0) - event_loop.call_soon(semaphore.release) - await semaphore.acquire() diff --git a/tests/test_pytest_min_version_warning.py b/tests/test_pytest_min_version_warning.py new file mode 100644 index 00000000..11de6800 --- /dev/null +++ b/tests/test_pytest_min_version_warning.py @@ -0,0 +1,26 @@ +from textwrap import dedent + +import pytest + + +@pytest.mark.skipif( + pytest.__version__ < "7.0.0", + reason="The warning shouldn't be present when run with recent pytest versions" +) +@pytest.mark.parametrize("mode", ("auto", "strict")) +def test_pytest_min_version_warning_is_not_triggered_for_pytest_7(testdir, mode): + testdir.makepyfile( + dedent( + """\ + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + async def test_triggers_pytest_warning(): + pass + """ + ) + ) + result = testdir.runpytest(f"--asyncio-mode={mode}") + result.assert_outcomes(passed=1, warnings=0) diff --git a/tests/test_simple.py b/tests/test_simple.py index c8dccaf8..dc68d61e 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.""" import asyncio +from textwrap import dedent + import pytest import pytest_asyncio.plugin @@ -7,14 +9,14 @@ async def async_coro(): await asyncio.sleep(0) - return 'ok' + return "ok" def test_event_loop_fixture(event_loop): """Test the injection of the event_loop fixture.""" assert event_loop ret = event_loop.run_until_complete(async_coro()) - assert ret == 'ok' + assert ret == "ok" @pytest.mark.asyncio @@ -23,16 +25,16 @@ async def test_asyncio_marker(): await asyncio.sleep(0) -@pytest.mark.xfail(reason='need a failure', strict=True) +@pytest.mark.xfail(reason="need a failure", strict=True) @pytest.mark.asyncio -def test_asyncio_marker_fail(): - assert False +async def test_asyncio_marker_fail(): + raise AssertionError @pytest.mark.asyncio -def test_asyncio_marker_with_default_param(a_param=None): +async def test_asyncio_marker_with_default_param(a_param=None): """Test the asyncio pytest marker.""" - yield # sleep(0) + await asyncio.sleep(0) @pytest.mark.asyncio @@ -42,17 +44,42 @@ async def test_unused_port_fixture(unused_tcp_port, event_loop): async def closer(_, writer): writer.close() - server1 = await asyncio.start_server(closer, host='localhost', - port=unused_tcp_port) + server1 = await asyncio.start_server(closer, host="localhost", port=unused_tcp_port) with pytest.raises(IOError): - await asyncio.start_server(closer, host='localhost', - port=unused_tcp_port) + await asyncio.start_server(closer, host="localhost", port=unused_tcp_port) server1.close() await server1.wait_closed() +@pytest.mark.asyncio +async def test_unused_udp_port_fixture(unused_udp_port, event_loop): + """Test the unused TCP port fixture.""" + + class Closer: + def connection_made(self, transport): + pass + + def connection_lost(self, *arg, **kwd): + pass + + transport1, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", unused_udp_port), + reuse_port=False, + ) + + with pytest.raises(IOError): + await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", unused_udp_port), + reuse_port=False, + ) + + transport1.abort() + + @pytest.mark.asyncio async def test_unused_port_factory_fixture(unused_tcp_port_factory, event_loop): """Test the unused TCP port factory fixture.""" @@ -60,20 +87,19 @@ async def test_unused_port_factory_fixture(unused_tcp_port_factory, event_loop): async def closer(_, writer): writer.close() - port1, port2, port3 = (unused_tcp_port_factory(), unused_tcp_port_factory(), - unused_tcp_port_factory()) + port1, port2, port3 = ( + unused_tcp_port_factory(), + unused_tcp_port_factory(), + unused_tcp_port_factory(), + ) - server1 = await asyncio.start_server(closer, host='localhost', - port=port1) - server2 = await asyncio.start_server(closer, host='localhost', - port=port2) - server3 = await asyncio.start_server(closer, host='localhost', - port=port3) + server1 = await asyncio.start_server(closer, host="localhost", port=port1) + server2 = await asyncio.start_server(closer, host="localhost", port=port2) + server3 = await asyncio.start_server(closer, host="localhost", port=port3) for port in port1, port2, port3: with pytest.raises(IOError): - await asyncio.start_server(closer, host='localhost', - port=port) + await asyncio.start_server(closer, host="localhost", port=port) server1.close() await server1.wait_closed() @@ -83,11 +109,57 @@ async def closer(_, writer): await server3.wait_closed() +@pytest.mark.asyncio +async def test_unused_udp_port_factory_fixture(unused_udp_port_factory, event_loop): + """Test the unused UDP port factory fixture.""" + + class Closer: + def connection_made(self, transport): + pass + + def connection_lost(self, *arg, **kwd): + pass + + port1, port2, port3 = ( + unused_udp_port_factory(), + unused_udp_port_factory(), + unused_udp_port_factory(), + ) + + transport1, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port1), + reuse_port=False, + ) + transport2, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port2), + reuse_port=False, + ) + transport3, _ = await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port3), + reuse_port=False, + ) + + for port in port1, port2, port3: + with pytest.raises(IOError): + await event_loop.create_datagram_endpoint( + Closer, + local_addr=("127.0.0.1", port), + reuse_port=False, + ) + + transport1.abort() + transport2.abort() + transport3.abort() + + def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch): """Test correct avoidance of duplicate ports.""" counter = 0 - def mock_unused_tcp_port(): + def mock_unused_tcp_port(_ignored): """Force some duplicate ports.""" nonlocal counter counter += 1 @@ -96,36 +168,47 @@ def mock_unused_tcp_port(): else: return 10000 + counter - monkeypatch.setattr(pytest_asyncio.plugin, '_unused_tcp_port', - mock_unused_tcp_port) + monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_tcp_port) assert unused_tcp_port_factory() == 10000 assert unused_tcp_port_factory() > 10000 -class Test: - """Test that asyncio marked functions work in test methods.""" +def test_unused_udp_port_factory_duplicate(unused_udp_port_factory, monkeypatch): + """Test correct avoidance of duplicate UDP ports.""" + counter = 0 - @pytest.mark.asyncio - async def test_asyncio_marker_method(self, event_loop): - """Test the asyncio pytest marker in a Test class.""" - ret = await async_coro() - assert ret == 'ok' + def mock_unused_udp_port(_ignored): + """Force some duplicate ports.""" + nonlocal counter + counter += 1 + if counter < 5: + return 10000 + else: + return 10000 + counter + monkeypatch.setattr(pytest_asyncio.plugin, "_unused_port", mock_unused_udp_port) -class TestUnexistingLoop: - @pytest.fixture - def remove_loop(self): - old_loop = asyncio.get_event_loop() - asyncio.set_event_loop(None) - yield - asyncio.set_event_loop(old_loop) + assert unused_udp_port_factory() == 10000 + assert unused_udp_port_factory() > 10000 + + +class TestMarkerInClassBasedTests: + """Test that asyncio marked functions work for methods of test classes.""" @pytest.mark.asyncio - async def test_asyncio_marker_without_loop(self, remove_loop): - """Test the asyncio pytest marker in a Test class.""" + async def test_asyncio_marker_with_explicit_loop_fixture(self, event_loop): + """Test the "asyncio" marker works on a method in + a class-based test with explicit loop fixture.""" ret = await async_coro() - assert ret == 'ok' + assert ret == "ok" + + @pytest.mark.asyncio + async def test_asyncio_marker_with_implicit_loop_fixture(self): + """Test the "asyncio" marker works on a method in + a class-based test with implicit loop fixture.""" + ret = await async_coro() + assert ret == "ok" class TestEventLoopStartedBeforeFixtures: @@ -150,7 +233,43 @@ async def test_event_loop_before_fixture(self, event_loop, loop): assert await loop.run_in_executor(None, self.foo) == 1 - @pytest.mark.asyncio async def test_no_warning_on_skip(): pytest.skip("Test a skip error inside asyncio") + + +def test_async_close_loop(event_loop): + event_loop.close() + return "ok" + + +def test_warn_asyncio_marker_for_regular_func(testdir): + testdir.makepyfile( + dedent( + """\ + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + def test_a(): + pass + """ + ) + ) + testdir.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] + ) diff --git a/tests/test_simple_35.py b/tests/test_simple_35.py deleted file mode 100644 index 4141fb0b..00000000 --- a/tests/test_simple_35.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Quick'n'dirty unit tests using async and await syntax.""" -import asyncio - -import pytest - - -@pytest.mark.asyncio -async def async_coro(loop): - await asyncio.sleep(0) - return 'ok' - - -@pytest.mark.asyncio -async def test_asyncio_marker(): - """Test the asyncio pytest marker.""" - - -@pytest.mark.asyncio -async def test_asyncio_marker_with_default_param(a_param=None): - """Test the asyncio pytest marker.""" - - -@pytest.mark.asyncio -async def test_unused_port_fixture(unused_tcp_port, event_loop): - """Test the unused TCP port fixture.""" - async def closer(_, writer): - writer.close() - - server1 = await asyncio.start_server(closer, host='localhost', - port=unused_tcp_port) - - server1.close() - await server1.wait_closed() - - -def test_unused_port_factory_fixture(unused_tcp_port_factory, event_loop): - """Test the unused TCP port factory fixture.""" - - async def closer(_, writer): - writer.close() - - port1, port2, port3 = (unused_tcp_port_factory(), unused_tcp_port_factory(), - unused_tcp_port_factory()) - - async def run_test(): - server1 = await asyncio.start_server(closer, host='localhost', - port=port1) - server2 = await asyncio.start_server(closer, host='localhost', - port=port2) - server3 = await asyncio.start_server(closer, host='localhost', - port=port3) - - for port in port1, port2, port3: - with pytest.raises(IOError): - await asyncio.start_server(closer, host='localhost', - port=port) - - server1.close() - await server1.wait_closed() - server2.close() - await server2.wait_closed() - server3.close() - await server3.wait_closed() - - event_loop.run_until_complete(run_test()) - - event_loop.stop() - event_loop.close() - - -class Test: - """Test that asyncio marked functions work in test methods.""" - - @pytest.mark.asyncio - async def test_asyncio_marker_method(self, event_loop): - """Test the asyncio pytest marker in a Test class.""" - ret = await async_coro(event_loop) - assert ret == 'ok' - - -def test_async_close_loop(event_loop): - event_loop.close() - return 'ok' diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 069c6c22..79c5109d 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -1,12 +1,10 @@ """Tests for using subprocesses in tests.""" -import sys -import asyncio import asyncio.subprocess +import sys import pytest - -if sys.platform == 'win32': +if sys.platform == "win32": # The default asyncio event loop implementation on Windows does not # support subprocesses. Subprocesses are available for Windows if a # ProactorEventLoop is used. @@ -17,17 +15,22 @@ def event_loop(): loop.close() -@pytest.mark.asyncio(forbid_global_loop=False) -async def test_subprocess(event_loop): - """Starting a subprocess should be possible.""" - proc = await asyncio.subprocess.create_subprocess_exec( - sys.executable, '--version', stdout=asyncio.subprocess.PIPE) - await proc.communicate() +@pytest.mark.skipif( + sys.version_info < (3, 8), + reason=""" + When run with Python 3.7 asyncio.subprocess.create_subprocess_exec seems to be + affected by an issue that prevents correct cleanup. Tests using pytest-trio + will report that signal handling is already performed by another library and + fail. [1] This is possibly a bug in CPython 3.7, so we ignore this test for + that Python version. - -@pytest.mark.asyncio(forbid_global_loop=True) -async def test_subprocess_forbid(event_loop): + [1] https://github.com/python-trio/pytest-trio/issues/126 + """, +) +@pytest.mark.asyncio +async def test_subprocess(event_loop): """Starting a subprocess should be possible.""" proc = await asyncio.subprocess.create_subprocess_exec( - sys.executable, '--version', stdout=asyncio.subprocess.PIPE) + sys.executable, "--version", stdout=asyncio.subprocess.PIPE + ) await proc.communicate() diff --git a/tests/trio/test_fixtures.py b/tests/trio/test_fixtures.py new file mode 100644 index 00000000..42b28437 --- /dev/null +++ b/tests/trio/test_fixtures.py @@ -0,0 +1,25 @@ +from textwrap import dedent + + +def test_strict_mode_ignores_trio_fixtures(testdir): + testdir.makepyfile( + dedent( + """\ + import pytest + import pytest_asyncio + import pytest_trio + + pytest_plugins = ["pytest_asyncio", "pytest_trio"] + + @pytest_trio.trio_fixture + async def any_fixture(): + return True + + @pytest.mark.trio + async def test_anything(any_fixture): + pass + """ + ) + ) + result = testdir.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) diff --git a/tools/get-version.py b/tools/get-version.py new file mode 100644 index 00000000..e988a32c --- /dev/null +++ b/tools/get-version.py @@ -0,0 +1,17 @@ +import json +import sys +from importlib import metadata + +from packaging.version import parse as parse_version + + +def main(): + version_string = metadata.version("pytest-asyncio") + version = parse_version(version_string) + print(f"::set-output name=version::{version}") + prerelease = json.dumps(version.is_prerelease) + print(f"::set-output name=prerelease::{prerelease}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tox.ini b/tox.ini index eed6fb67..1d8994ae 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,37 @@ [tox] minversion = 3.14.0 -envlist = py35, py36, py37, py38 -skip_missing_interpreters = true +envlist = py37, py38, py39, py310, py311, lint, version-info, pytest-min +isolated_build = true +passenv = + CI [testenv] extras = testing -commands = coverage run -m pytest {posargs} +deps = + --requirement dependencies/default/requirements.txt + --constraint dependencies/default/constraints.txt +commands = make test +allowlist_externals = + make + +[testenv:pytest-min] +extras = testing +deps = + --requirement dependencies/pytest-min/requirements.txt + --constraint dependencies/pytest-min/constraints.txt +commands = make test +allowlist_externals = + make + +[testenv:lint] +basepython = python3.10 +extras = testing +deps = + pre-commit == 2.16.0 +commands = + make lint +allowlist_externals = + make [testenv:coverage-report] deps = coverage @@ -13,3 +39,18 @@ skip_install = true commands = coverage combine coverage report + +[testenv:version-info] +deps = + packaging == 21.3 +commands = + python ./tools/get-version.py + +[gh-actions] +python = + 3.7: py37, pytest-min + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11-dev: py311 + pypy3: pypy3