^\s*assert False(,|$)
^\s*if TYPE_CHECKING:
+ ^\s*@overload( |$)
+++ /dev/null
-<!--
-Thanks for submitting an issue!
-
-Here's a quick checklist for what to provide:
--->
-
-- [ ] a detailed description of the bug or suggestion
-- [ ] output of `pip list` from the virtual environment you are using
-- [ ] pytest and operating system versions
-- [ ] minimal example if possible
--- /dev/null
+---
+name: 🐛 Bug Report
+about: Report errors and problems
+
+---
+
+<!--
+Thanks for submitting an issue!
+
+Quick check-list while reporting bugs:
+-->
+
+- [ ] a detailed description of the bug or problem you are having
+- [ ] output of `pip list` from the virtual environment you are using
+- [ ] pytest and operating system versions
+- [ ] minimal example if possible
--- /dev/null
+---
+name: 🚀 Feature Request
+about: Ideas for new features and improvements
+
+---
--- /dev/null
+blank_issues_enabled: false
+contact_links:
+ - name: ❓ Support Question
+ url: https://github.com/pytest-dev/pytest/discussions
+ about: Use GitHub's new Discussions feature for questions
"docs",
"doctesting",
+ "plugins",
]
include:
tox_env: "py38-xdist"
use_coverage: true
+ - name: "plugins"
+ python: "3.7"
+ os: ubuntu-latest
+ tox_env: "plugins"
+
- name: "docs"
python: "3.7"
os: ubuntu-latest
with:
python-version: ${{ matrix.python }}
- name: Set up Python ${{ matrix.python }} (deadsnakes)
- uses: deadsnakes/action@v1.0.0
+ uses: deadsnakes/action@v2.0.0
if: matrix.python == '3.9-dev'
with:
python-version: ${{ matrix.python }}
hooks:
- id: flake8
language_version: python3
- additional_dependencies: [flake8-typing-imports==1.9.0]
+ additional_dependencies:
+ - flake8-typing-imports==1.9.0
+ - flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.3.0
hooks:
_code\.|
builtin\.|
code\.|
- io\.(BytesIO|saferepr|TerminalWriter)|
+ io\.|
path\.local\.sysfind|
process\.|
- std\.
+ std\.|
+ error\.|
+ xml\.
)
types: [python]
--- /dev/null
+version: 2
+
+python:
+ version: 3.7
+ install:
+ - requirements: doc/en/requirements.txt
+ - method: pip
+ path: .
+
+formats:
+ - epub
+ - pdf
Jurko Gospodnetić
Justyna Janczyszyn
Kale Kundert
+Kamran Ahmad
Karl O. Pinc
Katarzyna Jachim
Katarzyna Król
The built documentation should be available in ``doc/en/_build/html``,
where 'en' refers to the documentation language.
+Pytest has an API reference which in large part is
+`generated automatically <https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`_
+from the docstrings of the documented items. Pytest uses the
+`Sphinx docstring format <https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html>`_.
+For example:
+
+.. code-block:: python
+
+ def my_function(arg: ArgType) -> Foo:
+ """Do important stuff.
+
+ More detailed info here, in separate paragraphs from the subject line.
+ Use proper sentences -- start sentences with capital letters and end
+ with periods.
+
+ Can include annotated documentation:
+
+ :param short_arg: An argument which determines stuff.
+ :param long_arg:
+ A long explanation which spans multiple lines, overflows
+ like this.
+ :returns: The result.
+ :raises ValueError:
+ Detailed information when this can happen.
+
+ .. versionadded:: 6.0
+
+ Including types into the annotations above is not necessary when
+ type-hinting is being used (as in this example).
+ """
+
+
.. _submitplugin:
Submitting Plugins to pytest-dev
- `pytest-dev on GitHub <https://github.com/pytest-dev>`_
-- `pytest-dev on Bitbucket <https://bitbucket.org/pytest-dev>`_
-
All pytest-dev Contributors team members have write access to all contained
repositories. Pytest core and plugins are generally developed
using `pull requests`_ to respective repositories.
mail pointing to your existing pytest plugin repository which must have
the following:
-- PyPI presence with a ``setup.py`` that contains a license, ``pytest-``
+- PyPI presence with packaging metadata that contains a ``pytest-``
prefixed name, version number, authors, short and long description.
-- a ``tox.ini`` for running tests using `tox <https://tox.readthedocs.io>`_.
+- a `tox configuration <https://tox.readthedocs.io/en/latest/config.html#configuration-discovery>`_
+ for running tests using `tox <https://tox.readthedocs.io>`_.
-- a ``README.txt`` describing how to use the plugin and on which
+- a ``README`` describing how to use the plugin and on which
platforms it runs.
-- a ``LICENSE.txt`` file or equivalent containing the licensing
- information, with matching info in ``setup.py``.
+- a ``LICENSE`` file containing the licensing information, with
+ matching info in its packaging metadata.
- an issue tracker for bug reports and enhancement requests.
* Delete the PR body, it usually contains a duplicate commit message.
+Who does the backporting
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+As mentioned above, bugs should first be fixed on ``master`` (except in rare occasions
+that a bug only happens in a previous release). So who should do the backport procedure described
+above?
+
+1. If the bug was fixed by a core developer, it is the main responsibility of that core developer
+ to do the backport.
+2. However, often the merge is done by another maintainer, in which case it is nice of them to
+ do the backport procedure if they have the time.
+3. For bugs submitted by non-maintainers, it is expected that a core developer will to do
+ the backport, normally the one that merged the PR on ``master``.
+4. If a non-maintainers notices a bug which is fixed on ``master`` but has not been backported
+ (due to maintainers forgetting to apply the *needs backport* label, or just plain missing it),
+ they are also welcome to open a PR with the backport. The procedure is simple and really
+ helps with the maintenance of the project.
+
+All the above are not rules, but merely some guidelines/suggestions on what we should expect
+about backports.
+
Handling stale issues/PRs
-------------------------
.. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master
:target: https://travis-ci.org/pytest-dev/pytest
-.. image:: https://dev.azure.com/pytest-dev/pytest/_apis/build/status/pytest-CI?branchName=master
- :target: https://dev.azure.com/pytest-dev/pytest
+.. image:: https://github.com/pytest-dev/pytest/workflows/main/badge.svg
+ :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Amain
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
Features
--------
-- Detailed info on failing `assert statements <https://docs.pytest.org/en/stable/assert.html>`_ (no need to remember ``self.assert*`` names);
+- Detailed info on failing `assert statements <https://docs.pytest.org/en/stable/assert.html>`_ (no need to remember ``self.assert*`` names)
- `Auto-discovery
<https://docs.pytest.org/en/stable/goodpractices.html#python-test-discovery>`_
- of test modules and functions;
+ of test modules and functions
- `Modular fixtures <https://docs.pytest.org/en/stable/fixture.html>`_ for
- managing small or parametrized long-lived test resources;
+ managing small or parametrized long-lived test resources
- Can run `unittest <https://docs.pytest.org/en/stable/unittest.html>`_ (or trial),
- `nose <https://docs.pytest.org/en/stable/nose.html>`_ test suites out of the box;
+ `nose <https://docs.pytest.org/en/stable/nose.html>`_ test suites out of the box
-- Python 3.5+ and PyPy3;
+- Python 3.5+ and PyPy3
-- Rich plugin architecture, with over 850+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;
+- Rich plugin architecture, with over 850+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community
Documentation
is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence
taking a lot of time to make a new one.
+The git commands assume the following remotes are setup:
+
+* ``origin``: your own fork of the repository.
+* ``upstream``: the ``pytest-dev/pytest`` official repository.
+
Preparing: Automatic Method
~~~~~~~~~~~~~~~~~~~~~~~~~~~
We have developed an automated workflow for releases, that uses GitHub workflows and is triggered
-by opening an issue or issuing a comment one.
+by opening an issue.
+
+Bug-fix releases
+^^^^^^^^^^^^^^^^
+
+A bug-fix release is always done from a maintenance branch, so for example to release bug-fix
+``5.1.2``, open a new issue and add this comment to the body::
+
+ @pytestbot please prepare release from 5.1.x
+
+Where ``5.1.x`` is the maintenance branch for the ``5.1`` series.
+
+The automated workflow will publish a PR for a branch ``release-5.1.2``
+and notify it as a comment in the issue.
+
+Minor releases
+^^^^^^^^^^^^^^
+
+1. Create a new maintenance branch from ``master``::
+
+ git fetch --all
+ git branch 5.2.x upstream/master
+ git push upstream 5.2.x
-The comment must be in the form::
+2. Open a new issue and add this comment to the body::
- @pytestbot please prepare release from BRANCH
+ @pytestbot please prepare release from 5.2.x
-Where ``BRANCH`` is ``master`` or one of the maintenance branches.
+The automated workflow will publish a PR for a branch ``release-5.2.0`` and
+notify it as a comment in the issue.
-For major releases the comment must be in the form::
+Major and release candidates
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- @pytestbot please prepare major release from master
+1. Create a new maintenance branch from ``master``::
-After that, the workflow should publish a PR and notify that it has done so as a comment
-in the original issue.
+ git fetch --all
+ git branch 6.0.x upstream/master
+ git push upstream 6.0.x
+
+2. For a **major release**, open a new issue and add this comment in the body::
+
+ @pytestbot please prepare major release from 6.0.x
+
+ For a **release candidate**, the comment must be (TODO: `#7551 <https://github.com/pytest-dev/pytest/issues/7551>`__)::
+
+ @pytestbot please prepare release candidate from 6.0.x
+
+The automated workflow will publish a PR for a branch ``release-6.0.0`` and
+notify it as a comment in the issue.
+
+At this point on, this follows the same workflow as other maintenance branches: bug-fixes are merged
+into ``master`` and ported back to the maintenance branch, even for release candidates.
+
+**A note about release candidates**
+
+During release candidates we can merge small improvements into
+the maintenance branch before releasing the final major version, however we must take care
+to avoid introducing big changes at this stage.
Preparing: Manual Method
~~~~~~~~~~~~~~~~~~~~~~~~
-.. important::
-
- pytest releases must be prepared on **Linux** because the docs and examples expect
- to be executed on that platform.
+**Important**: pytest releases must be prepared on **Linux** because the docs and examples expect
+to be executed on that platform.
To release a version ``MAJOR.MINOR.PATCH``, follow these steps:
-#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from the
- latest ``master`` and push it to the ``pytest-dev/pytest`` repo.
+#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from
+ ``upstream/master`` and push it to ``upstream``.
#. Create a branch ``release-MAJOR.MINOR.PATCH`` from the ``MAJOR.MINOR.x`` branch.
Both automatic and manual processes described above follow the same steps from this point onward.
#. After all tests pass and the PR has been approved, tag the release commit
- in the ``MAJOR.MINOR.x`` branch and push it. This will publish to PyPI::
+ in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
- git tag MAJOR.MINOR.PATCH
+ git fetch --all
+ git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
git push git@github.com:pytest-dev/pytest.git MAJOR.MINOR.PATCH
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
git fetch --all --prune
git checkout origin/master -b cherry-pick-release
- git cherry-pick --no-commit -m1 origin/MAJOR.MINOR.x
- git checkout origin/master -- changelog
- git commit # no arguments
+ git cherry-pick -x -m1 upstream/MAJOR.MINOR.x
+
+#. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step.
#. Send an email announcement with the contents from::
What does it mean to "adopt pytest"?
-----------------------------------------
-There can be many different definitions of "success". Pytest can run many `nose and unittest`_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
+There can be many different definitions of "success". Pytest can run many nose_ and unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
Progressive success might look like:
It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies.
-.. _`nose and unittest`: faq.html#how-does-pytest-relate-to-nose-and-unittest
+.. _nose: nose.html
+.. _unittest: unittest.html
.. _assert: assert.html
.. _pycmd: https://bitbucket.org/hpk42/pycmd/overview
.. _`setUp/tearDown methods`: xunit_setup.html
:maxdepth: 2
+ release-6.1.0
release-6.0.2
release-6.0.1
release-6.0.0
- pluginmanager.register(...) now raises ValueError if the
plugin has been already registered or the name is taken
-- fix issue159: improve http://pytest.org/en/stable/faq.html
+- fix issue159: improve https://docs.pytest.org/en/6.0.1/faq.html
especially with respect to the "magic" history, also mention
pytest-django, trial and unittest integration.
pytest-2.5.1: fixes and new home page styling
===========================================================================
-pytest is a mature Python testing tool with more than a 1000 tests
+pytest is a mature Python testing tool with more than 1000 tests
against itself, passing on many different interpreters and platforms.
The 2.5.1 release maintains the "zero-reported-bugs" promise by fixing
pytest-2.5.2: fixes
===========================================================================
-pytest is a mature Python testing tool with more than a 1000 tests
+pytest is a mature Python testing tool with more than 1000 tests
against itself, passing on many different interpreters and platforms.
The 2.5.2 release fixes a few bugs with two maybe-bugs remaining and
pytest-2.6.0: shorter tracebacks, new warning system, test runner compat
===========================================================================
-pytest is a mature Python testing tool with more than a 1000 tests
+pytest is a mature Python testing tool with more than 1000 tests
against itself, passing on many different interpreters and platforms.
The 2.6.0 release should be drop-in backward compatible to 2.5.2 and
pytest-2.6.1: fixes and new xfail feature
===========================================================================
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
The 2.6.1 release is drop-in compatible to 2.5.2 and actually fixes some
regressions introduced with 2.6.0. It also brings a little feature
pytest-2.6.2: few fixes and cx_freeze support
===========================================================================
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is drop-in compatible to 2.5.2 and 2.6.X. It also
brings support for including pytest with cx_freeze or similar
pytest-2.6.3: fixes and little improvements
===========================================================================
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is drop-in compatible to 2.5.2 and 2.6.X.
See below for the changes and see docs at:
pytest-2.7.0: fixes, features, speed improvements
===========================================================================
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.6.X.
pytest-2.7.1: bug fixes
=======================
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.7.0.
pytest-2.7.2: bug fixes
=======================
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.7.1.
pytest-2.8.2: bug fixes
=======================
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.8.1.
pytest-2.8.3: bug fixes
=======================
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.8.2.
pytest-2.8.4
============
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.8.2.
pytest-2.8.5
============
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.8.4.
pytest-2.8.6
============
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.8.5.
This is a hotfix release to solve a regression
in the builtin monkeypatch plugin that got introduced in 2.8.6.
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
This release is supposed to be drop-in compatible to 2.8.5.
pytest-2.9.0
============
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
See below for the changes and see docs at:
pytest-2.9.1
============
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
See below for the changes and see docs at:
pytest-2.9.2
============
-pytest is a mature Python testing tool with more than a 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
against itself, passing on many different interpreters and platforms.
See below for the changes and see docs at:
The pytest team is proud to announce the 3.0.0 release!
-pytest is a mature Python testing tool with more than a 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
against itself, passing on many different interpreters and platforms.
This release contains a lot of bugs fixes and improvements, and much of
The pytest team is proud to announce the 3.1.0 release!
-pytest is a mature Python testing tool with more than a 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
against itself, passing on many different interpreters and platforms.
This release contains a bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.10.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.2.0 release!
-pytest is a mature Python testing tool with more than a 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.3.0 release!
-pytest is a mature Python testing tool with more than a 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.4.0 release!
-pytest is a mature Python testing tool with more than a 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.5.0 release!
-pytest is a mature Python testing tool with more than a 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.6.0 release!
-pytest is a mature Python testing tool with more than a 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.7.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.8.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 3.9.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 4.0.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 4.1.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 4.2.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 4.3.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 4.4.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 4.5.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 4.6.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 5.0.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 5.1.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 5.2.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 5.3.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
The pytest team is proud to announce the 5.4.0 release!
-pytest is a mature Python testing tool with more than a 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bug fixes and improvements, so users are encouraged
--- /dev/null
+pytest-6.1.0
+=======================================
+
+The pytest team is proud to announce the 6.1.0 release!
+
+This release contains new features, improvements, bug fixes, and breaking changes, so users
+are encouraged to take a look at the CHANGELOG carefully:
+
+ https://docs.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+ https://docs.pytest.org/en/stable/
+
+As usual, you can upgrade from PyPI via:
+
+ pip install -U pytest
+
+Thanks to all of the contributors to this release:
+
+* Anthony Sottile
+* Bruno Oliveira
+* C. Titus Brown
+* Drew Devereux
+* Faris A Chugthai
+* Florian Bruhin
+* Hugo van Kemenade
+* Hynek Schlawack
+* Joseph Lucas
+* Kamran Ahmad
+* Mattreex
+* Maximilian Cosmo Sitter
+* Ran Benita
+* Rüdiger Busche
+* Sam Estep
+* Sorin Sbarnea
+* Thomas Grainger
+* Vipul Kumar
+* Yutaro Ikeda
+* hp310780
+
+
+Happy testing,
+The pytest Development Team
cache.get(key, default)
cache.set(key, value)
- Keys must be a ``/`` separated value, where the first part is usually the
+ Keys must be ``/`` separated strings, where the first part is usually the
name of your plugin or application to avoid clashes with other cache users.
Values can be any object handled by the json stdlib module.
``out`` and ``err`` will be ``byte`` objects.
doctest_namespace [session scope]
- Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.
+ Fixture that returns a :py:class:`dict` that will be injected into the
+ namespace of doctests.
pytestconfig [session scope]
Session-scoped fixture that returns the :class:`_pytest.config.Config` object.
automatically XML-encoded.
record_testsuite_property [session scope]
- Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
- writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.
+ Record a new ``<property>`` tag as child of the root ``<testsuite>``.
+
+ This is suitable to writing global information regarding the entire test
+ suite, and is compatible with ``xunit2`` JUnit family.
This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
+ .. warning::
+
+ Currently this fixture **does not work** with the
+ `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See issue
+ `#7767 <https://github.com/pytest-dev/pytest/issues/7767>`__ for details.
+
caplog
Access and control log capturing.
* caplog.clear() -> clear captured records and formatted log output string
monkeypatch
- The returned ``monkeypatch`` fixture provides these
- helper methods to modify objects, dictionaries or os.environ::
+ A convenient fixture for monkey-patching.
+
+ The fixture provides these methods to modify objects, dictionaries or
+ os.environ::
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
- All modifications will be undone after the requesting
- test function or fixture has finished. The ``raising``
- parameter determines if a KeyError or AttributeError
- will be raised if the set/deletion operation has no target.
+ All modifications will be undone after the requesting test function or
+ fixture has finished. The ``raising`` parameter determines if a KeyError
+ or AttributeError will be raised if the set/deletion operation has no target.
recwarn
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
tmpdir_factory [session scope]
Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
-
tmp_path_factory [session scope]
Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
-
tmpdir
- Return a temporary directory path object
- which is unique to each test function invocation,
- created as a sub directory of the base temporary
- directory. The returned object is a `py.path.local`_
- path object.
+ Return a temporary directory path object which is unique to each test
+ function invocation, created as a sub directory of the base temporary
+ directory.
+
+ The returned object is a `py.path.local`_ path object.
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
tmp_path
- Return a temporary directory path object
- which is unique to each test function invocation,
- created as a sub directory of the base temporary
- directory. The returned object is a :class:`pathlib.Path`
- object.
+ Return a temporary directory path object which is unique to each test
+ function invocation, created as a sub directory of the base temporary
+ directory.
+
+ The returned object is a :class:`pathlib.Path` object.
.. note::
- in python < 3.6 this is a pathlib2.Path
+ In python < 3.6 this is a pathlib2.Path.
no tests ran in 0.12s
FAILED test_caching.py::test_function - assert 42 == 23
1 failed in 0.12s
-See the :fixture:`config.cache fixture <config.cache>` for more details.
+See the :fixture:`config.cache fixture <cache>` for more details.
Inspecting Cache content
.. towncrier release notes start
+pytest 6.1.0 (2020-09-26)
+=========================
+
+Breaking Changes
+----------------
+
+- `#5585 <https://github.com/pytest-dev/pytest/issues/5585>`_: As per our policy, the following features which have been deprecated in the 5.X series are now
+ removed:
+
+ * The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute.
+
+ * ``@pytest.fixture`` no longer supports positional arguments, pass all arguments by keyword instead.
+
+ * Direct construction of ``Node`` subclasses now raise an error, use ``from_parent`` instead.
+
+ * The default value for ``junit_family`` has changed to ``xunit2``. If you require the old format, add ``junit_family=xunit1`` to your configuration file.
+
+ * The ``TerminalReporter`` no longer has a ``writer`` attribute. Plugin authors may use the public functions of the ``TerminalReporter`` instead of accessing the ``TerminalWriter`` object directly.
+
+ * The ``--result-log`` option has been removed. Users are recommended to use the `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin instead.
+
+
+ For more information consult
+ `Deprecations and Removals <https://docs.pytest.org/en/stable/deprecations.html>`__ in the docs.
+
+
+
+Deprecations
+------------
+
+- `#6981 <https://github.com/pytest-dev/pytest/issues/6981>`_: The ``pytest.collect`` module is deprecated: all its names can be imported from ``pytest`` directly.
+
+
+- `#7097 <https://github.com/pytest-dev/pytest/issues/7097>`_: The ``pytest._fillfuncargs`` function is deprecated. This function was kept
+ for backward compatibility with an older plugin.
+
+ It's functionality is not meant to be used directly, but if you must replace
+ it, use `function._request._fillfixtures()` instead, though note this is not
+ a public API and may break in the future.
+
+
+- `#7210 <https://github.com/pytest-dev/pytest/issues/7210>`_: The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'``
+ instead.
+
+ The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue
+ if you use this and want a replacement.
+
+
+- `#7255 <https://github.com/pytest-dev/pytest/issues/7255>`_: The :func:`pytest_warning_captured <_pytest.hookspec.pytest_warning_captured>` hook is deprecated in favor
+ of :func:`pytest_warning_recorded <_pytest.hookspec.pytest_warning_recorded>`, and will be removed in a future version.
+
+
+- `#7648 <https://github.com/pytest-dev/pytest/issues/7648>`_: The ``gethookproxy()`` and ``isinitpath()`` methods of ``FSCollector`` and ``Package`` are deprecated;
+ use ``self.session.gethookproxy()`` and ``self.session.isinitpath()`` instead.
+ This should work on all pytest versions.
+
+
+
+Features
+--------
+
+- `#7667 <https://github.com/pytest-dev/pytest/issues/7667>`_: New ``--durations-min`` command-line flag controls the minimal duration for inclusion in the slowest list of tests shown by ``--durations``. Previously this was hard-coded to ``0.005s``.
+
+
+
+Improvements
+------------
+
+- `#6681 <https://github.com/pytest-dev/pytest/issues/6681>`_: Internal pytest warnings issued during the early stages of initialization are now properly handled and can filtered through :confval:`filterwarnings` or ``--pythonwarnings/-W``.
+
+ This also fixes a number of long standing issues: `#2891 <https://github.com/pytest-dev/pytest/issues/2891>`__, `#7620 <https://github.com/pytest-dev/pytest/issues/7620>`__, `#7426 <https://github.com/pytest-dev/pytest/issues/7426>`__.
+
+
+- `#7572 <https://github.com/pytest-dev/pytest/issues/7572>`_: When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace.
+
+
+- `#7685 <https://github.com/pytest-dev/pytest/issues/7685>`_: Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`.
+ These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes,
+ and should be preferred over them when possible.
+
+
+- `#7780 <https://github.com/pytest-dev/pytest/issues/7780>`_: Public classes which are not designed to be inherited from are now marked `@final <https://docs.python.org/3/library/typing.html#typing.final>`_.
+ Code which inherits from these classes will trigger a type-checking (e.g. mypy) error, but will still work in runtime.
+ Currently the ``final`` designation does not appear in the API Reference but hopefully will in the future.
+
+
+
+Bug Fixes
+---------
+
+- `#1953 <https://github.com/pytest-dev/pytest/issues/1953>`_: Fixed error when overwriting a parametrized fixture, while also reusing the super fixture value.
+
+ .. code-block:: python
+
+ # conftest.py
+ import pytest
+
+
+ @pytest.fixture(params=[1, 2])
+ def foo(request):
+ return request.param
+
+
+ # test_foo.py
+ import pytest
+
+
+ @pytest.fixture
+ def foo(foo):
+ return foo * 2
+
+
+- `#4984 <https://github.com/pytest-dev/pytest/issues/4984>`_: Fixed an internal error crash with ``IndexError: list index out of range`` when
+ collecting a module which starts with a decorated function, the decorator
+ raises, and assertion rewriting is enabled.
+
+
+- `#7591 <https://github.com/pytest-dev/pytest/issues/7591>`_: pylint shouldn't complain anymore about unimplemented abstract methods when inheriting from :ref:`File <non-python tests>`.
+
+
+- `#7628 <https://github.com/pytest-dev/pytest/issues/7628>`_: Fixed test collection when a full path without a drive letter was passed to pytest on Windows (for example ``\projects\tests\test.py`` instead of ``c:\projects\tests\pytest.py``).
+
+
+- `#7638 <https://github.com/pytest-dev/pytest/issues/7638>`_: Fix handling of command-line options that appear as paths but trigger an OS-level syntax error on Windows, such as the options used internally by ``pytest-xdist``.
+
+
+- `#7742 <https://github.com/pytest-dev/pytest/issues/7742>`_: Fixed INTERNALERROR when accessing locals / globals with faulty ``exec``.
+
+
+
+Improved Documentation
+----------------------
+
+- `#1477 <https://github.com/pytest-dev/pytest/issues/1477>`_: Removed faq.rst and its reference in contents.rst.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#7536 <https://github.com/pytest-dev/pytest/issues/7536>`_: The internal ``junitxml`` plugin has rewritten to use ``xml.etree.ElementTree``.
+ The order of attributes in XML elements might differ. Some unneeded escaping is
+ no longer performed.
+
+
+- `#7587 <https://github.com/pytest-dev/pytest/issues/7587>`_: The dependency on the ``more-itertools`` package has been removed.
+
+
+- `#7631 <https://github.com/pytest-dev/pytest/issues/7631>`_: The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple,
+ but should behave like one in all respects. This was done for technical reasons.
+
+
+- `#7671 <https://github.com/pytest-dev/pytest/issues/7671>`_: When collecting tests, pytest finds test classes and functions by examining the
+ attributes of python objects (modules, classes and instances). To speed up this
+ process, pytest now ignores builtin attributes (like ``__class__``,
+ ``__delattr__`` and ``__new__``) without consulting the :confval:`python_classes` and
+ :confval:`python_functions` configuration options and without passing them to plugins
+ using the :func:`pytest_pycollect_makeitem <_pytest.hookspec.pytest_pycollect_makeitem>` hook.
+
+
pytest 6.0.2 (2020-09-04)
=========================
- pluginmanager.register(...) now raises ValueError if the
plugin has been already registered or the name is taken
-- fix issue159: improve http://pytest.org/en/stable/faq.html
+- fix issue159: improve https://docs.pytest.org/en/6.0.1/faq.html
especially with respect to the "magic" history, also mention
pytest-django, trial and unittest integration.
customize
example/index
bash-completion
- faq
backwards-compatibility
deprecations
The internal :class:`Config <_pytest.config.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
will subsequently carry these attributes:
-- ``config.rootdir``: the determined root directory, guaranteed to exist.
+- :attr:`config.rootpath <_pytest.config.Config.rootpath>`: the determined root directory, guaranteed to exist.
-- ``config.inifile``: the determined ``configfile``, may be ``None`` (it is named ``inifile``
- for historical reasons).
+- :attr:`config.inipath <_pytest.config.Config.inipath>`: the determined ``configfile``, may be ``None``
+ (it is named ``inipath`` for historical reasons).
+
+.. versionadded:: 6.1
+ The ``config.rootpath`` and ``config.inipath`` properties. They are :class:`pathlib.Path`
+ versions of the older ``config.rootdir`` and ``config.inifile``, which have type
+ ``py.path.local``, and still exist for backward compatibility.
The ``rootdir`` is used as a reference directory for constructing test
addresses ("nodeids") and can be used also by plugins for storing
-------------------
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
-:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
-:ref:`standard warning filters <warnings>`.
+:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
The ``pytest_warning_captured`` hook
Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter
by a ``nodeid`` parameter.
+The ``pytest.collect`` module
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 6.0
+
+The ``pytest.collect`` module is no longer part of the public API, all its names
+should now be imported from ``pytest`` directly instead.
+
The ``pytest._fillfuncargs`` function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. deprecated:: 5.5
+.. deprecated:: 6.0
This function was kept for backward compatibility with an older plugin.
a public API and may break in the future.
+Removed Features
+----------------
+
+As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
+an appropriate period of deprecation has passed.
``--no-print-logs`` command-line option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Node Construction changed to ``Node.from_parent``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Result log (``--result-log``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. deprecated:: 5.4
+.. deprecated:: 4.0
+.. versionremoved:: 6.0
-The construction of nodes now should use the named constructor ``from_parent``.
-This limitation in api surface intends to enable better/simpler refactoring of the collection tree.
+The ``--result-log`` option produces a stream of test reports which can be
+analysed at runtime, but it uses a custom format which requires users to implement their own
+parser.
-This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)`
-one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`.
+The `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing
+one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback.
-Plugins that wish to support older versions of pytest and suppress the warning can use
-`hasattr` to check if `from_parent` exists in that version:
+The ``pytest-reportlog`` plugin might even be merged into the core
+at some point, depending on the plans for the plugins and number of users using it.
-.. code-block:: python
+``pytest_collect_directory`` hook
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- def pytest_pycollect_makeitem(collector, name, obj):
- if hasattr(MyItem, "from_parent"):
- item = MyItem.from_parent(collector, name="foo")
- item.obj = 42
- return item
- else:
- return MyItem(name="foo", parent=collector, obj=42)
+.. versionremoved:: 6.0
-Note that ``from_parent`` should only be called with keyword arguments for the parameters.
+The ``pytest_collect_directory`` has not worked properly for years (it was called
+but the results were ignored). Users may consider using :func:`pytest_collection_modifyitems <_pytest.hookspec.pytest_collection_modifyitems>` instead.
+TerminalReporter.writer
+~~~~~~~~~~~~~~~~~~~~~~~
+.. versionremoved:: 6.0
+
+The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This
+was inadvertently exposed as part of the public API of that plugin and ties it too much
+with ``py.io.TerminalWriter``.
+
+Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter``
+methods that provide the same functionality.
``junit_family`` default value change to "xunit2"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. deprecated:: 5.2
+.. versionchanged:: 6.0
The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, which
is an update of the old ``xunit1`` format and is supported by default in modern tools
* `Jenkins <https://www.jenkins.io/>`__ with the `JUnit <https://plugins.jenkins.io/junit>`__ plugin.
* `Azure Pipelines <https://azure.microsoft.com/en-us/services/devops/pipelines>`__.
+Node Construction changed to ``Node.from_parent``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-``funcargnames`` alias for ``fixturenames``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. deprecated:: 5.0
-
-The ``FixtureRequest``, ``Metafunc``, and ``Function`` classes track the names of
-their associated fixtures, with the aptly-named ``fixturenames`` attribute.
+.. versionchanged:: 6.0
-Prior to pytest 2.3, this attribute was named ``funcargnames``, and we have kept
-that as an alias since. It is finally due for removal, as it is often confusing
-in places where we or plugin authors must distinguish between fixture names and
-names supplied by non-fixture things such as ``pytest.mark.parametrize``.
+The construction of nodes now should use the named constructor ``from_parent``.
+This limitation in api surface intends to enable better/simpler refactoring of the collection tree.
+This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)`
+one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`.
-Result log (``--result-log``)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Plugins that wish to support older versions of pytest and suppress the warning can use
+`hasattr` to check if `from_parent` exists in that version:
-.. deprecated:: 4.0
+.. code-block:: python
-The ``--result-log`` option produces a stream of test reports which can be
-analysed at runtime, but it uses a custom format which requires users to implement their own
-parser.
+ def pytest_pycollect_makeitem(collector, name, obj):
+ if hasattr(MyItem, "from_parent"):
+ item = MyItem.from_parent(collector, name="foo")
+ item.obj = 42
+ return item
+ else:
+ return MyItem(name="foo", parent=collector, obj=42)
-The `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing
-one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback.
+Note that ``from_parent`` should only be called with keyword arguments for the parameters.
-The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportlog`` proves satisfactory
-to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core
-at some point, depending on the plans for the plugins and number of users using it.
-TerminalReporter.writer
-~~~~~~~~~~~~~~~~~~~~~~~
+``pytest.fixture`` arguments are keyword only
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. deprecated:: 5.4
+.. versionremoved:: 6.0
-The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This
-was inadvertently exposed as part of the public API of that plugin and ties it too much
-with ``py.io.TerminalWriter``.
+Passing arguments to pytest.fixture() as positional arguments has been removed - pass them by keyword instead.
-Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter``
-methods that provide the same functionality.
+``funcargnames`` alias for ``fixturenames``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. versionremoved:: 6.0
-Removed Features
-----------------
+The ``FixtureRequest``, ``Metafunc``, and ``Function`` classes track the names of
+their associated fixtures, with the aptly-named ``fixturenames`` attribute.
-As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
-an appropriate period of deprecation has passed.
+Prior to pytest 2.3, this attribute was named ``funcargnames``, and we have kept
+that as an alias since. It is finally due for removal, as it is often confusing
+in places where we or plugin authors must distinguish between fixture names and
+names supplied by non-fixture things such as ``pytest.mark.parametrize``.
``pytest.config`` global
.. versionremoved:: 4.0
-:meth:`_pytest.python.Metafunc.addcall` was a precursor to the current parametrized mechanism. Users should use
+``_pytest.python.Metafunc.addcall`` was a precursor to the current parametrized mechanism. Users should use
:meth:`_pytest.python.Metafunc.parametrize` instead.
Example:
.. versionremoved:: 4.0
-As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See
+As part of a large :ref:`marker-revamp`, ``_pytest.nodes.Node.get_marker`` is removed. See
:ref:`the documentation <update marker code>` on tips on how to update your code.
You can use ``and``, ``or``, ``not`` and parentheses.
+In addition to the test's name, ``-k`` also matches the names of the test's parents (usually, the name of the file and class it's in),
+attributes set on the test function, markers applied to it or its parents and any :attr:`extra keywords <_pytest.nodes.Node.extra_keyword_matches>`
+explicitly added to it or its parents.
+
+
Registering markers
-------------------------------------
This is equivalent to directly applying the decorator to the
two test functions.
-To apply marks at the module level, use the :globalvar:`pytestmark` global variable:
+To apply marks at the module level, use the :globalvar:`pytestmark` global variable::
import pytest
pytestmark = pytest.mark.webtest
.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py
.. _`PyYAML`: https://pypi.org/project/PyYAML/
-Here is an example ``conftest.py`` (extracted from Ali Afshnars special purpose `pytest-yamlwsgi`_ plugin). This ``conftest.py`` will collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests:
+Here is an example ``conftest.py`` (extracted from Ali Afshar's special purpose `pytest-yamlwsgi`_ plugin). This ``conftest.py`` will collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests:
.. include:: nonpython/conftest.py
:literal:
class YamlFile(pytest.File):
def collect(self):
- import yaml # we need a yaml parser, e.g. PyYAML
+ # We need a yaml parser, e.g. PyYAML.
+ import yaml
raw = yaml.safe_load(self.fspath.open())
for name, spec in sorted(raw.items()):
def runtest(self):
for name, value in sorted(self.spec.items()):
- # some custom test execution (dumb example follows)
+ # Some custom test execution (dumb example follows).
if name != value:
raise YamlException(self, name, value)
def repr_failure(self, excinfo):
- """ called when self.runtest() raises an exception. """
+ """Called when self.runtest() raises an exception."""
if isinstance(excinfo.value, YamlException):
return "\n".join(
[
class YamlException(Exception):
- """ custom exception for error reporting. """
+ """Custom exception for error reporting."""
collect_ignore = ["setup.py"]
if sys.version_info[0] > 2:
collect_ignore_glob = ["*_py2.py"]
+
+Since Pytest 2.6, users can prevent pytest from discovering classes that start
+with ``Test`` by setting a boolean ``__test__`` attribute to ``False``.
+
+.. code-block:: python
+
+ # Will not be discovered as a test
+ class TestClass:
+ __test__ = False
+++ /dev/null
-Some Issues and Questions
-==================================
-
-.. note::
-
- This FAQ is here only mostly for historic reasons. Checkout
- `pytest Q&A at Stackoverflow <http://stackoverflow.com/search?q=pytest>`_
- for many questions and answers related to pytest and/or use
- :ref:`contact channels` to get help.
-
-On naming, nosetests, licensing and magic
-------------------------------------------------
-
-How does pytest relate to nose and unittest?
-+++++++++++++++++++++++++++++++++++++++++++++++++
-
-``pytest`` and nose_ share basic philosophy when it comes
-to running and writing Python tests. In fact, you can run many tests
-written for nose with ``pytest``. nose_ was originally created
-as a clone of ``pytest`` when ``pytest`` was in the ``0.8`` release
-cycle. Note that starting with pytest-2.0 support for running unittest
-test suites is majorly improved.
-
-how does pytest relate to twisted's trial?
-++++++++++++++++++++++++++++++++++++++++++++++
-
-Since some time ``pytest`` has builtin support for supporting tests
-written using trial. It does not itself start a reactor, however,
-and does not handle Deferreds returned from a test in pytest style.
-If you are using trial's unittest.TestCase chances are that you can
-just run your tests even if you return Deferreds. In addition,
-there also is a dedicated `pytest-twisted
-<https://pypi.org/project/pytest-twisted/>`_ plugin which allows you to
-return deferreds from pytest-style tests, allowing the use of
-:ref:`fixtures <fixtures>` and other features.
-
-how does pytest work with Django?
-++++++++++++++++++++++++++++++++++++++++++++++
-
-In 2012, some work is going into the `pytest-django plugin <https://pypi.org/project/pytest-django/>`_. It substitutes the usage of Django's
-``manage.py test`` and allows the use of all pytest features_ most of which
-are not available from Django directly.
-
-.. _features: features.html
-
-
-What's this "magic" with pytest? (historic notes)
-++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-Around 2007 (version ``0.8``) some people thought that ``pytest``
-was using too much "magic". It had been part of the `pylib`_ which
-contains a lot of unrelated python library code. Around 2010 there
-was a major cleanup refactoring, which removed unused or deprecated code
-and resulted in the new ``pytest`` PyPI package which strictly contains
-only test-related code. This release also brought a complete pluginification
-such that the core is around 300 lines of code and everything else is
-implemented in plugins. Thus ``pytest`` today is a small, universally runnable
-and customizable testing framework for Python. Note, however, that
-``pytest`` uses metaprogramming techniques and reading its source is
-thus likely not something for Python beginners.
-
-A second "magic" issue was the assert statement debugging feature.
-Nowadays, ``pytest`` explicitly rewrites assert statements in test modules
-in order to provide more useful :ref:`assert feedback <assertfeedback>`.
-This completely avoids previous issues of confusing assertion-reporting.
-It also means, that you can use Python's ``-O`` optimization without losing
-assertions in test modules.
-
-You can also turn off all assertion interaction using the
-``--assert=plain`` option.
-
-.. _`py namespaces`: index.html
-.. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py
-
-
-Why can I use both ``pytest`` and ``py.test`` commands?
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-pytest used to be part of the py package, which provided several developer
-utilities, all starting with ``py.<TAB>``, thus providing nice TAB-completion.
-If you install ``pip install pycmd`` you get these tools from a separate
-package. Once ``pytest`` became a separate package, the ``py.test`` name was
-retained due to avoid a naming conflict with another tool. This conflict was
-eventually resolved, and the ``pytest`` command was therefore introduced. In
-future versions of pytest, we may deprecate and later remove the ``py.test``
-command to avoid perpetuating the confusion.
-
-pytest fixtures, parametrized tests
--------------------------------------------------------
-
-.. _funcargs: funcargs.html
-
-Is using pytest fixtures versus xUnit setup a style question?
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-For simple applications and for people experienced with nose_ or
-unittest-style test setup using `xUnit style setup`_ probably
-feels natural. For larger test suites, parametrized testing
-or setup of complex test resources using fixtures_ may feel more natural.
-Moreover, fixtures are ideal for writing advanced test support
-code (like e.g. the monkeypatch_, the tmpdir_ or capture_ fixtures)
-because the support code can register setup/teardown functions
-in a managed class/module/function scope.
-
-.. _monkeypatch: monkeypatch.html
-.. _tmpdir: tmpdir.html
-.. _capture: capture.html
-.. _fixtures: fixture.html
-
-.. _`why pytest_pyfuncarg__ methods?`:
-
-.. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration
-
-Can I yield multiple values from a fixture function?
-++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-There are two conceptual reasons why yielding from a factory function
-is not possible:
-
-* If multiple factories yielded values there would
- be no natural place to determine the combination
- policy - in real-world examples some combinations
- often should not run.
-
-* Calling factories for obtaining test function arguments
- is part of setting up and running a test. At that
- point it is not possible to add new test calls to
- the test collection anymore.
-
-However, with pytest-2.3 you can use the :ref:`@pytest.fixture` decorator
-and specify ``params`` so that all tests depending on the factory-created
-resource will run multiple times with different parameters.
-
-You can also use the ``pytest_generate_tests`` hook to
-implement the `parametrization scheme of your choice`_. See also
-:ref:`paramexamples` for more examples.
-
-.. _`parametrization scheme of your choice`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/
-
-pytest interaction with other packages
----------------------------------------------------
-
-Issues with pytest, multiprocess and setuptools?
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-On Windows the multiprocess package will instantiate sub processes
-by pickling and thus implicitly re-import a lot of local modules.
-Unfortunately, setuptools-0.6.11 does not ``if __name__=='__main__'``
-protect its generated command line script. This leads to infinite
-recursion when running a test that instantiates Processes.
-
-As of mid-2013, there shouldn't be a problem anymore when you
-use the standard setuptools (note that distribute has been merged
-back into setuptools which is now shipped directly with virtualenv).
-
-.. _nose: https://nose.readthedocs.io/en/latest/
-.. _pylib: https://py.readthedocs.io/en/latest/
-.. _`xUnit style setup`: xunit_setup.html
Test functions can receive fixture objects by naming them as an input
argument. For each argument name, a fixture function with that name provides
the fixture object. Fixture functions are registered by marking them with
-:py:func:`@pytest.fixture <_pytest.python.fixture>`. Let's look at a simple
+:py:func:`@pytest.fixture <pytest.fixture>`. Let's look at a simple
self-contained test module containing a fixture and a test function
using it:
assert 0 # for demo purposes
Here, the ``test_ehlo`` needs the ``smtp_connection`` fixture value. pytest
-will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>`
+will discover and call the :py:func:`@pytest.fixture <pytest.fixture>`
marked ``smtp_connection`` fixture function. Running the test looks like this:
.. code-block:: pytest
Fixtures requiring network access depend on connectivity and are
usually time-expensive to create. Extending the previous example, we
can add a ``scope="module"`` parameter to the
-:py:func:`@pytest.fixture <_pytest.python.fixture>` invocation
+:py:func:`@pytest.fixture <pytest.fixture>` invocation
to cause the decorated ``smtp_connection`` fixture function to only be invoked
once per test *module* (the default is to invoke once per test *function*).
Multiple test functions in a test module will thus
Fixtures can introspect the requesting test context
-------------------------------------------------------------
-Fixture functions can accept the :py:class:`request <FixtureRequest>` object
+Fixture functions can accept the :py:class:`request <_pytest.fixtures.FixtureRequest>` object
to introspect the "requesting" test function, class or module context.
Further extending the previous ``smtp_connection`` fixture example, let's
read an optional server URL from the test module which uses our fixture:
Using markers to pass data to fixtures
-------------------------------------------------------------
-Using the :py:class:`request <FixtureRequest>` object, a fixture can also access
+Using the :py:class:`request <_pytest.fixtures.FixtureRequest>` object, a fixture can also access
markers which are applied to a test function. This can be useful to pass data
into a fixture from a test:
smtp_connection.close()
The main change is the declaration of ``params`` with
-:py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values
+:py:func:`@pytest.fixture <pytest.fixture>`, a list of values
for each of which the fixture function will execute and can access
a value via ``request.param``. No test function code needs to change.
So let's just do another run:
.. _`usefixtures`:
-Using fixtures from classes, modules or projects
-----------------------------------------------------------------------
+Use fixtures in classes and modules with ``usefixtures``
+--------------------------------------------------------
.. regendoc:wipe
In the example above, a parametrized fixture is overridden with a non-parametrized version, and
a non-parametrized fixture is overridden with a parametrized version for certain test module.
The same applies for the test folder level obviously.
+
+
+Using fixtures from other projects
+----------------------------------
+
+Usually projects that provide pytest support will use :ref:`entry points <setuptools entry points>`,
+so just installing those projects into an environment will make those fixtures available for use.
+
+In case you want to use fixtures from a project that does not use entry points, you can
+define :globalvar:`pytest_plugins` in your top ``conftest.py`` file to register that module
+as a plugin.
+
+Suppose you have some fixtures in ``mylibrary.fixtures`` and you want to reuse them into your
+``app/tests`` directory.
+
+All you need to do is to define :globalvar:`pytest_plugins` in ``app/tests/conftest.py``
+pointing to that module.
+
+.. code-block:: python
+
+ pytest_plugins = "mylibrary.fixtures"
+
+This effectively registers ``mylibrary.fixtures`` as a plugin, making all its fixtures and
+hooks available to tests in ``app/tests``.
+
+.. note::
+
+ Sometimes users will *import* fixtures from other projects for use, however this is not
+ recommended: importing fixtures into a module will register them in pytest
+ as *defined* in that module.
+
+ This has minor consequences, such as appearing multiple times in ``pytest --help``,
+ but it is not **recommended** because this behavior might change/stop working
+ in future versions.
performs parametrization at the places where the resource
is used. Moreover, you need to modify the factory to use an
``extrakey`` parameter containing ``request.param`` to the
- :py:func:`~python.Request.cached_setup` call.
+ ``Request.cached_setup`` call.
3. Multiple parametrized session-scoped resources will be active
at the same time, making it hard for them to affect global state
allow to re-use already written factories because effectively
``request.param`` was already used when test functions/classes were
parametrized via
-:py:func:`~_pytest.python.Metafunc.parametrize(indirect=True)` calls.
+:py:func:`metafunc.parametrize(indirect=True) <_pytest.python.Metafunc.parametrize>` calls.
Of course it's perfectly fine to combine parametrization and scoping:
.. code-block:: bash
$ pytest --version
- pytest 6.0.2
+ pytest 6.1.0
.. _`simpletest`:
.. note::
in a future major release of pytest we will introduce class based markers,
- at which point markers will no longer be limited to instances of :py:class:`Mark`.
+ at which point markers will no longer be limited to instances of :py:class:`~_pytest.mark.Mark`.
cache plugin integrated into the core
:orphan:
+.. sidebar:: Next Open Trainings
+
+ - `Professional testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via Python Academy, February 1-3 2021, Leipzig (Germany) and remote.
+
+ Also see `previous talks and blogposts <talks.html>`_.
+
.. _features:
pytest: helps you write better programs
Features
--------
-- Detailed info on failing :ref:`assert statements <assert>` (no need to remember ``self.assert*`` names);
+- Detailed info on failing :ref:`assert statements <assert>` (no need to remember ``self.assert*`` names)
-- :ref:`Auto-discovery <test discovery>` of test modules and functions;
+- :ref:`Auto-discovery <test discovery>` of test modules and functions
-- :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources;
+- :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources
-- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box;
+- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
-- Python 3.5+ and PyPy 3;
+- Python 3.5+ and PyPy 3
-- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;
+- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community
Documentation
slow: marks tests as slow (deselect with '-m "not slow"')
serial
-Note that everything after the ``:`` is an optional description.
+or in your ``pyproject.toml`` file like this:
+
+.. code-block:: toml
+
+ [tool.pytest.ini_options]
+ markers = [
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
+ "serial",
+ ]
+
+Note that everything past the ``:`` after the mark name is an optional description.
Alternatively, you can register new markers programmatically in a
:ref:`pytest_configure <initialization-hooks>` hook:
Unregistered marks applied with the ``@pytest.mark.name_of_the_mark`` decorator
will always emit a warning in order to avoid silently doing something
-surprising due to mis-typed names. As described in the previous section, you can disable
+surprising due to mistyped names. As described in the previous section, you can disable
the warning for custom marks by registering them in your ``pytest.ini`` file or
using a custom ``pytest_configure`` hook.
1. Modifying the behavior of a function or the property of a class for a test e.g.
there is an API call or database connection you will not make for a test but you know
-what the expected output should be. Use :py:meth:`monkeypatch.setattr` to patch the
+what the expected output should be. Use :py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` to patch the
function or property with your desired testing behavior. This can include your own functions.
-Use :py:meth:`monkeypatch.delattr` to remove the function or property for the test.
+Use :py:meth:`monkeypatch.delattr <MonkeyPatch.delattr>` to remove the function or property for the test.
2. Modifying the values of dictionaries e.g. you have a global configuration that
-you want to modify for certain test cases. Use :py:meth:`monkeypatch.setitem` to patch the
-dictionary for the test. :py:meth:`monkeypatch.delitem` can be used to remove items.
+you want to modify for certain test cases. Use :py:meth:`monkeypatch.setitem <MonkeyPatch.setitem>` to patch the
+dictionary for the test. :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` can be used to remove items.
3. Modifying environment variables for a test e.g. to test program behavior if an
environment variable is missing, or to set multiple values to a known variable.
-:py:meth:`monkeypatch.setenv` and :py:meth:`monkeypatch.delenv` can be used for
+:py:meth:`monkeypatch.setenv <MonkeyPatch.setenv>` and :py:meth:`monkeypatch.delenv <MonkeyPatch.delenv>` can be used for
these patches.
4. Use ``monkeypatch.setenv("PATH", value, prepend=os.pathsep)`` to modify ``$PATH``, and
-:py:meth:`monkeypatch.chdir` to change the context of the current working directory
+:py:meth:`monkeypatch.chdir <MonkeyPatch.chdir>` to change the context of the current working directory
during a test.
-5. Use :py:meth:`monkeypatch.syspath_prepend` to modify ``sys.path`` which will also
-call :py:meth:`pkg_resources.fixup_namespace_packages` and :py:meth:`importlib.invalidate_caches`.
+5. Use :py:meth:`monkeypatch.syspath_prepend <MonkeyPatch.syspath_prepend>` to modify ``sys.path`` which will also
+call ``pkg_resources.fixup_namespace_packages`` and :py:func:`importlib.invalidate_caches`.
See the `monkeypatch blog post`_ for some introduction material
and a discussion of its motivation.
can be used to patch functions dependent on the user to always return a
specific value.
-In this example, :py:meth:`monkeypatch.setattr` is used to patch ``Path.home``
+In this example, :py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` is used to patch ``Path.home``
so that the known testing path ``Path("/abc")`` is always used when the test is run.
This removes any dependency on the running user for testing purposes.
-:py:meth:`monkeypatch.setattr` must be called before the function which will use
+:py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` must be called before the function which will use
the patched function is called.
After the test function finishes the ``Path.home`` modification will be undone.
Monkeypatching returned objects: building mock classes
------------------------------------------------------
-:py:meth:`monkeypatch.setattr` can be used in conjunction with classes to mock returned
+:py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` can be used in conjunction with classes to mock returned
objects from functions instead of values.
Imagine a simple function to take an API url and return the json response.
Monkeypatching dictionaries
---------------------------
-:py:meth:`monkeypatch.setitem` can be used to safely set the values of dictionaries
+:py:meth:`monkeypatch.setitem <MonkeyPatch.setitem>` can be used to safely set the values of dictionaries
to specific values during tests. Take this simplified connection string example:
.. code-block:: python
result = app.create_connection_string()
assert result == expected
-You can use the :py:meth:`monkeypatch.delitem` to remove values.
+You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove values.
.. code-block:: python
.. py:function:: pytest.mark.usefixtures(*names)
- :param args: the names of the fixture to use, as strings
+ :param args: The names of the fixture to use, as strings.
.. note::
Condition for marking the test function as xfail (``True/False`` or a
:ref:`condition string <string conditions>`). If a bool, you also have
to specify ``reason`` (see :ref:`condition string <string conditions>`).
- :keyword str reason: Reason why the test function is marked as xfail.
- :keyword Exception raises: Exception subclass expected to be raised by the test function; other exceptions will fail the test.
+ :keyword str reason:
+ Reason why the test function is marked as xfail.
+ :keyword Type[Exception] raises:
+ Exception subclass expected to be raised by the test function; other exceptions will fail the test.
:keyword bool run:
If the test function should actually be executed. If ``False``, the function will always xfail and will
not be executed (useful if a function is segfaulting).
a new release of a library fixes a known bug).
-custom marks
+Custom marks
~~~~~~~~~~~~
Marks are created dynamically using the factory object ``pytest.mark`` and applied as a decorator.
...
Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected
-:class:`Item <_pytest.nodes.Item>`, which can then be accessed by fixtures or hooks with
+:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
:meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
.. code-block:: python
.. autofunction:: _pytest.logging.caplog()
:no-auto-options:
- This returns a :class:`_pytest.logging.LogCaptureFixture` instance.
+ Returns a :class:`_pytest.logging.LogCaptureFixture` instance.
.. autoclass:: _pytest.logging.LogCaptureFixture
:members:
.. autofunction:: _pytest.monkeypatch.monkeypatch()
:no-auto-options:
- This returns a :class:`MonkeyPatch` instance.
+ Returns a :class:`MonkeyPatch` instance.
.. autoclass:: _pytest.monkeypatch.MonkeyPatch
:members:
.. autofunction:: recwarn()
:no-auto-options:
-.. autoclass:: _pytest.recwarn.WarningsRecorder()
+.. autoclass:: WarningsRecorder()
:members:
Each recorded warning is an instance of :class:`warnings.WarningMessage`.
-.. note::
- :class:`RecordedWarning` was changed from a plain class to a namedtuple in pytest 3.1
-
.. note::
``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
differently; see :ref:`ensuring_function_triggers`.
.. autofunction:: pytest_collection
.. autofunction:: pytest_ignore_collect
-.. autofunction:: pytest_collect_directory
.. autofunction:: pytest_collect_file
.. autofunction:: pytest_pycollect_makemodule
Test running (runtest) hooks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object.
+All runtest related hooks receive a :py:class:`pytest.Item <pytest.Item>` object.
.. autofunction:: pytest_runtestloop
.. autofunction:: pytest_runtest_protocol
.. autofunction:: pytest_runtest_makereport
For deeper understanding you may look at the default implementation of
-these hooks in :py:mod:`_pytest.runner` and maybe also
-in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture`
+these hooks in ``_pytest.runner`` and maybe also
+in ``_pytest.pdb`` which interacts with ``_pytest.capture``
and its input/output capturing in order to immediately drop
into interactive debugging when a test failure occurs.
Class
~~~~~
-.. autoclass:: _pytest.python.Class()
+.. autoclass:: pytest.Class()
:members:
:show-inheritance:
Collector
~~~~~~~~~
-.. autoclass:: _pytest.nodes.Collector()
+.. autoclass:: pytest.Collector()
:members:
:show-inheritance:
ExitCode
~~~~~~~~
-.. autoclass:: _pytest.config.ExitCode
+.. autoclass:: pytest.ExitCode
:members:
File
~~~~
-.. autoclass:: _pytest.nodes.File()
+.. autoclass:: pytest.File()
:members:
:show-inheritance:
Function
~~~~~~~~
-.. autoclass:: _pytest.python.Function()
+.. autoclass:: pytest.Function()
:members:
:show-inheritance:
Item
~~~~
-.. autoclass:: _pytest.nodes.Item()
+.. autoclass:: pytest.Item()
:members:
:show-inheritance:
Module
~~~~~~
-.. autoclass:: _pytest.python.Module()
+.. autoclass:: pytest.Module()
:members:
:show-inheritance:
.. autoclass:: _pytest.config.argparsing.Parser()
:members:
-PluginManager
-~~~~~~~~~~~~~
-
-.. autoclass:: pluggy.PluginManager()
- :members:
-
PytestPluginManager
~~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.config.PytestPluginManager()
:members:
:undoc-members:
+ :inherited-members:
:show-inheritance:
Session
~~~~~~~
-.. autoclass:: _pytest.main.Session()
+.. autoclass:: pytest.Session()
:members:
:show-inheritance:
Exceptions
----------
-.. autoclass:: _pytest.config.UsageError()
+.. autoclass:: pytest.UsageError()
:show-inheritance:
.. _`warnings ref`:
[pytest]
xfail_strict = True
+
+
+.. _`command-line-flags`:
+
+Command-line Flags
+------------------
+
+All the command-line flags can be obtained by running ``pytest --help``::
+
+ $ pytest --help
+ usage: pytest [options] [file_or_dir] [file_or_dir] [...]
+
+ positional arguments:
+ file_or_dir
+
+ general:
+ -k EXPRESSION only run tests which match the given substring
+ expression. An expression is a python evaluatable
+ expression where all names are substring-matched
+ against test names and their parent classes.
+ Example: -k 'test_method or test_other' matches all
+ test functions and classes whose name contains
+ 'test_method' or 'test_other', while -k 'not
+ test_method' matches those that don't contain
+ 'test_method' in their names. -k 'not test_method
+ and not test_other' will eliminate the matches.
+ Additionally keywords are matched to classes and
+ functions containing extra names in their
+ 'extra_keyword_matches' set, as well as functions
+ which have names assigned directly to them. The
+ matching is case-insensitive.
+ -m MARKEXPR only run tests matching given mark expression.
+ For example: -m 'mark1 and not mark2'.
+ --markers show markers (builtin, plugin and per-project ones).
+ -x, --exitfirst exit instantly on first error or failed test.
+ --fixtures, --funcargs
+ show available fixtures, sorted by plugin appearance
+ (fixtures with leading '_' are only shown with '-v')
+ --fixtures-per-test show fixtures per test
+ --pdb start the interactive Python debugger on errors or
+ KeyboardInterrupt.
+ --pdbcls=modulename:classname
+ start a custom interactive Python debugger on
+ errors. For example:
+ --pdbcls=IPython.terminal.debugger:TerminalPdb
+ --trace Immediately break when running each test.
+ --capture=method per-test capturing method: one of fd|sys|no|tee-sys.
+ -s shortcut for --capture=no.
+ --runxfail report the results of xfail tests as if they were
+ not marked
+ --lf, --last-failed rerun only the tests that failed at the last run (or
+ all if none failed)
+ --ff, --failed-first run all tests, but run the last failures first.
+ This may re-order tests and thus lead to repeated
+ fixture setup/teardown.
+ --nf, --new-first run tests from new files first, then the rest of the
+ tests sorted by file mtime
+ --cache-show=[CACHESHOW]
+ show cache contents, don't perform collection or
+ tests. Optional argument: glob (default: '*').
+ --cache-clear remove all cache contents at start of test run.
+ --lfnf={all,none}, --last-failed-no-failures={all,none}
+ which tests to run with no previously (known)
+ failures.
+ --sw, --stepwise exit on test failure and continue from last failing
+ test next time
+ --stepwise-skip ignore the first failing test but stop on the next
+ failing test
+
+ reporting:
+ --durations=N show N slowest setup/test durations (N=0 for all).
+ --durations-min=N Minimal duration in seconds for inclusion in slowest
+ list. Default 0.005
+ -v, --verbose increase verbosity.
+ --no-header disable header
+ --no-summary disable summary
+ -q, --quiet decrease verbosity.
+ --verbosity=VERBOSE set verbosity. Default is 0.
+ -r chars show extra test summary info as specified by chars:
+ (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed,
+ (p)assed, (P)assed with output, (a)ll except passed
+ (p/P), or (A)ll. (w)arnings are enabled by default
+ (see --disable-warnings), 'N' can be used to reset
+ the list. (default: 'fE').
+ --disable-warnings, --disable-pytest-warnings
+ disable warnings summary
+ -l, --showlocals show locals in tracebacks (disabled by default).
+ --tb=style traceback print mode
+ (auto/long/short/line/native/no).
+ --show-capture={no,stdout,stderr,log,all}
+ Controls how captured stdout/stderr/log is shown on
+ failed tests. Default is 'all'.
+ --full-trace don't cut any tracebacks (default is to cut).
+ --color=color color terminal output (yes/no/auto).
+ --code-highlight={yes,no}
+ Whether code should be highlighted (only if --color
+ is also enabled)
+ --pastebin=mode send failed|all info to bpaste.net pastebin service.
+ --junit-xml=path create junit-xml style report file at given path.
+ --junit-prefix=str prepend prefix to classnames in junit-xml output
+
+ pytest-warnings:
+ -W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS
+ set which warnings to report, see -W option of
+ python itself.
+ --maxfail=num exit after first num failures or errors.
+ --strict-config any warnings encountered while parsing the `pytest`
+ section of the configuration file raise errors.
+ --strict-markers, --strict
+ markers not registered in the `markers` section of
+ the configuration file raise errors.
+ -c file load configuration from `file` instead of trying to
+ locate one of the implicit configuration files.
+ --continue-on-collection-errors
+ Force test execution even if collection errors
+ occur.
+ --rootdir=ROOTDIR Define root directory for tests. Can be relative
+ path: 'root_dir', './root_dir',
+ 'root_dir/another_dir/'; absolute path:
+ '/home/user/root_dir'; path with variables:
+ '$HOME/root_dir'.
+
+ collection:
+ --collect-only, --co only collect tests, don't execute them.
+ --pyargs try to interpret all arguments as python packages.
+ --ignore=path ignore path during collection (multi-allowed).
+ --ignore-glob=path ignore path pattern during collection (multi-
+ allowed).
+ --deselect=nodeid_prefix
+ deselect item (via node id prefix) during collection
+ (multi-allowed).
+ --confcutdir=dir only load conftest.py's relative to specified dir.
+ --noconftest Don't load any conftest.py files.
+ --keep-duplicates Keep duplicate tests.
+ --collect-in-virtualenv
+ Don't ignore tests in a local virtualenv directory
+ --import-mode={prepend,append,importlib}
+ prepend/append to sys.path when importing test
+ modules and conftest files, default is to prepend.
+ --doctest-modules run doctests in all .py modules
+ --doctest-report={none,cdiff,ndiff,udiff,only_first_failure}
+ choose another output format for diffs on doctest
+ failure
+ --doctest-glob=pat doctests file matching pattern, default: test*.txt
+ --doctest-ignore-import-errors
+ ignore doctest ImportErrors
+ --doctest-continue-on-failure
+ for a given doctest, continue to run after the first
+ failure
+
+ test session debugging and configuration:
+ --basetemp=dir base temporary directory for this test run.(warning:
+ this directory is removed if it exists)
+ -V, --version display pytest version and information about
+ plugins.When given twice, also display information
+ about plugins.
+ -h, --help show help message and configuration info
+ -p name early-load given plugin module name or entry point
+ (multi-allowed).
+ To avoid loading of plugins, use the `no:` prefix,
+ e.g. `no:doctest`.
+ --trace-config trace considerations of conftest.py files.
+ --debug store internal tracing debug information in
+ 'pytestdebug.log'.
+ -o OVERRIDE_INI, --override-ini=OVERRIDE_INI
+ override ini option with "option=value" style, e.g.
+ `-o xfail_strict=True -o cache_dir=cache`.
+ --assert=MODE Control assertion debugging tools.
+ 'plain' performs no assertion debugging.
+ 'rewrite' (the default) rewrites assert statements
+ in test modules on import to provide assert
+ expression information.
+ --setup-only only setup fixtures, do not execute tests.
+ --setup-show show setup of fixtures while executing tests.
+ --setup-plan show what fixtures and tests would be executed but
+ don't execute anything.
+
+ logging:
+ --log-level=LEVEL level of messages to catch/display.
+ Not set by default, so it depends on the root/parent
+ log handler's effective level, where it is "WARNING"
+ by default.
+ --log-format=LOG_FORMAT
+ log format as used by the logging module.
+ --log-date-format=LOG_DATE_FORMAT
+ log date format as used by the logging module.
+ --log-cli-level=LOG_CLI_LEVEL
+ cli logging level.
+ --log-cli-format=LOG_CLI_FORMAT
+ log format as used by the logging module.
+ --log-cli-date-format=LOG_CLI_DATE_FORMAT
+ log date format as used by the logging module.
+ --log-file=LOG_FILE path to a file when logging will be written to.
+ --log-file-level=LOG_FILE_LEVEL
+ log file logging level.
+ --log-file-format=LOG_FILE_FORMAT
+ log format as used by the logging module.
+ --log-file-date-format=LOG_FILE_DATE_FORMAT
+ log date format as used by the logging module.
+ --log-auto-indent=LOG_AUTO_INDENT
+ Auto-indent multiline messages passed to the logging
+ module. Accepts true|on, false|off or an integer.
+
+ [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:
+
+ markers (linelist): markers for test functions
+ empty_parameter_set_mark (string):
+ default marker for empty parametersets
+ norecursedirs (args): directory patterns to avoid for recursion
+ testpaths (args): directories to search for tests when no files or
+ directories are given in the command line.
+ filterwarnings (linelist):
+ Each line specifies a pattern for
+ warnings.filterwarnings. Processed after
+ -W/--pythonwarnings.
+ usefixtures (args): list of default fixtures to be used with this
+ project
+ python_files (args): glob-style file patterns for Python test module
+ discovery
+ python_classes (args):
+ prefixes or glob names for Python test class
+ discovery
+ python_functions (args):
+ prefixes or glob names for Python test function and
+ method discovery
+ disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool):
+ disable string escape non-ascii characters, might
+ cause unwanted side effects(use at your own risk)
+ console_output_style (string):
+ console output: "classic", or with additional
+ progress information ("progress" (percentage) |
+ "count").
+ xfail_strict (bool): default for the strict parameter of xfail markers
+ when not given explicitly (default: False)
+ enable_assertion_pass_hook (bool):
+ Enables the pytest_assertion_pass hook.Make sure to
+ delete any previously generated pyc cache files.
+ junit_suite_name (string):
+ Test suite name for JUnit report
+ junit_logging (string):
+ Write captured log messages to JUnit report: one of
+ no|log|system-out|system-err|out-err|all
+ junit_log_passing_tests (bool):
+ Capture log information for passing tests to JUnit
+ report:
+ junit_duration_report (string):
+ Duration time to report: one of total|call
+ junit_family (string):
+ Emit XML for schema: one of legacy|xunit1|xunit2
+ doctest_optionflags (args):
+ option flags for doctests
+ doctest_encoding (string):
+ encoding used for doctest files
+ cache_dir (string): cache directory path.
+ log_level (string): default value for --log-level
+ log_format (string): default value for --log-format
+ log_date_format (string):
+ default value for --log-date-format
+ log_cli (bool): enable log display during test run (also known as
+ "live logging").
+ log_cli_level (string):
+ default value for --log-cli-level
+ log_cli_format (string):
+ default value for --log-cli-format
+ log_cli_date_format (string):
+ default value for --log-cli-date-format
+ log_file (string): default value for --log-file
+ log_file_level (string):
+ default value for --log-file-level
+ log_file_format (string):
+ default value for --log-file-format
+ log_file_date_format (string):
+ default value for --log-file-date-format
+ log_auto_indent (string):
+ default value for --log-auto-indent
+ faulthandler_timeout (string):
+ Dump the traceback of all threads if a test takes
+ more than TIMEOUT seconds to finish.
+ addopts (args): extra command line options
+ minversion (string): minimally required pytest version
+ required_plugins (args):
+ plugins that must be present for pytest to run
+
+ environment variables:
+ PYTEST_ADDOPTS extra command line options
+ PYTEST_PLUGINS comma-separated plugins to load during startup
+ PYTEST_DISABLE_PLUGIN_AUTOLOAD set to disable plugin auto-loading
+ PYTEST_DEBUG set to enable debug tracing of pytest's internals
+
+
+ to see available markers type: pytest --markers
+ to see available fixtures type: pytest --fixtures
+ (shown according to specified file_or_dir or current dir if not specified; fixtures with leading '_' are only shown with the '-v' option
Talks and Tutorials
==========================
-.. sidebar:: Next Open Trainings
-
- - `Free 1h webinar: "pytest: Test Driven Development für Python" <https://mylearning.ch/kurse/online-kurse/tech-webinar/>`_ (German), online, August 18 2020.
- - `"pytest: Test Driven Development (nicht nur) für Python" <https://workshoptage.ch/workshops/2020/pytest-test-driven-development-nicht-nur-fuer-python/>`_ (German) at the `CH Open Workshoptage <https://workshoptage.ch/>`_, September 8 2020, HSLU Campus Rotkreuz (ZG), Switzerland.
-
-.. _`funcargs`: funcargs.html
-
Books
---------------------------------------------
Talks and blog postings
---------------------------------------------
+- Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020
+
+- Webinar: `Simplify Your Tests with Fixtures <https://blog.jetbrains.com/pycharm/2020/08/webinar-recording-simplify-your-tests-with-fixtures-with-oliver-bestwalter/>`_, Oliver Bestwalter, via JetBrains, 2020
+
+- Training: `Introduction to pytest - simple, rapid and fun testing with Python <https://www.youtube.com/watch?v=CMuSn9cofbI>`_, Florian Bruhin, PyConDE 2019
+
+- Abridged metaprogramming classics - this episode: pytest, Oliver Bestwalter, PyConDE 2019 (`repository <https://github.com/obestwalter/abridged-meta-programming-classics>`__, `recording <https://www.youtube.com/watch?v=zHpeMTJsBRk&feature=youtu.be>`__)
+
+- Testing PySide/PyQt code easily using the pytest framework, Florian Bruhin, Qt World Summit 2019 (`slides <https://bruhin.software/talks/qtws19.pdf>`__, `recording <https://www.youtube.com/watch?v=zdsBS5BXGqQ>`__)
+
- `pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyBCN June 2019 <https://www.slideshare.net/AndreuVallbonaPlazas/pybcn-pytest-recomendaciones-paquetes-bsicos-para-testing-en-python-y-django>`_.
- pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyconES 2017 (`slides in english <http://talks.apsl.io/testing-pycones-2017/>`_, `video in spanish <https://www.youtube.com/watch?v=K20GeR-lXDk>`_)
- `pytest: helps you write better Django apps, Andreas Pelme, DjangoCon
Europe 2014 <https://www.youtube.com/watch?v=aaArYVh6XSM>`_.
-- :ref:`fixtures`
-
- `Testing Django Applications with pytest, Andreas Pelme, EuroPython
2013 <https://www.youtube.com/watch?v=aUf8Fkb7TaY>`_.
:Exit code 4: pytest command line usage error
:Exit code 5: No tests were collected
-They are represented by the :class:`_pytest.config.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
+They are represented by the :class:`pytest.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
.. code-block:: python
pytest -h | --help # show help on command line and config file options
+The full command-line flags can be found in the :ref:`reference <command-line-flags>`.
+
.. _maxfail:
Stopping after the first (or N) failures
Profiling test execution duration
-------------------------------------
+.. versionchanged:: 6.0
-To get a list of the slowest 10 test durations:
+To get a list of the slowest 10 test durations over 1.0s long:
.. code-block:: bash
- pytest --durations=10
+ pytest --durations=10 --durations-min=1.0
-By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line.
+By default, pytest will not show test durations that are too small (<0.005s) unless ``-vv`` is passed on the command-line.
.. _faulthandler:
FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ...
1 failed in 0.12s
-The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option.
-For example, the configuration below will ignore all user warnings, but will transform
+The same option can be set in the ``pytest.ini`` or ``pyproject.toml`` file using the
+``filterwarnings`` ini option. For example, the configuration below will ignore all
+user warnings and specific deprecation warnings matching a regex, but will transform
all other warnings into errors.
.. code-block:: ini
+ # pytest.ini
[pytest]
filterwarnings =
error
ignore::UserWarning
+ ignore:function ham\(\) is deprecated:DeprecationWarning
+
+.. code-block:: toml
+
+ # pyproject.toml
+ [tool.pytest.ini_options]
+ filterwarnings = [
+ "error",
+ "ignore::UserWarning",
+ # note the use of single quote below to denote "raw" strings in TOML
+ 'ignore:function ham\(\) is deprecated:DeprecationWarning',
+ ]
When a warning matches more than one option in the list, the action for the last matching option
.. currentmodule:: _pytest.warnings
-Full API: :class:`WarningsRecorder`.
+Full API: :class:`~_pytest.recwarn.WarningsRecorder`.
.. _custom_failure_messages:
result.assert_outcomes(passed=4)
-additionally it is possible to copy examples for an example folder before running pytest on it
+Additionally it is possible to copy examples for an example folder before running pytest on it.
.. code-block:: ini
Declaring new hooks
------------------------
+.. note::
+
+ This is a quick overview on how to add new hooks and how they work in general, but a more complete
+ overview can be found in `the pluggy documentation <https://pluggy.readthedocs.io/en/latest/>`__.
+
.. currentmodule:: _pytest.hookspec
Plugins and ``conftest.py`` files may declare new hooks that can then be
documentation describing when the hook will be called and what return values
are expected. The names of the functions must start with `pytest_` otherwise pytest won't recognize them.
-Here's an example. Let's assume this code is in the ``hooks.py`` module.
+Here's an example. Let's assume this code is in the ``sample_hook.py`` module.
.. code-block:: python
.. code-block:: python
def pytest_addhooks(pluginmanager):
- """ This example assumes the hooks are grouped in the 'hooks' module. """
- from my_app.tests import hooks
+ """ This example assumes the hooks are grouped in the 'sample_hook' module. """
+ from my_app.tests import sample_hook
- pluginmanager.add_hookspecs(hooks)
+ pluginmanager.add_hookspecs(sample_hook)
For a real world example, see `newhooks.py`_ from `xdist <https://github.com/pytest-dev/pytest-xdist>`_.
.. note::
While these setup/teardown methods are simple and familiar to those
- coming from a ``unittest`` or nose ``background``, you may also consider
+ coming from a ``unittest`` or ``nose`` background, you may also consider
using pytest's more powerful :ref:`fixture mechanism
<fixture>` which leverages the concept of dependency injection, allowing
for a more modular and more scalable approach for managing test state,
import json
+from pathlib import Path
-import py
import requests
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
def main(args):
- cachefile = py.path.local(args.cache)
+ cachefile = Path(args.cache)
if not cachefile.exists() or args.refresh:
issues = get_issues()
- cachefile.write(json.dumps(issues))
+ cachefile.write_text(json.dumps(issues), "utf-8")
else:
- issues = json.loads(cachefile.read())
+ issues = json.loads(cachefile.read_text("utf-8"))
open_issues = [x for x in issues if x["state"] == "open"]
[build-system]
requires = [
# sync with setup.py until we discard non-pep-517/518
- "setuptools>=40.0",
- "setuptools-scm",
+ "setuptools>=42.0",
+ "setuptools-scm[toml]>=3.4",
"wheel",
]
build-backend = "setuptools.build_meta"
+[tool.setuptools_scm]
+write_to = "src/_pytest/_version.py"
+
[tool.pytest.ini_options]
minversion = "2.0"
addopts = "-rfEX -p pytester --strict-markers"
-python_files = ["test_*.py", "*_test.py", "testing/*/*.py"]
+python_files = ["test_*.py", "*_test.py", "testing/python/*.py"]
python_classes = ["Test", "Acceptance"]
python_functions = ["test"]
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
This script is part of the pytest release process which is triggered by comments
in issues.
-This script is started by the `release-on-comment.yml` workflow, which is triggered by two comment
-related events:
+This script is started by the `release-on-comment.yml` workflow, which always executes on
+`master` and is triggered by two comment related events:
* https://help.github.com/en/actions/reference/events-that-trigger-workflows#issue-comment-event-issue_comment
* https://help.github.com/en/actions/reference/events-that-trigger-workflows#issues-event-issues
import json
import os
import re
-import sys
+import traceback
from pathlib import Path
from subprocess import CalledProcessError
from subprocess import check_call
def trigger_release(payload_path: Path, token: str) -> None:
- error_contents = "" # to be used to store error output in case any command fails
payload, base_branch, is_major = validate_and_get_issue_comment_payload(
payload_path
)
issue.create_comment(str(e))
print_and_exit(f"{Fore.RED}{e}")
+ error_contents = ""
try:
print(f"Version: {Fore.CYAN}{version}")
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
+ # important to use tox here because we have changed branches, so dependencies
+ # might have changed as well
+ cmdline = ["tox", "-e", "release", "--", version, "--skip-check-links"]
+ print("Running", " ".join(cmdline))
run(
- [sys.executable, "scripts/release.py", version, "--skip-check-links"],
- text=True,
- check=True,
- capture_output=True,
+ cmdline, text=True, check=True, capture_output=True,
)
oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git"
)
print(f"Notified in original comment {Fore.CYAN}{comment.url}{Fore.RESET}.")
- print(f"{Fore.GREEN}Success.")
except CalledProcessError as e:
- error_contents = e.output
- except Exception as e:
- error_contents = str(e)
- link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}"
- issue.create_comment(
- dedent(
- f"""
- Sorry, the request to prepare release `{version}` from {base_branch} failed with:
-
- ```
- {e}
- ```
-
- See: {link}.
- """
- )
- )
- print_and_exit(f"{Fore.RED}{e}")
+ error_contents = f"CalledProcessError\noutput:\n{e.output}\nstderr:\n{e.stderr}"
+ except Exception:
+ error_contents = f"Exception:\n{traceback.format_exc()}"
if error_contents:
link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}"
- issue.create_comment(
- dedent(
- f"""
- Sorry, the request to prepare release `{version}` from {base_branch} failed with:
-
- ```
- {error_contents}
- ```
-
- See: {link}.
- """
- )
+ msg = ERROR_COMMENT.format(
+ version=version, base_branch=base_branch, contents=error_contents, link=link
)
+ issue.create_comment(msg)
print_and_exit(f"{Fore.RED}{error_contents}")
+ else:
+ print(f"{Fore.GREEN}Success.")
+
+
+ERROR_COMMENT = """\
+The request to prepare release `{version}` from {base_branch} failed with:
+
+```
+{contents}
+```
+
+See: {link}.
+"""
def find_next_version(base_branch: str, is_major: bool) -> str:
-"""
-Invoke development tasks.
-"""
+"""Invoke development tasks."""
import argparse
import os
from pathlib import Path
install_requires =
attrs>=17.4.0
iniconfig
- more-itertools>=4.0.0
packaging
pluggy>=0.12,<1.0
py>=1.8.2
[mypy]
mypy_path = src
check_untyped_defs = True
+disallow_any_generics = True
ignore_missing_imports = True
no_implicit_optional = True
show_error_codes = True
strict_equality = True
warn_redundant_casts = True
warn_return_any = True
+warn_unreachable = True
warn_unused_configs = True
+no_implicit_reexport = True
from setuptools import setup
-
-def main():
- setup(use_scm_version={"write_to": "src/_pytest/_version.py"})
-
-
if __name__ == "__main__":
- main()
+ setup()
-"""allow bash-completion for argparse with argcomplete if installed
-needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
+"""Allow bash-completion for argparse with argcomplete if installed.
+
+Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
to find the magic string, so _ARGCOMPLETE env. var is never set, and
-this does not need special code.
+this does not need special code).
Function try_argcomplete(parser) should be called directly before
the call to ArgumentParser.parse_args().
arguments specification, in order to get "dirname/" after "dirn<TAB>"
instead of the default "dirname ":
- optparser.add_argument(Config._file_or_dir, nargs='*'
- ).completer=filescompleter
+ optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
Other, application specific, completers should go in the file
doing the add_argument calls as they need to be specified as .completer
SPEEDUP
=======
+
The generic argcomplete script for bash-completion
-(/etc/bash_completion.d/python-argcomplete.sh )
+(/etc/bash_completion.d/python-argcomplete.sh)
uses a python program to determine startup script generated by pip.
You can speed up completion somewhat by changing this script to include
# PYTHON_ARGCOMPLETE_OK
so the the python-argcomplete-check-easy-install-script does not
need to be called to find the entry point of the code and see if that is
-marked with PYTHON_ARGCOMPLETE_OK
+marked with PYTHON_ARGCOMPLETE_OK.
INSTALL/DEBUGGING
=================
+
To include this support in another application that has setup.py generated
scripts:
-- add the line:
+
+- Add the line:
# PYTHON_ARGCOMPLETE_OK
- near the top of the main python entry point
-- include in the file calling parse_args():
+ near the top of the main python entry point.
+
+- Include in the file calling parse_args():
from _argcomplete import try_argcomplete, filescompleter
- , call try_argcomplete just before parse_args(), and optionally add
- filescompleter to the positional arguments' add_argument()
+ Call try_argcomplete just before parse_args(), and optionally add
+ filescompleter to the positional arguments' add_argument().
+
If things do not work right away:
-- switch on argcomplete debugging with (also helpful when doing custom
+
+- Switch on argcomplete debugging with (also helpful when doing custom
completers):
export _ARC_DEBUG=1
-- run:
+
+- Run:
python-argcomplete-check-easy-install-script $(which appname)
echo $?
- will echo 0 if the magic line has been found, 1 if not
-- sometimes it helps to find early on errors using:
+ will echo 0 if the magic line has been found, 1 if not.
+
+- Sometimes it helps to find early on errors using:
_ARGCOMPLETE=1 _ARC_DEBUG=1 appname
which should throw a KeyError: 'COMPLINE' (which is properly set by the
global argcomplete script).
class FastFilesCompleter:
- "Fast file completer class"
+ """Fast file completer class."""
def __init__(self, directories: bool = True) -> None:
self.directories = directories
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
- """only called on non option completions"""
+ # Only called on non option completions.
if os.path.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
else:
completion = []
globbed = []
if "*" not in prefix and "?" not in prefix:
- # we are on unix, otherwise no bash
+ # We are on unix, otherwise no bash.
if not prefix or prefix[-1] == os.path.sep:
globbed.extend(glob(prefix + ".*"))
prefix += "*"
for x in sorted(globbed):
if os.path.isdir(x):
x += "/"
- # append stripping the prefix (like bash, not like compgen)
+ # Append stripping the prefix (like bash, not like compgen).
completion.append(x[prefix_dir:])
return completion
from .code import filter_traceback
from .code import Frame
from .code import getfslineno
-from .code import getrawcode
from .code import Traceback
from .code import TracebackEntry
+from .source import getrawcode
from .source import Source
__all__ = [
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest.compat import ATTRS_EQ_FIELD
+from _pytest.compat import final
from _pytest.compat import get_real_func
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
+from _pytest.pathlib import Path
if TYPE_CHECKING:
from typing import Type
@property
def path(self) -> Union[py.path.local, str]:
- """Return a path object pointing to source code (or a str in case
- of OSError / non-existing file).
- """
+ """Return a path object pointing to source code, or an ``str`` in
+ case of ``OSError`` / non-existing file."""
if not self.raw.co_filename:
return ""
try:
Mostly for internal use.
"""
- f = self.frame
- tbh = f.f_locals.get(
- "__tracebackhide__", f.f_globals.get("__tracebackhide__", False)
+ tbh = (
+ False
) # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]]
+ for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
+ # in normal cases, f_locals and f_globals are dictionaries
+ # however via `exec(...)` / `eval(...)` they can be other types
+ # (even incorrect types!).
+ # as such, we suppress all exceptions while accessing __tracebackhide__
+ try:
+ tbh = maybe_ns_dct["__tracebackhide__"]
+ except Exception:
+ pass
+ else:
+ break
if tbh and callable(tbh):
return tbh(None if self._excinfo is None else self._excinfo())
return tbh
@overload
def __getitem__(self, key: int) -> TracebackEntry:
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def __getitem__(self, key: slice) -> "Traceback": # noqa: F811
- raise NotImplementedError()
+ ...
def __getitem__( # noqa: F811
self, key: Union[int, slice]
_E = TypeVar("_E", bound=BaseException, covariant=True)
+@final
@attr.s(repr=False)
class ExceptionInfo(Generic[_E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
exc_info: Tuple["Type[_E]", "_E", TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[_E]":
- """Returns an ExceptionInfo for an existing exc_info tuple.
+ """Return an ExceptionInfo for an existing exc_info tuple.
.. warning::
Experimental API
- :param exprinfo: a text string helping to determine if we should
- strip ``AssertionError`` from the output, defaults
- to the exception message/``__str__()``
+ :param exprinfo:
+ A text string helping to determine if we should strip
+ ``AssertionError`` from the output. Defaults to the exception
+ message/``__str__()``.
"""
_striptext = ""
if exprinfo is None and isinstance(exc_info[1], AssertionError):
def from_current(
cls, exprinfo: Optional[str] = None
) -> "ExceptionInfo[BaseException]":
- """Returns an ExceptionInfo matching the current traceback.
+ """Return an ExceptionInfo matching the current traceback.
.. warning::
Experimental API
- :param exprinfo: a text string helping to determine if we should
- strip ``AssertionError`` from the output, defaults
- to the exception message/``__str__()``
+ :param exprinfo:
+ A text string helping to determine if we should strip
+ ``AssertionError`` from the output. Defaults to the exception
+ message/``__str__()``.
"""
tup = sys.exc_info()
assert tup[0] is not None, "no current exception"
return cls(None)
def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
- """fill an unfilled ExceptionInfo created with for_later()"""
+ """Fill an unfilled ExceptionInfo created with ``for_later()``."""
assert self._excinfo is None, "ExceptionInfo was already filled"
self._excinfo = exc_info
Show locals per traceback entry.
Ignored if ``style=="native"``.
- :param str style: long|short|no|native|value traceback style
+ :param str style:
+ long|short|no|native|value traceback style.
:param bool abspath:
If paths should be changed to absolute or left unchanged.
:param bool truncate_locals:
With ``showlocals==True``, make sure locals can be safely represented as strings.
- :param bool chain: if chained exceptions in Python 3 should be shown.
+ :param bool chain:
+ If chained exceptions in Python 3 should be shown.
.. versionchanged:: 3.9
)
return fmt.repr_excinfo(self)
- def match(self, regexp: "Union[str, Pattern]") -> "Literal[True]":
+ def match(self, regexp: "Union[str, Pattern[str]]") -> "Literal[True]":
"""Check whether the regular expression `regexp` matches the string
representation of the exception using :func:`python:re.search`.
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
def _getindent(self, source: "Source") -> int:
- # figure out indent for given source
+ # Figure out indent for the given source.
try:
s = str(source.getstatement(len(source) - 1))
except KeyboardInterrupt:
def get_source(
self,
- source: "Source",
+ source: Optional["Source"],
line_index: int = -1,
- excinfo: Optional[ExceptionInfo] = None,
+ excinfo: Optional[ExceptionInfo[BaseException]] = None,
short: bool = False,
) -> List[str]:
"""Return formatted and marked up source lines."""
return lines
def get_exconly(
- self, excinfo: ExceptionInfo, indent: int = 4, markall: bool = False
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ indent: int = 4,
+ markall: bool = False,
) -> List[str]:
lines = []
indentstr = " " * indent
- # get the real exception information out
+ # Get the real exception information out.
exlines = excinfo.exconly(tryshort=True).split("\n")
failindent = self.fail_marker + indentstr[1:]
for line in exlines:
str_repr = saferepr(value)
else:
str_repr = safeformat(value)
- # if len(str_repr) < 70 or not isinstance(value,
- # (list, tuple, dict)):
+ # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
lines.append("{:<10} = {}".format(name, str_repr))
# else:
# self._line("%-10s =\\" % (name,))
return None
def repr_traceback_entry(
- self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None
+ self,
+ entry: TracebackEntry,
+ excinfo: Optional[ExceptionInfo[BaseException]] = None,
) -> "ReprEntry":
lines = [] # type: List[str]
style = entry._repr_style if entry._repr_style is not None else self.style
path = np
return path
- def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback":
+ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
traceback = excinfo.traceback
if self.tbfilter:
traceback = traceback.filter()
def _truncate_recursive_traceback(
self, traceback: Traceback
) -> Tuple[Traceback, Optional[str]]:
- """
- Truncate the given recursive traceback trying to find the starting point
- of the recursion.
+ """Truncate the given recursive traceback trying to find the starting
+ point of the recursion.
- The detection is done by going through each traceback entry and finding the
- point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``.
+ The detection is done by going through each traceback entry and
+ finding the point in which the locals of the frame are equal to the
+ locals of a previous frame (see ``recursionindex()``).
- Handle the situation where the recursion process might raise an exception (for example
- comparing numpy arrays using equality raises a TypeError), in which case we do our best to
- warn the user of the error and show a limited traceback.
+ Handle the situation where the recursion process might raise an
+ exception (for example comparing numpy arrays using equality raises a
+ TypeError), in which case we do our best to warn the user of the
+ error and show a limited traceback.
"""
try:
recursionindex = traceback.recursionindex()
return traceback, extraline
- def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr":
+ def repr_excinfo(
+ self, excinfo: ExceptionInfo[BaseException]
+ ) -> "ExceptionChainRepr":
repr_chain = (
[]
) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
- e = excinfo.value
- excinfo_ = excinfo # type: Optional[ExceptionInfo]
+ e = excinfo.value # type: Optional[BaseException]
+ excinfo_ = excinfo # type: Optional[ExceptionInfo[BaseException]]
descr = None
seen = set() # type: Set[int]
while e is not None and id(e) not in seen:
excinfo_._getreprcrash() if self.style != "value" else None
) # type: Optional[ReprFileLocation]
else:
- # fallback to native repr if the exception doesn't have a traceback:
- # ExceptionInfo objects require a full traceback to work
+ # Fallback to native repr if the exception doesn't have a traceback:
+ # ExceptionInfo objects require a full traceback to work.
reprtraceback = ReprTracebackNative(
traceback.format_exception(type(e), e, None)
)
# This class is abstract -- only subclasses are instantiated.
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
class ExceptionRepr(TerminalRepr):
- # Provided by in subclasses.
+ # Provided by subclasses.
reprcrash = None # type: Optional[ReprFileLocation]
reprtraceback = None # type: ReprTraceback
def __attrs_post_init__(self) -> None:
super().__attrs_post_init__()
# reprcrash and reprtraceback of the outermost (the newest) exception
- # in the chain
+ # in the chain.
self.reprtraceback = self.chain[-1][0]
self.reprcrash = self.chain[-1][1]
entrysep = "_ "
def toterminal(self, tw: TerminalWriter) -> None:
- # the entries might have different styles
+ # The entries might have different styles.
for i, entry in enumerate(self.reprentries):
if entry.style == "long":
tw.line("")
style = attr.ib(type="_TracebackStyle")
def _write_entry_lines(self, tw: TerminalWriter) -> None:
- """Writes the source code portions of a list of traceback entries with syntax highlighting.
+ """Write the source code portions of a list of traceback entries with syntax highlighting.
Usually entries are lines like these:
message = attr.ib(type=str)
def toterminal(self, tw: TerminalWriter) -> None:
- # filename and lineno output for each entry,
- # using an output format that most editors understand
+ # Filename and lineno output for each entry, using an output format
+ # that most editors understand.
msg = self.message
i = msg.find("\n")
if i != -1:
return code.path, code.firstlineno
-# relative paths that we use to filter traceback entries from appearing to the user;
-# see filter_traceback
+# Relative paths that we use to filter traceback entries from appearing to the user;
+# see filter_traceback.
# note: if we need to add more paths than what we have now we should probably use a list
-# for better maintenance
+# for better maintenance.
-_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc"))
+_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
# pluggy is either a package or a single module depending on the version
-if _PLUGGY_DIR.basename == "__init__.py":
- _PLUGGY_DIR = _PLUGGY_DIR.dirpath()
-_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath()
-_PY_DIR = py.path.local(py.__file__).dirpath()
+if _PLUGGY_DIR.name == "__init__.py":
+ _PLUGGY_DIR = _PLUGGY_DIR.parent
+_PYTEST_DIR = Path(_pytest.__file__).parent
+_PY_DIR = Path(py.__file__).parent
def filter_traceback(entry: TracebackEntry) -> bool:
* internal traceback from pytest or its internal libraries, py and pluggy.
"""
# entry.path might sometimes return a str object when the entry
- # points to dynamically generated code
- # see https://bitbucket.org/pytest-dev/py/issues/71
+ # points to dynamically generated code.
+ # See https://bitbucket.org/pytest-dev/py/issues/71.
raw_filename = entry.frame.code.raw.co_filename
is_generated = "<" in raw_filename and ">" in raw_filename
if is_generated:
return False
+
# entry.path might point to a non-existing file, in which case it will
- # also return a str object. see #1133
- p = py.path.local(entry.path)
- return (
- not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR)
- )
+ # also return a str object. See #1133.
+ p = Path(entry.path)
+
+ parents = p.parents
+ if _PLUGGY_DIR in parents:
+ return False
+ if _PYTEST_DIR in parents:
+ return False
+ if _PY_DIR in parents:
+ return False
+
+ return True
@overload
def __getitem__(self, key: int) -> str:
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def __getitem__(self, key: slice) -> "Source": # noqa: F811
- raise NotImplementedError()
+ ...
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811
if isinstance(key, int):
return len(self.lines)
def strip(self) -> "Source":
- """ return new source object with trailing
- and leading blank lines removed.
- """
+ """Return new Source object with trailing and leading blank lines removed."""
start, end = 0, len(self)
while start < end and not self.lines[start].strip():
start += 1
return source
def indent(self, indent: str = " " * 4) -> "Source":
- """ return a copy of the source object with
- all lines indented by the given indent-string.
- """
+ """Return a copy of the source object with all lines indented by the
+ given indent-string."""
newsource = Source()
newsource.lines = [(indent + line) for line in self.lines]
return newsource
def getstatement(self, lineno: int) -> "Source":
- """ return Source statement which contains the
- given linenumber (counted from 0).
- """
+ """Return Source statement which contains the given linenumber
+ (counted from 0)."""
start, end = self.getstatementrange(lineno)
return self[start:end]
def getstatementrange(self, lineno: int) -> Tuple[int, int]:
- """ return (start, end) tuple which spans the minimal
- statement region which containing the given lineno.
- """
+ """Return (start, end) tuple which spans the minimal statement region
+ which containing the given lineno."""
if not (0 <= lineno < len(self)):
raise IndexError("lineno out of range")
ast, start, end = getstatementrange_ast(lineno, self)
return start, end
def deindent(self) -> "Source":
- """return a new source object deindented."""
+ """Return a new Source object deindented."""
newsource = Source()
newsource.lines[:] = deindent(self.lines)
return newsource
def getrawcode(obj, trycall: bool = True):
- """ return code object for given function. """
+ """Return code object for given function."""
try:
return obj.__code__
except AttributeError:
def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
- # flatten all statements and except handlers into one lineno-list
- # AST's line numbers start indexing at 1
+ # Flatten all statements and except handlers into one lineno-list.
+ # AST's line numbers start indexing at 1.
values = [] # type: List[int]
for x in ast.walk(node):
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
for name in ("finalbody", "orelse"):
val = getattr(x, name, None) # type: Optional[List[ast.stmt]]
if val:
- # treat the finally/orelse part as its own statement
+ # Treat the finally/orelse part as its own statement.
values.append(val[0].lineno - 1 - 1)
values.sort()
insert_index = bisect_right(values, lineno)
if astnode is None:
content = str(source)
# See #4260:
- # don't produce duplicate warnings when compiling source to find ast
+ # Don't produce duplicate warnings when compiling source to find AST.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
astnode = ast.parse(content, "source", "exec")
start, end = get_statement_startend2(lineno, astnode)
- # we need to correct the end:
+ # We need to correct the end:
# - ast-parsing strips comments
# - there might be empty lines
# - we might have lesser indented code blocks at the end
end = len(source.lines)
if end > start + 1:
- # make sure we don't span differently indented code blocks
- # by using the BlockFinder helper used which inspect.getsource() uses itself
+ # Make sure we don't span differently indented code blocks
+ # by using the BlockFinder helper used which inspect.getsource() uses itself.
block_finder = inspect.BlockFinder()
- # if we start with an indented line, put blockfinder to "started" mode
+ # If we start with an indented line, put blockfinder to "started" mode.
block_finder.started = source.lines[start][0].isspace()
it = ((x + "\n") for x in source.lines[start:end])
try:
except Exception:
pass
- # the end might still point to a comment or empty line, correct it
+ # The end might still point to a comment or empty line, correct it.
while end:
line = source.lines[end - 1].lstrip()
if line.startswith("#") or not line:
class SafeRepr(reprlib.Repr):
- """subclass of repr.Repr that limits the resulting size of repr()
- and includes information on exceptions raised during the call.
- """
+ """repr.Repr that limits the resulting size of repr() and includes
+ information on exceptions raised during the call."""
def __init__(self, maxsize: int) -> None:
super().__init__()
def safeformat(obj: object) -> str:
- """return a pretty printed string for the given object.
+ """Return a pretty printed string for the given object.
+
Failing __repr__ functions of user instances will be represented
with a short exception info.
"""
def saferepr(obj: object, maxsize: int = 240) -> str:
- """return a size-limited safe repr-string for the given object.
+ """Return a size-limited safe repr-string for the given object.
+
Failing __repr__ functions of user instances will be represented
with a short exception info and 'saferepr' generally takes
- care to never raise exceptions itself. This function is a wrapper
- around the Repr/reprlib functionality of the standard 2.6 lib.
+ care to never raise exceptions itself.
+
+ This function is a wrapper around the Repr/reprlib functionality of the
+ standard 2.6 lib.
"""
return SafeRepr(maxsize).repr(obj)
from typing import TextIO
from .wcwidth import wcswidth
+from _pytest.compat import final
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
)
+@final
class TerminalWriter:
_esctable = dict(
black=30,
) -> None:
if fullwidth is None:
fullwidth = self.fullwidth
- # the goal is to have the line be as long as possible
- # under the condition that len(line) <= fullwidth
+ # The goal is to have the line be as long as possible
+ # under the condition that len(line) <= fullwidth.
if sys.platform == "win32":
- # if we print in the last column on windows we are on a
+ # If we print in the last column on windows we are on a
# new line but there is no way to verify/neutralize this
- # (we may not know the exact line width)
- # so let's be defensive to avoid empty lines in the output
+ # (we may not know the exact line width).
+ # So let's be defensive to avoid empty lines in the output.
fullwidth -= 1
if title is not None:
# we want 2 + 2*len(fill) + len(title) <= fullwidth
# we want len(sepchar)*N <= fullwidth
# i.e. N <= fullwidth // len(sepchar)
line = sepchar * (fullwidth // len(sepchar))
- # in some situations there is room for an extra sepchar at the right,
+ # In some situations there is room for an extra sepchar at the right,
# in particular if we consider that with a sepchar like "_ " the
- # trailing space is not important at the end of the line
+ # trailing space is not important at the end of the line.
if len(line) + len(sepchar.rstrip()) <= fullwidth:
line += sepchar.rstrip()
-"""
-support for presenting detailed information in failing assertions.
-"""
+"""Support for presenting detailed information in failing assertions."""
import sys
from typing import Any
from typing import Generator
actually imported, usually in your __init__.py if you are a plugin
using a package.
- :raise TypeError: if the given module names are not strings.
+ :raises TypeError: If the given module names are not strings.
"""
for name in names:
if not isinstance(name, str):
- msg = "expected module names as *args, got {0} instead"
+ msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
raise TypeError(msg.format(repr(names)))
for hook in sys.meta_path:
if isinstance(hook, rewrite.AssertionRewritingHook):
def pytest_collection(session: "Session") -> None:
- # this hook is only called when test modules are collected
+ # This hook is only called when test modules are collected
# so for example not in the master process of pytest-xdist
- # (which does not collect test modules)
+ # (which does not collect test modules).
assertstate = session.config._store.get(assertstate_key, None)
if assertstate:
if assertstate.hook is not None:
@hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
- """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks
+ """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
- The rewrite module will use util._reprcompare if
- it exists to use custom reporting via the
- pytest_assertrepr_compare hook. This sets up this custom
+ The rewrite module will use util._reprcompare if it exists to use custom
+ reporting via the pytest_assertrepr_compare hook. This sets up this custom
comparison for the test.
"""
ihook = item.ihook
def callbinrepr(op, left: object, right: object) -> Optional[str]:
- """Call the pytest_assertrepr_compare hook and prepare the result
+ """Call the pytest_assertrepr_compare hook and prepare the result.
This uses the first result from the hook and then ensures the
following:
-"""Rewrite assertion AST to produce nice error messages"""
+"""Rewrite assertion AST to produce nice error messages."""
import ast
import errno
import functools
from typing import Callable
from typing import Dict
from typing import IO
+from typing import Iterable
from typing import List
from typing import Optional
from typing import Sequence
exec(co, module.__dict__)
def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
- """This is a fast way to get out of rewriting modules.
+ """A fast way to get out of rewriting modules.
Profiling has shown that the call to PathFinder.find_spec (inside of
the find_spec from this class) is a major slowdown, so, this method
def _warn_already_imported(self, name: str) -> None:
from _pytest.warning_types import PytestAssertRewriteWarning
- from _pytest.warnings import _issue_warning_captured
- _issue_warning_captured(
+ self.config.issue_config_time_warning(
PytestAssertRewriteWarning(
"Module already imported so cannot be rewritten: %s" % name
),
- self.config.hook,
stacklevel=5,
)
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
- """read and rewrite *fn* and return the code object."""
+ """Read and rewrite *fn* and return the code object."""
fn_ = fspath(fn)
stat = os.stat(fn_)
with open(fn_, "rb") as f:
def _saferepr(obj: object) -> str:
- """Get a safe repr of an object for assertion error messages.
+ r"""Get a safe repr of an object for assertion error messages.
The assertion formatting (util.format_explanation()) requires
newlines to be escaped since they are a special character for it.
custom repr it is possible to contain one of the special escape
sequences, especially '\n{' and '\n}' are likely to be present in
JSON reprs.
-
"""
return saferepr(obj).replace("\n", "\\n")
def _format_assertmsg(obj: object) -> str:
- """Format the custom assertion message given.
+ r"""Format the custom assertion message given.
For strings this simply replaces newlines with '\n~' so that
util.format_explanation() will preserve them instead of escaping
newlines. For other objects saferepr() is used first.
-
"""
# reprlib appears to have a bug which means that if a string
# contains a newline it gets escaped, however if an object has a
return True
-def _format_boolop(explanations, is_or: bool):
+def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
- if isinstance(explanation, str):
- return explanation.replace("%", "%%")
- else:
- return explanation.replace(b"%", b"%%")
+ return explanation.replace("%", "%%")
def _call_reprcompare(
def _check_if_assertion_pass_impl() -> bool:
- """Checks if any plugins implement the pytest_assertion_pass hook
- in order not to generate explanation unecessarily (might be expensive)"""
+ """Check if any plugins implement the pytest_assertion_pass hook
+ in order not to generate explanation unecessarily (might be expensive)."""
return True if util._assertion_pass else False
def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
- """Returns a mapping from {lineno: "assertion test expression"}"""
+ """Return a mapping from {lineno: "assertion test expression"}."""
ret = {} # type: Dict[int, str]
depth = 0
This state is reset on every new assert statement visited and used
by the other visitors.
-
"""
def __init__(
return
expect_docstring = False
elif (
- not isinstance(item, ast.ImportFrom)
- or item.level > 0
- or item.module != "__future__"
+ isinstance(item, ast.ImportFrom)
+ and item.level == 0
+ and item.module == "__future__"
):
- lineno = item.lineno
+ pass
+ else:
break
pos += 1
+ # Special case: for a decorated function, set the lineno to that of the
+ # first decorator, not the `def`. Issue #4984.
+ if isinstance(item, ast.FunctionDef) and item.decorator_list:
+ lineno = item.decorator_list[0].lineno
else:
lineno = item.lineno
imports = [
node = nodes.pop()
for name, field in ast.iter_fields(node):
if isinstance(field, list):
- new = [] # type: List
+ new = [] # type: List[ast.AST]
for i, child in enumerate(field):
if isinstance(child, ast.Assert):
# Transform assert.
current formatting context, e.g. ``%(py0)s``. The placeholder
and expr are placed in the current format context so that it
can be used on the next call to .pop_format_context().
-
"""
specifier = "py" + str(next(self.variable_counter))
self.explanation_specifiers[specifier] = expr
.explanation_param(). Finally .pop_format_context() is used
to format a string of %-formatted values as added by
.explanation_param().
-
"""
self.explanation_specifiers = {} # type: Dict[str, ast.expr]
self.stack.append(self.explanation_specifiers)
the %-placeholders created by .explanation_param(). This will
add the required code to format said string to .expl_stmts and
return the ast.Name instance of the formatted string.
-
"""
current = self.stack.pop()
if self.stack:
intermediate values and replace it with an if statement which
raises an assertion error with a detailed explanation in case
the expression is false.
-
"""
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
from _pytest.warning_types import PytestAssertRewriteWarning
return res, explanation
def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
- """
- visit `ast.Call` nodes
- """
new_func, func_expl = self.visit(call.func)
arg_expls = []
new_args = []
return res, outer_expl
def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
- # From Python 3.5, a Starred node can appear in a function call
+ # From Python 3.5, a Starred node can appear in a function call.
res, expl = self.visit(starred.value)
new_starred = ast.Starred(res, starred.ctx)
return new_starred, "*" + expl
def try_makedirs(cache_dir: Path) -> bool:
- """Attempts to create the given directory and sub-directories exist, returns True if
- successful or it already exists"""
+ """Attempt to create the given directory and sub-directories exist.
+
+ Returns True if successful or if it already exists.
+ """
try:
os.makedirs(fspath(cache_dir), exist_ok=True)
except (FileNotFoundError, NotADirectoryError, FileExistsError):
def get_cache_dir(file_path: Path) -> Path:
- """Returns the cache directory to write .pyc files for the given .py file path"""
+ """Return the cache directory to write .pyc files for the given .py file path."""
if sys.version_info >= (3, 8) and sys.pycache_prefix:
# given:
# prefix = '/tmp/pycs'
-"""
-Utilities for truncating assertion output.
+"""Utilities for truncating assertion output.
Current default behaviour is to truncate assertion explanations at
~8 terminal lines, unless running in "-vv" mode or running on CI.
def truncate_if_required(
explanation: List[str], item: Item, max_length: Optional[int] = None
) -> List[str]:
- """
- Truncate this assertion explanation if the given test item is eligible.
- """
+ """Truncate this assertion explanation if the given test item is eligible."""
if _should_truncate_item(item):
return _truncate_explanation(explanation)
return explanation
def _should_truncate_item(item: Item) -> bool:
- """
- Whether or not this test item is eligible for truncation.
- """
+ """Whether or not this test item is eligible for truncation."""
verbose = item.config.option.verbose
return verbose < 2 and not _running_on_ci()
max_lines: Optional[int] = None,
max_chars: Optional[int] = None,
) -> List[str]:
- """
- Truncate given list of strings that makes up the assertion explanation.
+ """Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches
first. The remaining lines will be replaced by a usage message.
-"""Utilities for assertion debugging"""
+"""Utilities for assertion debugging."""
import collections.abc
import pprint
from typing import AbstractSet
def format_explanation(explanation: str) -> str:
- """This formats an explanation
+ r"""Format an explanation.
Normally all embedded newlines are escaped, however there are
three exceptions: \n{, \n} and \n~. The first two are intended
def _split_explanation(explanation: str) -> List[str]:
- """Return a list of individual lines in the explanation
+ r"""Return a list of individual lines in the explanation.
This will return a list of lines split on '\n{', '\n}' and '\n~'.
Any other newlines will be escaped and appear in the line as the
def _format_lines(lines: Sequence[str]) -> List[str]:
- """Format the individual lines
+ """Format the individual lines.
- This will replace the '{', '}' and '~' characters of our mini
- formatting language with the proper 'where ...', 'and ...' and ' +
- ...' text, taking care of indentation along the way.
+ This will replace the '{', '}' and '~' characters of our mini formatting
+ language with the proper 'where ...', 'and ...' and ' + ...' text, taking
+ care of indentation along the way.
Return a list of formatted lines.
"""
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
- """Return specialised explanations for some operators/operands"""
+ """Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose")
if verbose > 1:
left_repr = safeformat(left)
-"""
-merged implementation of the cache provider
-
-the name cache was not chosen to ensure pluggy automatically
-ignores the external pytest-cache
-"""
+"""Implementation of the cache provider."""
+# This plugin was not named "cache" to avoid conflicts with the external
+# pytest-cache version.
import json
import os
from typing import Dict
from .reports import CollectReport
from _pytest import nodes
from _pytest._io import TerminalWriter
+from _pytest.compat import final
from _pytest.compat import order_preserving_dict
from _pytest.config import Config
from _pytest.config import ExitCode
"""
+@final
@attr.s
class Cache:
_cachedir = attr.ib(type=Path, repr=False)
@classmethod
def clear_cache(cls, cachedir: Path) -> None:
- """Clears the sub-directories used to hold cached directories and values."""
+ """Clear the sub-directories used to hold cached directories and values."""
for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
d = cachedir / prefix
if d.is_dir():
@staticmethod
def cache_dir_from_config(config: Config) -> Path:
- return resolve_from_str(config.getini("cache_dir"), config.rootdir)
+ return resolve_from_str(config.getini("cache_dir"), config.rootpath)
def warn(self, fmt: str, **args: object) -> None:
import warnings
)
def makedir(self, name: str) -> py.path.local:
- """ return a directory path object with the given name. If the
- directory does not yet exist, it will be created. You can use it
- to manage files likes e. g. store/retrieve database
- dumps across test sessions.
-
- :param name: must be a string not containing a ``/`` separator.
- Make sure the name contains your plugin or application
- identifiers to prevent clashes with other cache users.
+ """Return a directory path object with the given name.
+
+ If the directory does not yet exist, it will be created. You can use
+ it to manage files to e.g. store/retrieve database dumps across test
+ sessions.
+
+ :param name:
+ Must be a string not containing a ``/`` separator.
+ Make sure the name contains your plugin or application
+ identifiers to prevent clashes with other cache users.
"""
path = Path(name)
if len(path.parts) > 1:
return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
def get(self, key: str, default):
- """ return cached value for the given key. If no value
- was yet cached or the value cannot be read, the specified
- default is returned.
+ """Return the cached value for the given key.
- :param key: must be a ``/`` separated value. Usually the first
- name is the name of your plugin or your application.
- :param default: must be provided in case of a cache-miss or
- invalid cache values.
+ If no value was yet cached or the value cannot be read, the specified
+ default is returned.
+ :param key:
+ Must be a ``/`` separated value. Usually the first
+ name is the name of your plugin or your application.
+ :param default:
+ The value to return in case of a cache-miss or invalid cache value.
"""
path = self._getvaluepath(key)
try:
return default
def set(self, key: str, value: object) -> None:
- """ save value for the given key.
-
- :param key: must be a ``/`` separated value. Usually the first
- name is the name of your plugin or your application.
- :param value: must be of any combination of basic
- python types, including nested types
- like e. g. lists of dictionaries.
+ """Save value for the given key.
+
+ :param key:
+ Must be a ``/`` separated value. Usually the first
+ name is the name of your plugin or your application.
+ :param value:
+ Must be of any combination of basic python types,
+ including nested types like lists of dictionaries.
"""
path = self._getvaluepath(key)
try:
self._collected_at_least_one_failure = False
@pytest.hookimpl(hookwrapper=True)
- def pytest_make_collect_report(self, collector: nodes.Collector) -> Generator:
+ def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, Session):
out = yield
res = out.get_result() # type: CollectReport
class LFPlugin:
- """ Plugin which implements the --lf (run last-failing) option """
+ """Plugin which implements the --lf (run last-failing) option."""
def __init__(self, config: Config) -> None:
self.config = config
)
def get_last_failed_paths(self) -> Set[Path]:
- """Returns a set with all Paths()s of the previously failed nodeids."""
- rootpath = Path(str(self.config.rootdir))
+ """Return a set with all Paths()s of the previously failed nodeids."""
+ rootpath = self.config.rootpath
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
return {x for x in result if x.exists()}
class NFPlugin:
- """ Plugin which implements the --nf (run new-first) option """
+ """Plugin which implements the --nf (run new-first) option."""
def __init__(self, config: Config) -> None:
self.config = config
@pytest.fixture
def cache(request: FixtureRequest) -> Cache:
- """
- Return a cache object that can persist state between testing sessions.
+ """Return a cache object that can persist state between testing sessions.
cache.get(key, default)
cache.set(key, value)
- Keys must be a ``/`` separated value, where the first part is usually the
+ Keys must be ``/`` separated strings, where the first part is usually the
name of your plugin or application to avoid clashes with other cache users.
Values can be any object handled by the json stdlib module.
# starting with .., ../.. if sensible
try:
- displaypath = cachedir.relative_to(str(config.rootdir))
+ displaypath = cachedir.relative_to(config.rootpath)
except ValueError:
displaypath = cachedir
return "cachedir: {}".format(displaypath)
-"""
-per-test stdout/stderr capturing mechanism.
-
-"""
-import collections
+"""Per-test stdout/stderr capturing mechanism."""
import contextlib
+import functools
import io
import os
import sys
from io import UnsupportedOperation
from tempfile import TemporaryFile
+from typing import Any
+from typing import AnyStr
from typing import Generator
+from typing import Generic
+from typing import Iterator
from typing import Optional
from typing import TextIO
from typing import Tuple
from typing import Union
import pytest
+from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config.argparsing import Parser
def _colorama_workaround() -> None:
- """
- Ensure colorama is imported so that it attaches to the correct stdio
+ """Ensure colorama is imported so that it attaches to the correct stdio
handles on Windows.
colorama uses the terminal on import time. So if something does the
def _readline_workaround() -> None:
- """
- Ensure readline is imported so that it attaches to the correct stdio
+ """Ensure readline is imported so that it attaches to the correct stdio
handles on Windows.
Pdb uses readline support where available--when not running from the Python
workaround ensures that readline is imported before I/O capture is setup so
that it can attach to the actual stdin/out for the console.
- See https://github.com/pytest-dev/pytest/pull/1281
+ See https://github.com/pytest-dev/pytest/pull/1281.
"""
if sys.platform.startswith("win32"):
try:
def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
- """
- Python 3.6 implemented unicode console handling for Windows. This works
+ """Workaround for Windows Unicode console handling on Python>=3.6.
+
+ Python 3.6 implemented Unicode console handling for Windows. This works
by reading/writing to the raw console handle using
``{Read,Write}ConsoleW``.
also means a different handle by replicating the logic in
"Py_lifecycle.c:initstdio/create_stdio".
- :param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given
+ :param stream:
+ In practice ``sys.stdout`` or ``sys.stderr``, but given
here as parameter for unittesting purposes.
- See https://github.com/pytest-dev/py/issues/103
+ See https://github.com/pytest-dev/py/issues/103.
"""
if (
not sys.platform.startswith("win32")
):
return
- # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666)
- if not hasattr(stream, "buffer"):
+ # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
+ if not hasattr(stream, "buffer"): # type: ignore[unreachable]
return
buffered = hasattr(stream.buffer, "raw")
capman = CaptureManager(ns.capture)
pluginmanager.register(capman, "capturemanager")
- # make sure that capturemanager is properly reset at final shutdown
+ # Make sure that capturemanager is properly reset at final shutdown.
early_config.add_cleanup(capman.stop_global_capturing)
- # finally trigger conftest loading but while capturing (issue93)
+ # Finally trigger conftest loading but while capturing (issue #93).
capman.start_global_capturing()
outcome = yield
capman.suspend_global_capture()
class FDCaptureBinary:
- """Capture IO to/from a given os-level filedescriptor.
+ """Capture IO to/from a given OS-level file descriptor.
- snap() produces `bytes`
+ snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
)
def start(self) -> None:
- """ Start capturing on targetfd using memorized tmpfile. """
+ """Start capturing on targetfd using memorized tmpfile."""
self._assert_state("start", ("initialized",))
os.dup2(self.tmpfile.fileno(), self.targetfd)
self.syscapture.start()
return res
def done(self) -> None:
- """ stop capturing, restore streams, return original capture file,
- seeked to position zero. """
+ """Stop capturing, restore streams, return original capture file,
+ seeked to position zero."""
self._assert_state("done", ("initialized", "started", "suspended", "done"))
if self._state == "done":
return
self._state = "started"
def writeorg(self, data):
- """ write to original file descriptor. """
+ """Write to original file descriptor."""
self._assert_state("writeorg", ("started", "suspended"))
os.write(self.targetfd_save, data)
class FDCapture(FDCaptureBinary):
- """Capture IO to/from a given os-level filedescriptor.
+ """Capture IO to/from a given OS-level file descriptor.
- snap() produces text
+ snap() produces text.
"""
# Ignore type because it doesn't match the type in the superclass (bytes).
return res
def writeorg(self, data):
- """ write to original file descriptor. """
+ """Write to original file descriptor."""
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
# MultiCapture
-CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
+# This class was a namedtuple, but due to mypy limitation[0] it could not be
+# made generic, so was replaced by a regular class which tries to emulate the
+# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
+# make it a namedtuple again.
+# [0]: https://github.com/python/mypy/issues/685
+@final
+@functools.total_ordering
+class CaptureResult(Generic[AnyStr]):
+ """The result of :method:`CaptureFixture.readouterr`."""
+
+ # Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272
+ if sys.version_info >= (3, 5, 3):
+ __slots__ = ("out", "err")
+
+ def __init__(self, out: AnyStr, err: AnyStr) -> None:
+ self.out = out # type: AnyStr
+ self.err = err # type: AnyStr
+
+ def __len__(self) -> int:
+ return 2
+
+ def __iter__(self) -> Iterator[AnyStr]:
+ return iter((self.out, self.err))
+
+ def __getitem__(self, item: int) -> AnyStr:
+ return tuple(self)[item]
+
+ def _replace(
+ self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
+ ) -> "CaptureResult[AnyStr]":
+ return CaptureResult(
+ out=self.out if out is None else out, err=self.err if err is None else err
+ )
+
+ def count(self, value: AnyStr) -> int:
+ return tuple(self).count(value)
+
+ def index(self, value) -> int:
+ return tuple(self).index(value)
-class MultiCapture:
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, (CaptureResult, tuple)):
+ return NotImplemented
+ return tuple(self) == tuple(other)
+
+ def __hash__(self) -> int:
+ return hash(tuple(self))
+
+ def __lt__(self, other: object) -> bool:
+ if not isinstance(other, (CaptureResult, tuple)):
+ return NotImplemented
+ return tuple(self) < tuple(other)
+
+ def __repr__(self) -> str:
+ return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err)
+
+
+class MultiCapture(Generic[AnyStr]):
_state = None
_in_suspended = False
if self.err:
self.err.start()
- def pop_outerr_to_orig(self):
- """ pop current snapshot out/err capture and flush to orig streams. """
+ def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:
+ """Pop current snapshot out/err capture and flush to orig streams."""
out, err = self.readouterr()
if out:
self.out.writeorg(out)
self._in_suspended = False
def stop_capturing(self) -> None:
- """ stop capturing and reset capturing streams """
+ """Stop capturing and reset capturing streams."""
if self._state == "stopped":
raise ValueError("was already stopped")
self._state = "stopped"
"""Whether actively capturing -- not suspended or stopped."""
return self._state == "started"
- def readouterr(self) -> CaptureResult:
+ def readouterr(self) -> CaptureResult[AnyStr]:
if self.out:
out = self.out.snap()
else:
return CaptureResult(out, err)
-def _get_multicapture(method: "_CaptureMethod") -> MultiCapture:
+def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
if method == "fd":
return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
elif method == "sys":
class CaptureManager:
- """
- Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
- test phase (setup, call, teardown). After each of those points, the captured output is obtained and
- attached to the collection/runtest report.
+ """The capture plugin.
+
+ Manages that the appropriate capture method is enabled/disabled during
+ collection and each test phase (setup, call, teardown). After each of
+ those points, the captured output is obtained and attached to the
+ collection/runtest report.
There are two levels of capture:
- * global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled
- during collection and each test phase.
- * fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this
- case special handling is needed to ensure the fixtures take precedence over the global capture.
+
+ * global: enabled by default and can be suppressed by the ``-s``
+ option. This is always enabled/disabled during collection and each test
+ phase.
+
+ * fixture: when a test function or one of its fixture depend on the
+ ``capsys`` or ``capfd`` fixtures. In this case special handling is
+ needed to ensure the fixtures take precedence over the global capture.
"""
def __init__(self, method: "_CaptureMethod") -> None:
self._method = method
- self._global_capturing = None # type: Optional[MultiCapture]
- self._capture_fixture = None # type: Optional[CaptureFixture]
+ self._global_capturing = None # type: Optional[MultiCapture[str]]
+ self._capture_fixture = None # type: Optional[CaptureFixture[Any]]
def __repr__(self) -> str:
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
self.resume_global_capture()
self.resume_fixture()
- def read_global_capture(self):
+ def read_global_capture(self) -> CaptureResult[str]:
assert self._global_capturing is not None
return self._global_capturing.readouterr()
# Fixture Control
- def set_fixture(self, capture_fixture: "CaptureFixture") -> None:
+ def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None:
if self._capture_fixture:
current_fixture = self._capture_fixture.request.fixturename
requested_fixture = capture_fixture.request.fixturename
self._capture_fixture = None
def activate_fixture(self) -> None:
- """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
- the global capture.
- """
+ """If the current item is using ``capsys`` or ``capfd``, activate
+ them so they take precedence over the global capture."""
if self._capture_fixture:
self._capture_fixture._start()
def deactivate_fixture(self) -> None:
- """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
+ """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
if self._capture_fixture:
self._capture_fixture.close()
self.stop_global_capturing()
-class CaptureFixture:
- """
- Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary`
- fixtures.
- """
+class CaptureFixture(Generic[AnyStr]):
+ """Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`,
+ :py:func:`capfd` and :py:func:`capfdbinary` fixtures."""
def __init__(self, captureclass, request: SubRequest) -> None:
self.captureclass = captureclass
self.request = request
- self._capture = None # type: Optional[MultiCapture]
+ self._capture = None # type: Optional[MultiCapture[AnyStr]]
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER
self._capture.stop_capturing()
self._capture = None
- def readouterr(self):
- """Read and return the captured output so far, resetting the internal buffer.
+ def readouterr(self) -> CaptureResult[AnyStr]:
+ """Read and return the captured output so far, resetting the internal
+ buffer.
- :return: captured content as a namedtuple with ``out`` and ``err`` string attributes
+ :returns:
+ The captured content as a namedtuple with ``out`` and ``err``
+ string attributes.
"""
captured_out, captured_err = self._captured_out, self._captured_err
if self._capture is not None:
return CaptureResult(captured_out, captured_err)
def _suspend(self) -> None:
- """Suspends this fixture's own capturing temporarily."""
+ """Suspend this fixture's own capturing temporarily."""
if self._capture is not None:
self._capture.suspend_capturing()
def _resume(self) -> None:
- """Resumes this fixture's own capturing temporarily."""
+ """Resume this fixture's own capturing temporarily."""
if self._capture is not None:
self._capture.resume_capturing()
@contextlib.contextmanager
def disabled(self) -> Generator[None, None, None]:
- """Temporarily disables capture while inside the 'with' block."""
+ """Temporarily disable capturing while inside the ``with`` block."""
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
with capmanager.global_and_fixture_disabled():
yield
@pytest.fixture
-def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]:
+def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
``out`` and ``err`` will be ``text`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
- capture_fixture = CaptureFixture(SysCapture, request)
+ capture_fixture = CaptureFixture[str](SysCapture, request)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@pytest.fixture
-def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]:
+def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()``
``out`` and ``err`` will be ``bytes`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
- capture_fixture = CaptureFixture(SysCaptureBinary, request)
+ capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@pytest.fixture
-def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]:
+def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
``out`` and ``err`` will be ``text`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
- capture_fixture = CaptureFixture(FDCapture, request)
+ capture_fixture = CaptureFixture[str](FDCapture, request)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@pytest.fixture
-def capfdbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]:
+def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
``out`` and ``err`` will be ``byte`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
- capture_fixture = CaptureFixture(FDCaptureBinary, request)
+ capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
-"""
-python version compatibility code
-"""
+"""Python version compatibility code."""
import enum
import functools
import inspect
from typing import Callable
from typing import Generic
from typing import Optional
-from typing import overload
+from typing import overload as overload
from typing import Tuple
from typing import TypeVar
from typing import Union
import attr
-import py
-from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
def fspath(p):
"""os.fspath replacement, useful to point out when we should replace it by the
- real function once we drop py35.
- """
+ real function once we drop py35."""
return str(p)
def iscoroutinefunction(func: object) -> bool:
- """
- Return True if func is a coroutine function (a function defined with async
+ """Return True if func is a coroutine function (a function defined with async
def syntax, and doesn't contain yield), or a function decorated with
@asyncio.coroutine.
def is_async_function(func: object) -> bool:
- """Return True if the given function seems to be an async function or async generator"""
+ """Return True if the given function seems to be an async function or
+ an async generator."""
return iscoroutinefunction(func) or (
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
)
-def getlocation(function, curdir=None) -> str:
+def getlocation(function, curdir: Optional[str] = None) -> str:
+ from _pytest.pathlib import Path
+
function = get_real_func(function)
- fn = py.path.local(inspect.getfile(function))
+ fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno
if curdir is not None:
- relfn = fn.relto(curdir)
- if relfn:
+ try:
+ relfn = fn.relative_to(curdir)
+ except ValueError:
+ pass
+ else:
return "%s:%d" % (relfn, lineno + 1)
return "%s:%d" % (fn, lineno + 1)
def num_mock_patch_args(function) -> int:
- """ return number of arguments used up by mock arguments (if any) """
+ """Return number of arguments used up by mock arguments (if any)."""
patchings = getattr(function, "patchings", None)
if not patchings:
return 0
is_method: bool = False,
cls: Optional[type] = None
) -> Tuple[str, ...]:
- """Returns the names of a function's mandatory arguments.
+ """Return the names of a function's mandatory arguments.
- This should return the names of all function arguments that:
- * Aren't bound to an instance or type as in instance or class methods.
- * Don't have default values.
- * Aren't bound with functools.partial.
- * Aren't replaced with mocks.
+ Should return the names of all function arguments that:
+ * Aren't bound to an instance or type as in instance or class methods.
+ * Don't have default values.
+ * Aren't bound with functools.partial.
+ * Aren't replaced with mocks.
The is_method and cls arguments indicate that the function should
be treated as a bound method even though it's not unless, only in
p.name
for p in parameters.values()
if (
- p.kind is Parameter.POSITIONAL_OR_KEYWORD
- or p.kind is Parameter.KEYWORD_ONLY
+ # TODO: Remove type ignore after https://github.com/python/typeshed/pull/4383
+ p.kind is Parameter.POSITIONAL_OR_KEYWORD # type: ignore[unreachable]
+ or p.kind is Parameter.KEYWORD_ONLY # type: ignore[unreachable]
)
and p.default is Parameter.empty
)
else:
- from contextlib import nullcontext # noqa
+ from contextlib import nullcontext as nullcontext # noqa: F401
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
- # Note: this code intentionally mirrors the code at the beginning of getfuncargnames,
- # to get the arguments which were excluded from its result because they had default values
+ # Note: this code intentionally mirrors the code at the beginning of
+ # getfuncargnames, to get the arguments which were excluded from its result
+ # because they had default values.
return tuple(
p.name
for p in signature(function).parameters.values()
def ascii_escaped(val: Union[bytes, str]) -> str:
- """If val is pure ascii, returns it as a str(). Otherwise, escapes
+ r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes:
- b'\xc3\xb4\xc5\xd6' -> '\\xc3\\xb4\\xc5\\xd6'
+ b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
and escapes unicode objects into a sequence of escaped unicode
ids, e.g.:
- '4\\nV\\U00043efa\\x0eMXWB\\x1e\\u3028\\u15fd\\xcd\\U0007d944'
+ r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
- note:
- the obvious "v.decode('unicode-escape')" will return
- valid utf-8 unicode if it finds them in bytes, but we
+ Note:
+ The obvious "v.decode('unicode-escape')" will return
+ valid UTF-8 unicode if it finds them in bytes, but we
want to return escaped bytes for any byte, even if they match
- a utf-8 string.
-
+ a UTF-8 string.
"""
if isinstance(val, bytes):
ret = _bytes_to_ascii(val)
class _PytestWrapper:
"""Dummy wrapper around a function object for internal use only.
- Used to correctly unwrap the underlying function object
- when we are creating fixtures, because we wrap the function object ourselves with a decorator
- to issue warnings when the fixture function is called directly.
+ Used to correctly unwrap the underlying function object when we are
+ creating fixtures, because we wrap the function object ourselves with a
+ decorator to issue warnings when the fixture function is called directly.
"""
obj = attr.ib()
def get_real_func(obj):
- """ gets the real function object of the (possibly) wrapped object by
- functools.wraps or functools.partial.
- """
+ """Get the real function object of the (possibly) wrapped object by
+ functools.wraps or functools.partial."""
start_obj = obj
for i in range(100):
# __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
break
obj = new_obj
else:
+ from _pytest._io.saferepr import saferepr
+
raise ValueError(
("could not find real function of {start}\nstopped at {current}").format(
start=saferepr(start_obj), current=saferepr(obj)
def get_real_method(obj, holder):
- """
- Attempts to obtain the real function object that might be wrapping ``obj``, while at the same time
- returning a bound method to ``holder`` if the original object was a bound method.
- """
+ """Attempt to obtain the real function object that might be wrapping
+ ``obj``, while at the same time returning a bound method to ``holder`` if
+ the original object was a bound method."""
try:
is_method = hasattr(obj, "__func__")
obj = get_real_func(obj)
def safe_getattr(object: Any, name: str, default: Any) -> Any:
- """ Like getattr but return default upon any Exception or any OutcomeException.
+ """Like getattr but return default upon any Exception or any OutcomeException.
Attribute access can potentially fail for 'evil' Python objects.
See issue #214.
- It catches OutcomeException because of #2490 (issue #580), new outcomes are derived from BaseException
- instead of Exception (for more details check #2707)
+ It catches OutcomeException because of #2490 (issue #580), new outcomes
+ are derived from BaseException instead of Exception (for more details
+ check #2707).
"""
try:
return getattr(object, name, default)
return f
+if TYPE_CHECKING:
+ if sys.version_info >= (3, 8):
+ from typing import final as final
+ else:
+ from typing_extensions import final as final
+elif sys.version_info >= (3, 8):
+ from typing import final as final
+else:
+
+ def final(f): # noqa: F811
+ return f
+
+
if getattr(attr, "__version_info__", ()) >= (19, 2):
ATTRS_EQ_FIELD = "eq"
else:
if sys.version_info >= (3, 8):
- from functools import cached_property
+ from functools import cached_property as cached_property
else:
class cached_property(Generic[_S, _T]):
def __get__(
self, instance: None, owner: Optional["Type[_S]"] = ...
) -> "cached_property[_S, _T]":
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def __get__( # noqa: F811
self, instance: _S, owner: Optional["Type[_S]"] = ...
) -> _T:
- raise NotImplementedError()
+ ...
def __get__(self, instance, owner=None): # noqa: F811
if instance is None:
#
# With `assert_never` we can do better:
#
-# // throw new Error('unreachable');
+# // raise Exception('unreachable')
# return assert_never(x)
#
# Now, if we forget to handle the new variant, the type-checker will emit a
-""" command line options, ini-file and conftest.py processing. """
+"""Command line options, ini-file and conftest.py processing."""
import argparse
import collections.abc
import contextlib
import enum
import inspect
import os
+import re
import shlex
import sys
import types
from typing import Any
from typing import Callable
from typing import Dict
+from typing import Generator
from typing import IO
from typing import Iterable
from typing import Iterator
import _pytest._code
import _pytest.deprecated
-import _pytest.hookspec # the extension point definitions
-from .exceptions import PrintHelp
-from .exceptions import UsageError
+import _pytest.hookspec
+from .exceptions import PrintHelp as PrintHelp
+from .exceptions import UsageError as UsageError
from .findpaths import determine_setup
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter
+from _pytest.compat import final
from _pytest.compat import importlib_metadata
from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
+from _pytest.pathlib import bestrelpath
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import Path
_PluggyPlugin = object
"""A type to represent plugin objects.
+
Plugins can be any namespace, so we can't narrow it down much, but we use an
alias to make the intent clear.
-Ideally this type would be provided by pluggy itself."""
+
+Ideally this type would be provided by pluggy itself.
+"""
hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest")
+@final
class ExitCode(enum.IntEnum):
- """
- .. versionadded:: 5.0
-
- Encodes the valid exit codes by pytest.
+ """Encodes the valid exit codes by pytest.
Currently users and plugins may supply other exit codes as well.
+
+ .. versionadded:: 5.0
"""
- #: tests passed
+ #: Tests passed.
OK = 0
- #: tests failed
+ #: Tests failed.
TESTS_FAILED = 1
- #: pytest was interrupted
+ #: pytest was interrupted.
INTERRUPTED = 2
- #: an internal error got in the way
+ #: An internal error got in the way.
INTERNAL_ERROR = 3
- #: pytest was misused
+ #: pytest was misused.
USAGE_ERROR = 4
- #: pytest couldn't find tests
+ #: pytest couldn't find tests.
NO_TESTS_COLLECTED = 5
def filter_traceback_for_conftest_import_failure(
entry: _pytest._code.TracebackEntry,
) -> bool:
- """filters tracebacks entries which point to pytest internals or importlib.
+ """Filter tracebacks entries which point to pytest internals or importlib.
Make a special case for importlib because we use it to import test modules and conftest files
in _pytest.pathlib.import_path.
def main(
- args: Optional[List[str]] = None,
+ args: Optional[Union[List[str], py.path.local]] = None,
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
) -> Union[int, ExitCode]:
- """ return exit code, after performing an in-process test run.
+ """Perform an in-process test run.
- :arg args: list of command line arguments.
+ :param args: List of command line arguments.
+ :param plugins: List of plugin objects to be auto-registered during initialization.
- :arg plugins: list of plugin objects to be auto-registered during
- initialization.
+ :returns: An exit code.
"""
try:
try:
def console_main() -> int:
- """pytest's CLI entry point.
+ """The CLI entry point of pytest.
This function is not meant for programmable use; use `main()` instead.
"""
def filename_arg(path: str, optname: str) -> str:
- """ Argparse type validator for filename arguments.
+ """Argparse type validator for filename arguments.
- :path: path of filename
- :optname: name of the option
+ :path: Path of filename.
+ :optname: Name of the option.
"""
if os.path.isdir(path):
raise UsageError("{} must be a filename, given: {}".format(optname, path))
def directory_arg(path: str, optname: str) -> str:
"""Argparse type validator for directory arguments.
- :path: path of directory
- :optname: name of the option
+ :path: Path of directory.
+ :optname: Name of the option.
"""
if not os.path.isdir(path):
raise UsageError("{} must be a directory, given: {}".format(optname, path))
"nose",
"assertion",
"junitxml",
- "resultlog",
"doctest",
"cacheprovider",
"freeze_support",
def get_plugin_manager() -> "PytestPluginManager":
- """
- Obtain a new instance of the
+ """Obtain a new instance of the
:py:class:`_pytest.config.PytestPluginManager`, with default plugins
already loaded.
raise
+@final
class PytestPluginManager(PluginManager):
- """
- Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add pytest-specific
- functionality:
+ """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
+ additional pytest-specific functionality:
- * loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
- ``pytest_plugins`` global variables found in plugins being loaded;
- * ``conftest.py`` loading during start-up;
+ * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
+ ``pytest_plugins`` global variables found in plugins being loaded.
+ * ``conftest.py`` loading during start-up.
"""
def __init__(self) -> None:
self._noconftest = False
self._duplicatepaths = set() # type: Set[py.path.local]
+ # plugins that were explicitly skipped with pytest.skip
+ # list of (module name, skip reason)
+ # previously we would issue a warning when a plugin was skipped, but
+ # since we refactored warnings as first citizens of Config, they are
+ # just stored here to be used later.
+ self.skipped_plugins = [] # type: List[Tuple[str, str]]
+
self.add_hookspecs(_pytest.hookspec)
self.register(self)
if os.environ.get("PYTEST_DEBUG"):
# Config._consider_importhook will set a real object if required.
self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
- # Used to know when we are importing conftests after the pytest_configure stage
+ # Used to know when we are importing conftests after the pytest_configure stage.
self._configured = False
def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
- # pytest hooks are always prefixed with pytest_
+ # pytest hooks are always prefixed with "pytest_",
# so we avoid accessing possibly non-readable attributes
- # (see issue #1073)
+ # (see issue #1073).
if not name.startswith("pytest_"):
return
- # ignore names which can not be hooks
+ # Ignore names which can not be hooks.
if name == "pytest_plugins":
return
method = getattr(plugin, name)
opts = super().parse_hookimpl_opts(plugin, name)
- # consider only actual functions for hooks (#3775)
+ # Consider only actual functions for hooks (#3775).
if not inspect.isroutine(method):
return
- # collect unmarked hooks as long as they have the `pytest_' prefix
+ # Collect unmarked hooks as long as they have the `pytest_' prefix.
if opts is None and name.startswith("pytest_"):
opts = {}
if opts is not None:
return ret
def getplugin(self, name: str):
- # support deprecated naming because plugins (xdist e.g.) use it
+ # Support deprecated naming because plugins (xdist e.g.) use it.
plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin]
return plugin
def hasplugin(self, name: str) -> bool:
- """Return True if the plugin with the given name is registered."""
+ """Return whether a plugin with the given name is registered."""
return bool(self.get_plugin(name))
def pytest_configure(self, config: "Config") -> None:
+ """:meta private:"""
# XXX now that the pluginmanager exposes hookimpl(tryfirst...)
- # we should remove tryfirst/trylast as markers
+ # we should remove tryfirst/trylast as markers.
config.addinivalue_line(
"markers",
"tryfirst: mark a hook implementation function such that the "
self._configured = True
#
- # internal API for local conftest plugin handling
+ # Internal API for local conftest plugin handling.
#
def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
- """ load initial conftest files given a preparsed "namespace".
- As conftest files may add their own command line options
- which have arguments ('--my-opt somepath') we might get some
- false positives. All builtin and 3rd party plugins will have
- been loaded, however, so common options will not confuse our logic
- here.
+ """Load initial conftest files given a preparsed "namespace".
+
+ As conftest files may add their own command line options which have
+ arguments ('--my-opt somepath') we might get some false positives.
+ All builtin and 3rd party plugins will have been loaded, however, so
+ common options will not confuse our logic here.
"""
current = py.path.local()
self._confcutdir = (
else:
directory = path
- # XXX these days we may rather want to use config.rootdir
+ # XXX these days we may rather want to use config.rootpath
# and allow users to opt into looking into the rootdir parent
- # directories instead of requiring to specify confcutdir
+ # directories instead of requiring to specify confcutdir.
clist = []
for parent in directory.parts():
if self._confcutdir and self._confcutdir.relto(parent):
def _importconftest(
self, conftestpath: py.path.local, importmode: Union[str, ImportMode],
) -> types.ModuleType:
- # Use a resolved Path object as key to avoid loading the same conftest twice
- # with build systems that create build directories containing
+ # Use a resolved Path object as key to avoid loading the same conftest
+ # twice with build systems that create build directories containing
# symlinks to actual files.
# Using Path().resolve() is better than py.path.realpath because
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
if name in essential_plugins:
raise UsageError("plugin %s cannot be disabled" % name)
- # PR #4304 : remove stepwise if cacheprovider is blocked
+ # PR #4304: remove stepwise if cacheprovider is blocked.
if name == "cacheprovider":
self.set_blocked("stepwise")
self.set_blocked("pytest_stepwise")
self.import_plugin(import_spec)
def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
+ """Import a plugin with ``modname``.
+
+ If ``consider_entry_points`` is True, entry point names are also
+ considered to find a plugin.
"""
- Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point
- names are also considered to find a plugin.
- """
- # most often modname refers to builtin modules, e.g. "pytester",
+ # Most often modname refers to builtin modules, e.g. "pytester",
# "terminal" or "capture". Those plugins are registered under their
# basename for historic purposes but must be imported with the
# _pytest prefix.
).with_traceback(e.__traceback__) from e
except Skipped as e:
- from _pytest.warnings import _issue_warning_captured
-
- _issue_warning_captured(
- PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)),
- self.hook,
- stacklevel=2,
- )
+ self.skipped_plugins.append((modname, e.msg or ""))
else:
mod = sys.modules[importspec]
self.register(mod, modname)
def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
- """
- Given an iterable of file names in a source distribution, return the "names" that should
- be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should
- be added as "pytest_mock" in the assertion rewrite mechanism.
+ """Given an iterable of file names in a source distribution, return the "names" that should
+ be marked for assertion rewrite.
+
+ For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
+ the assertion rewrite mechanism.
This function has to deal with dist-info based distributions and egg based distributions
(which are still very much in use for "editable" installs).
yield package_name
if not seen_some:
- # at this point we did not find any packages or modules suitable for assertion
+ # At this point we did not find any packages or modules suitable for assertion
# rewriting, so we try again by stripping the first path component (to account for
- # "src" based source trees for example)
- # this approach lets us have the common case continue to be fast, as egg-distributions
- # are rarer
+ # "src" based source trees for example).
+ # This approach lets us have the common case continue to be fast, as egg-distributions
+ # are rarer.
new_package_files = []
for fn in package_files:
parts = fn.split("/")
return tuple(args)
+@final
class Config:
- """
- Access to configuration values, pluginmanager and plugin hooks.
+ """Access to configuration values, pluginmanager and plugin hooks.
:param PytestPluginManager pluginmanager:
:param InvocationParams invocation_params:
- Object containing the parameters regarding the ``pytest.main``
+ Object containing parameters regarding the :func:`pytest.main`
invocation.
"""
+ @final
@attr.s(frozen=True)
class InvocationParams:
- """Holds parameters passed during ``pytest.main()``
+ """Holds parameters passed during :func:`pytest.main`.
The object attributes are read-only.
"""
args = attr.ib(type=Tuple[str, ...], converter=_args_converter)
- """tuple of command-line arguments as passed to ``pytest.main()``."""
+ """The command-line arguments as passed to :func:`pytest.main`.
+
+ :type: Tuple[str, ...]
+ """
plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]])
- """list of extra plugins, might be `None`."""
+ """Extra plugins, might be `None`.
+
+ :type: Optional[Sequence[Union[str, plugin]]]
+ """
dir = attr.ib(type=Path)
- """directory where ``pytest.main()`` was invoked from."""
+ """The directory from which :func:`pytest.main` was invoked.
+
+ :type: pathlib.Path
+ """
def __init__(
self,
)
self.option = argparse.Namespace()
- """access to command line option as attributes.
+ """Access to command line option as attributes.
- :type: argparse.Namespace"""
+ :type: argparse.Namespace
+ """
self.invocation_params = invocation_params
+ """The parameters with which pytest was invoked.
+
+ :type: InvocationParams
+ """
_a = FILE_OR_DIR
self._parser = Parser(
processopt=self._processopt,
)
self.pluginmanager = pluginmanager
- """the plugin manager handles plugin registration and hook invocation.
+ """The plugin manager handles plugin registration and hook invocation.
- :type: PytestPluginManager"""
+ :type: PytestPluginManager
+ """
self.trace = self.pluginmanager.trace.root.get("config")
self.hook = self.pluginmanager.hook
@property
def invocation_dir(self) -> py.path.local:
- """Backward compatibility"""
+ """The directory from which pytest was invoked.
+
+ Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
+ which is a :class:`pathlib.Path`.
+
+ :type: py.path.local
+ """
return py.path.local(str(self.invocation_params.dir))
+ @property
+ def rootpath(self) -> Path:
+ """The path to the :ref:`rootdir <rootdir>`.
+
+ :type: pathlib.Path
+
+ .. versionadded:: 6.1
+ """
+ return self._rootpath
+
+ @property
+ def rootdir(self) -> py.path.local:
+ """The path to the :ref:`rootdir <rootdir>`.
+
+ Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
+
+ :type: py.path.local
+ """
+ return py.path.local(str(self.rootpath))
+
+ @property
+ def inipath(self) -> Optional[Path]:
+ """The path to the :ref:`configfile <configfiles>`.
+
+ :type: Optional[pathlib.Path]
+
+ .. versionadded:: 6.1
+ """
+ return self._inipath
+
+ @property
+ def inifile(self) -> Optional[py.path.local]:
+ """The path to the :ref:`configfile <configfiles>`.
+
+ Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
+
+ :type: Optional[py.path.local]
+ """
+ return py.path.local(str(self.inipath)) if self.inipath else None
+
def add_cleanup(self, func: Callable[[], None]) -> None:
- """ Add a function to be called when the config object gets out of
+ """Add a function to be called when the config object gets out of
use (usually coninciding with pytest_unconfigure)."""
self._cleanup.append(func)
sys.stderr.flush()
def cwd_relative_nodeid(self, nodeid: str) -> str:
- # nodeid's are relative to the rootpath, compute relative to cwd
- if self.invocation_dir != self.rootdir:
- fullpath = self.rootdir.join(nodeid)
- nodeid = self.invocation_dir.bestrelpath(fullpath)
+ # nodeid's are relative to the rootpath, compute relative to cwd.
+ if self.invocation_params.dir != self.rootpath:
+ fullpath = self.rootpath / nodeid
+ nodeid = bestrelpath(self.invocation_params.dir, fullpath)
return nodeid
@classmethod
def fromdictargs(cls, option_dict, args) -> "Config":
- """ constructor usable for subprocesses. """
+ """Constructor usable for subprocesses."""
config = get_config(args)
config.option.__dict__.update(option_dict)
config.parse(args, addopts=False)
ns, unknown_args = self._parser.parse_known_and_unknown_args(
args, namespace=copy.copy(self.option)
)
- self.rootdir, self.inifile, self.inicfg = determine_setup(
+ rootpath, inipath, inicfg = determine_setup(
ns.inifilename,
ns.file_or_dir + unknown_args,
rootdir_cmd_arg=ns.rootdir or None,
config=self,
)
- self._parser.extra_info["rootdir"] = self.rootdir
- self._parser.extra_info["inifile"] = self.inifile
+ self._rootpath = rootpath
+ self._inipath = inipath
+ self.inicfg = inicfg
+ self._parser.extra_info["rootdir"] = str(self.rootpath)
+ self._parser.extra_info["inifile"] = str(self.inipath)
self._parser.addini("addopts", "extra command line options", "args")
self._parser.addini("minversion", "minimally required pytest version")
self._parser.addini(
self._warn_about_missing_assertion(mode)
def _mark_plugins_for_rewrite(self, hook) -> None:
- """
- Given an importhook, mark for rewrite any top-level
+ """Given an importhook, mark for rewrite any top-level
modules or packages in the distribution package for
- all pytest plugins.
- """
+ all pytest plugins."""
self.pluginmanager.rewrite_hook = hook
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
self._validate_args(self.getini("addopts"), "via addopts config") + args
)
+ self.known_args_namespace = self._parser.parse_known_args(
+ args, namespace=copy.copy(self.option)
+ )
self._checkversion()
self._consider_importhook(args)
self.pluginmanager.consider_preparse(args, exclude_only=False)
# plugins are going to be loaded.
self.pluginmanager.load_setuptools_entrypoints("pytest11")
self.pluginmanager.consider_env()
- self.known_args_namespace = ns = self._parser.parse_known_args(
- args, namespace=copy.copy(self.option)
+
+ self.known_args_namespace = self._parser.parse_known_args(
+ args, namespace=copy.copy(self.known_args_namespace)
)
+
self._validate_plugins()
- if self.known_args_namespace.confcutdir is None and self.inifile:
- confcutdir = py.path.local(self.inifile).dirname
+ self._warn_about_skipped_plugins()
+
+ if self.known_args_namespace.confcutdir is None and self.inipath is not None:
+ confcutdir = str(self.inipath.parent)
self.known_args_namespace.confcutdir = confcutdir
try:
self.hook.pytest_load_initial_conftests(
early_config=self, args=args, parser=self._parser
)
except ConftestImportFailure as e:
- if ns.help or ns.version:
+ if self.known_args_namespace.help or self.known_args_namespace.version:
# we don't want to prevent --help/--version to work
# so just let is pass and print a warning at the end
- from _pytest.warnings import _issue_warning_captured
-
- _issue_warning_captured(
+ self.issue_config_time_warning(
PytestConfigWarning(
"could not load initial conftests: {}".format(e.path)
),
- self.hook,
stacklevel=2,
)
else:
raise
- self._validate_keys()
+
+ @hookimpl(hookwrapper=True)
+ def pytest_collection(self) -> Generator[None, None, None]:
+ """Validate invalid ini keys after collection is done so we take in account
+ options added by late-loading conftest files."""
+ yield
+ self._validate_config_options()
def _checkversion(self) -> None:
import pytest
if not isinstance(minver, str):
raise pytest.UsageError(
- "%s: 'minversion' must be a single value" % self.inifile
+ "%s: 'minversion' must be a single value" % self.inipath
)
if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError(
"%s: 'minversion' requires pytest-%s, actual pytest-%s'"
- % (self.inifile, minver, pytest.__version__,)
+ % (self.inipath, minver, pytest.__version__,)
)
- def _validate_keys(self) -> None:
+ def _validate_config_options(self) -> None:
for key in sorted(self._get_unknown_ini_keys()):
- self._warn_or_fail_if_strict("Unknown config ini key: {}\n".format(key))
+ self._warn_or_fail_if_strict("Unknown config option: {}\n".format(key))
def _validate_plugins(self) -> None:
required_plugins = sorted(self.getini("required_plugins"))
missing_plugins = []
for required_plugin in required_plugins:
- spec = None
try:
spec = Requirement(required_plugin)
except InvalidRequirement:
missing_plugins.append(required_plugin)
if missing_plugins:
- fail(
+ raise UsageError(
"Missing required plugins: {}".format(", ".join(missing_plugins)),
- pytrace=False,
)
def _warn_or_fail_if_strict(self, message: str) -> None:
if self.known_args_namespace.strict_config:
- fail(message, pytrace=False)
+ raise UsageError(message)
- from _pytest.warnings import _issue_warning_captured
-
- _issue_warning_captured(
- PytestConfigWarning(message), self.hook, stacklevel=3,
- )
+ self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
def _get_unknown_ini_keys(self) -> List[str]:
parser_inicfg = self._parser._inidict
return [name for name in self.inicfg if name not in parser_inicfg]
def parse(self, args: List[str], addopts: bool = True) -> None:
- # parse given cmdline arguments into this config object.
+ # Parse given cmdline arguments into this config object.
assert not hasattr(
self, "args"
), "can only parse cmdline args at most once per Config object"
args, self.option, namespace=self.option
)
if not args:
- if self.invocation_dir == self.rootdir:
+ if self.invocation_params.dir == self.rootpath:
args = self.getini("testpaths")
if not args:
- args = [str(self.invocation_dir)]
+ args = [str(self.invocation_params.dir)]
self.args = args
except PrintHelp:
pass
+ def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
+ """Issue and handle a warning during the "configure" stage.
+
+ During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
+ function because it is not possible to have hookwrappers around ``pytest_configure``.
+
+ This function is mainly intended for plugins that need to issue warnings during
+ ``pytest_configure`` (or similar stages).
+
+ :param warning: The warning instance.
+ :param stacklevel: stacklevel forwarded to warnings.warn.
+ """
+ if self.pluginmanager.is_blocked("warnings"):
+ return
+
+ cmdline_filters = self.known_args_namespace.pythonwarnings or []
+ config_filters = self.getini("filterwarnings")
+
+ with warnings.catch_warnings(record=True) as records:
+ warnings.simplefilter("always", type(warning))
+ apply_warning_filters(config_filters, cmdline_filters)
+ warnings.warn(warning, stacklevel=stacklevel)
+
+ if records:
+ frame = sys._getframe(stacklevel - 1)
+ location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
+ self.hook.pytest_warning_captured.call_historic(
+ kwargs=dict(
+ warning_message=records[0],
+ when="config",
+ item=None,
+ location=location,
+ )
+ )
+ self.hook.pytest_warning_recorded.call_historic(
+ kwargs=dict(
+ warning_message=records[0],
+ when="config",
+ nodeid="",
+ location=location,
+ )
+ )
+
def addinivalue_line(self, name: str, line: str) -> None:
- """ add a line to an ini-file option. The option must have been
- declared but might not yet be set in which case the line becomes the
- the first line in its value. """
+ """Add a line to an ini-file option. The option must have been
+ declared but might not yet be set in which case the line becomes
+ the first line in its value."""
x = self.getini(name)
assert isinstance(x, list)
x.append(line) # modifies the cached list inline
def getini(self, name: str):
- """ return configuration value from an :ref:`ini file <configfiles>`. If the
- specified name hasn't been registered through a prior
+ """Return configuration value from an :ref:`ini file <configfiles>`.
+
+ If the specified name hasn't been registered through a prior
:py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>`
- call (usually from a plugin), a ValueError is raised. """
+ call (usually from a plugin), a ValueError is raised.
+ """
try:
return self._inicache[name]
except KeyError:
return []
else:
value = override_value
- # coerce the values based on types
- # note: some coercions are only required if we are reading from .ini files, because
+ # Coerce the values based on types.
+ #
+ # Note: some coercions are only required if we are reading from .ini files, because
# the file format doesn't contain type information, but when reading from toml we will
# get either str or list of str values (see _parse_ini_config_from_pyproject_toml).
- # for example:
+ # For example:
#
# ini:
# a_line_list = "tests acceptance"
- # in this case, we need to split the string to obtain a list of strings
+ # in this case, we need to split the string to obtain a list of strings.
#
# toml:
# a_line_list = ["tests", "acceptance"]
- # in this case, we already have a list ready to use
+ # in this case, we already have a list ready to use.
#
if type == "pathlist":
# TODO: This assert is probably not valid in all cases.
- assert self.inifile is not None
- dp = py.path.local(self.inifile).dirpath()
+ assert self.inipath is not None
+ dp = self.inipath.parent
input_values = shlex.split(value) if isinstance(value, str) else value
- return [dp.join(x, abs=True) for x in input_values]
+ return [py.path.local(str(dp / x)) for x in input_values]
elif type == "args":
return shlex.split(value) if isinstance(value, str) else value
elif type == "linelist":
values = [] # type: List[py.path.local]
for relroot in relroots:
if not isinstance(relroot, py.path.local):
- relroot = relroot.replace("/", py.path.local.sep)
+ relroot = relroot.replace("/", os.sep)
relroot = modpath.join(relroot, abs=True)
values.append(relroot)
return values
def _get_override_ini_value(self, name: str) -> Optional[str]:
value = None
- # override_ini is a list of "ini=value" options
- # always use the last item if multiple values are set for same ini-name,
- # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
+ # override_ini is a list of "ini=value" options.
+ # Always use the last item if multiple values are set for same ini-name,
+ # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
for ini_config in self._override_ini:
try:
key, user_ini_value = ini_config.split("=", 1)
return value
def getoption(self, name: str, default=notset, skip: bool = False):
- """ return command line option value.
+ """Return command line option value.
- :arg name: name of the option. You may also specify
+ :param name: Name of the option. You may also specify
the literal ``--OPT`` option instead of the "dest" option name.
- :arg default: default value if no option of that name exists.
- :arg skip: if True raise pytest.skip if option does not exists
+ :param default: Default value if no option of that name exists.
+ :param skip: If True, raise pytest.skip if option does not exists
or has a None value.
"""
name = self._opt2dest.get(name, name)
raise ValueError("no option named {!r}".format(name)) from e
def getvalue(self, name: str, path=None):
- """ (deprecated, use getoption()) """
+ """Deprecated, use getoption() instead."""
return self.getoption(name)
def getvalueorskip(self, name: str, path=None):
- """ (deprecated, use getoption(skip=True)) """
+ """Deprecated, use getoption(skip=True) instead."""
return self.getoption(name, skip=True)
def _warn_about_missing_assertion(self, mode: str) -> None:
if not _assertion_supported():
- from _pytest.warnings import _issue_warning_captured
-
if mode == "plain":
warning_text = (
"ASSERTIONS ARE NOT EXECUTED"
"by the underlying Python interpreter "
"(are you using python -O?)\n"
)
- _issue_warning_captured(
- PytestConfigWarning(warning_text), self.hook, stacklevel=3,
+ self.issue_config_time_warning(
+ PytestConfigWarning(warning_text), stacklevel=3,
+ )
+
+ def _warn_about_skipped_plugins(self) -> None:
+ for module_name, msg in self.pluginmanager.skipped_plugins:
+ self.issue_config_time_warning(
+ PytestConfigWarning("skipped plugin {!r}: {}".format(module_name, msg)),
+ stacklevel=2,
)
except AssertionError:
return True
else:
- return False
+ return False # type: ignore[unreachable]
def create_terminal_writer(
config: Config, file: Optional[TextIO] = None
) -> TerminalWriter:
"""Create a TerminalWriter instance configured according to the options
- in the config object. Every code which requires a TerminalWriter object
- and has access to a config object should use this function.
+ in the config object.
+
+ Every code which requires a TerminalWriter object and has access to a
+ config object should use this function.
"""
tw = TerminalWriter(file=file)
+
if config.option.color == "yes":
tw.hasmarkup = True
elif config.option.color == "no":
tw.code_highlight = True
elif config.option.code_highlight == "no":
tw.code_highlight = False
+
return tw
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
'val' is anything else.
- .. note:: copied from distutils.util
+ .. note:: Copied from distutils.util.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return False
else:
raise ValueError("invalid truth value {!r}".format(val))
+
+
+@lru_cache(maxsize=50)
+def parse_warning_filter(
+ arg: str, *, escape: bool
+) -> "Tuple[str, str, Type[Warning], str, int]":
+ """Parse a warnings filter string.
+
+ This is copied from warnings._setoption, but does not apply the filter,
+ only parses it, and makes the escaping optional.
+ """
+ parts = arg.split(":")
+ if len(parts) > 5:
+ raise warnings._OptionError("too many fields (max 5): {!r}".format(arg))
+ while len(parts) < 5:
+ parts.append("")
+ action_, message, category_, module, lineno_ = [s.strip() for s in parts]
+ action = warnings._getaction(action_) # type: str # type: ignore[attr-defined]
+ category = warnings._getcategory(
+ category_
+ ) # type: Type[Warning] # type: ignore[attr-defined]
+ if message and escape:
+ message = re.escape(message)
+ if module and escape:
+ module = re.escape(module) + r"\Z"
+ if lineno_:
+ try:
+ lineno = int(lineno_)
+ if lineno < 0:
+ raise ValueError
+ except (ValueError, OverflowError) as e:
+ raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e
+ else:
+ lineno = 0
+ return action, message, category, module, lineno
+
+
+def apply_warning_filters(
+ config_filters: Iterable[str], cmdline_filters: Iterable[str]
+) -> None:
+ """Applies pytest-configured filters to the warnings module"""
+ # Filters should have this precedence: cmdline options, config.
+ # Filters should be applied in the inverse order of precedence.
+ for arg in config_filters:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
+
+ for arg in cmdline_filters:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
import py
import _pytest._io
+from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config.exceptions import UsageError
FILE_OR_DIR = "file_or_dir"
+@final
class Parser:
- """ Parser for command line arguments and ini-file values.
+ """Parser for command line arguments and ini-file values.
- :ivar extra_info: dict of generic param -> value to display in case
+ :ivar extra_info: Dict of generic param -> value to display in case
there's an error processing the command line arguments.
"""
def getgroup(
self, name: str, description: str = "", after: Optional[str] = None
) -> "OptionGroup":
- """ get (or create) a named option Group.
+ """Get (or create) a named option Group.
- :name: name of the option group.
- :description: long description for --help output.
- :after: name of other group, used for ordering --help output.
+ :name: Name of the option group.
+ :description: Long description for --help output.
+ :after: Name of another group, used for ordering --help output.
The returned group object has an ``addoption`` method with the same
signature as :py:func:`parser.addoption
return group
def addoption(self, *opts: str, **attrs: Any) -> None:
- """ register a command line option.
+ """Register a command line option.
- :opts: option names, can be short or long options.
- :attrs: same attributes which the ``add_argument()`` function of the
- `argparse library
- <https://docs.python.org/library/argparse.html>`_
+ :opts: Option names, can be short or long options.
+ :attrs: Same attributes which the ``add_argument()`` function of the
+ `argparse library <https://docs.python.org/library/argparse.html>`_
accepts.
- After command line parsing options are available on the pytest config
+ After command line parsing, options are available on the pytest config
object via ``config.option.NAME`` where ``NAME`` is usually set
by passing a ``dest`` attribute, for example
``addoption("--long", dest="NAME", ...)``.
args: Sequence[Union[str, py.path.local]],
namespace: Optional[argparse.Namespace] = None,
) -> argparse.Namespace:
- """parses and returns a namespace object with known arguments at this
- point.
- """
+ """Parse and return a namespace object with known arguments at this point."""
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
def parse_known_and_unknown_args(
args: Sequence[Union[str, py.path.local]],
namespace: Optional[argparse.Namespace] = None,
) -> Tuple[argparse.Namespace, List[str]]:
- """parses and returns a namespace object with known arguments, and
- the remaining arguments unknown at this point.
- """
+ """Parse and return a namespace object with known arguments, and
+ the remaining arguments unknown at this point."""
optparser = self._getparser()
strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
return optparser.parse_known_args(strargs, namespace=namespace)
type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
default=None,
) -> None:
- """ register an ini-file option.
+ """Register an ini-file option.
- :name: name of the ini-variable
- :type: type of the variable, can be ``pathlist``, ``args``, ``linelist``
+ :name: Name of the ini-variable.
+ :type: Type of the variable, can be ``pathlist``, ``args``, ``linelist``
or ``bool``.
- :default: default value if no ini-file option exists but is queried.
+ :default: Default value if no ini-file option exists but is queried.
The value of ini-variables can be retrieved via a call to
:py:func:`config.getini(name) <_pytest.config.Config.getini>`.
class ArgumentError(Exception):
- """
- Raised if an Argument instance is created with invalid or
- inconsistent arguments.
- """
+ """Raised if an Argument instance is created with invalid or
+ inconsistent arguments."""
def __init__(self, msg: str, option: Union["Argument", str]) -> None:
self.msg = msg
class Argument:
- """class that mimics the necessary behaviour of optparse.Option
+ """Class that mimics the necessary behaviour of optparse.Option.
+
+ It's currently a least effort implementation and ignoring choices
+ and integer prefixes.
- it's currently a least effort implementation
- and ignoring choices and integer prefixes
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
"""
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
def __init__(self, *names: str, **attrs: Any) -> None:
- """store parms in private vars for use in add_argument"""
+ """Store parms in private vars for use in add_argument."""
self._attrs = attrs
self._short_opts = [] # type: List[str]
self._long_opts = [] # type: List[str]
except KeyError:
pass
else:
- # this might raise a keyerror as well, don't want to catch that
+ # This might raise a keyerror as well, don't want to catch that.
if isinstance(typ, str):
if typ == "choice":
warnings.warn(
stacklevel=4,
)
attrs["type"] = Argument._typ_map[typ]
- # used in test_parseopt -> test_parse_defaultgetter
+ # Used in test_parseopt -> test_parse_defaultgetter.
self.type = attrs["type"]
else:
self.type = typ
try:
- # attribute existence is tested in Config._processopt
+ # Attribute existence is tested in Config._processopt.
self.default = attrs["default"]
except KeyError:
pass
return self._short_opts + self._long_opts
def attrs(self) -> Mapping[str, Any]:
- # update any attributes set by processopt
+ # Update any attributes set by processopt.
attrs = "default dest help".split()
attrs.append(self.dest)
for attr in attrs:
return self._attrs
def _set_opt_strings(self, opts: Sequence[str]) -> None:
- """directly from optparse
+ """Directly from optparse.
- might not be necessary as this is passed to argparse later on"""
+ Might not be necessary as this is passed to argparse later on.
+ """
for opt in opts:
if len(opt) < 2:
raise ArgumentError(
self.parser = parser
def addoption(self, *optnames: str, **attrs: Any) -> None:
- """ add an option to this group.
+ """Add an option to this group.
- if a shortened version of a long option is specified it will
+ If a shortened version of a long option is specified, it will
be suppressed in the help. addoption('--twowords', '--two-words')
results in help showing '--two-words' only, but --twowords gets
- accepted **and** the automatic destination is in args.twowords
+ accepted **and** the automatic destination is in args.twowords.
"""
conflict = set(optnames).intersection(
name for opt in self.options for name in opt.names()
allow_abbrev=False,
)
# extra_info is a dict of (param -> value) to display if there's
- # an usage error to provide more contextual information to the user
+ # an usage error to provide more contextual information to the user.
self.extra_info = extra_info if extra_info else {}
def error(self, message: str) -> "NoReturn":
args: Optional[Sequence[str]] = None,
namespace: Optional[argparse.Namespace] = None,
) -> argparse.Namespace:
- """allow splitting of positional arguments"""
+ """Allow splitting of positional arguments."""
parsed, unrecognized = self.parse_known_args(args, namespace)
if unrecognized:
for arg in unrecognized:
class DropShorterLongHelpFormatter(argparse.HelpFormatter):
- """shorten help for long options that differ only in extra hyphens
+ """Shorten help for long options that differ only in extra hyphens.
- - collapse **long** options that are the same except for extra hyphens
- - shortcut if there are only two options and one of them is a short one
- - cache result on action object as this is called at least 2 times
+ - Collapse **long** options that are the same except for extra hyphens.
+ - Shortcut if there are only two options and one of them is a short one.
+ - Cache result on the action object as this is called at least 2 times.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
- """Use more accurate terminal width via pylib."""
+ # Use more accurate terminal width.
if "width" not in kwargs:
kwargs["width"] = _pytest._io.get_terminal_width()
super().__init__(*args, **kwargs)
+from _pytest.compat import final
+
+
+@final
class UsageError(Exception):
- """ error in pytest usage or invocation"""
+ """Error in pytest usage or invocation."""
class PrintHelp(Exception):
- """Raised when pytest should print it's help to skip the rest of the
+ """Raised when pytest should print its help to skip the rest of the
argument parsing and validation."""
-
- pass
+import itertools
import os
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
+from typing import Sequence
from typing import Tuple
from typing import Union
import iniconfig
-import py
from .exceptions import UsageError
from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import commonpath
+from _pytest.pathlib import Path
if TYPE_CHECKING:
from . import Config
-def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
- """Parses the given generic '.ini' file using legacy IniConfig parser, returning
+def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
+ """Parse the given generic '.ini' file using legacy IniConfig parser, returning
the parsed object.
- Raises UsageError if the file cannot be parsed.
+ Raise UsageError if the file cannot be parsed.
"""
try:
return iniconfig.IniConfig(path)
def load_config_dict_from_file(
- filepath: py.path.local,
+ filepath: Path,
) -> Optional[Dict[str, Union[str, List[str]]]]:
- """Loads pytest configuration from the given file path, if supported.
+ """Load pytest configuration from the given file path, if supported.
Return None if the file does not contain valid pytest configuration.
"""
- # configuration from ini files are obtained from the [pytest] section, if present.
- if filepath.ext == ".ini":
+ # Configuration from ini files are obtained from the [pytest] section, if present.
+ if filepath.suffix == ".ini":
iniconfig = _parse_ini_config(filepath)
if "pytest" in iniconfig:
return dict(iniconfig["pytest"].items())
else:
- # "pytest.ini" files are always the source of configuration, even if empty
- if filepath.basename == "pytest.ini":
+ # "pytest.ini" files are always the source of configuration, even if empty.
+ if filepath.name == "pytest.ini":
return {}
- # '.cfg' files are considered if they contain a "[tool:pytest]" section
- elif filepath.ext == ".cfg":
+ # '.cfg' files are considered if they contain a "[tool:pytest]" section.
+ elif filepath.suffix == ".cfg":
iniconfig = _parse_ini_config(filepath)
if "tool:pytest" in iniconfig.sections:
# plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
- # '.toml' files are considered if they contain a [tool.pytest.ini_options] table
- elif filepath.ext == ".toml":
+ # '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
+ elif filepath.suffix == ".toml":
import toml
config = toml.load(str(filepath))
def locate_config(
- args: Iterable[Union[str, py.path.local]]
+ args: Iterable[Path],
) -> Tuple[
- Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]],
+ Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]],
]:
- """
- Search in the list of arguments for a valid ini-file for pytest,
- and return a tuple of (rootdir, inifile, cfg-dict).
- """
+ """Search in the list of arguments for a valid ini-file for pytest,
+ and return a tuple of (rootdir, inifile, cfg-dict)."""
config_names = [
"pytest.ini",
"pyproject.toml",
]
args = [x for x in args if not str(x).startswith("-")]
if not args:
- args = [py.path.local()]
+ args = [Path.cwd()]
for arg in args:
- arg = py.path.local(arg)
- for base in arg.parts(reverse=True):
+ argpath = absolutepath(arg)
+ for base in itertools.chain((argpath,), reversed(argpath.parents)):
for config_name in config_names:
- p = base.join(config_name)
- if p.isfile():
+ p = base / config_name
+ if p.is_file():
ini_config = load_config_dict_from_file(p)
if ini_config is not None:
return base, p, ini_config
return None, None, {}
-def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local:
- common_ancestor = None
+def get_common_ancestor(paths: Iterable[Path]) -> Path:
+ common_ancestor = None # type: Optional[Path]
for path in paths:
if not path.exists():
continue
if common_ancestor is None:
common_ancestor = path
else:
- if path.relto(common_ancestor) or path == common_ancestor:
+ if common_ancestor in path.parents or path == common_ancestor:
continue
- elif common_ancestor.relto(path):
+ elif path in common_ancestor.parents:
common_ancestor = path
else:
- shared = path.common(common_ancestor)
+ shared = commonpath(path, common_ancestor)
if shared is not None:
common_ancestor = shared
if common_ancestor is None:
- common_ancestor = py.path.local()
- elif common_ancestor.isfile():
- common_ancestor = common_ancestor.dirpath()
+ common_ancestor = Path.cwd()
+ elif common_ancestor.is_file():
+ common_ancestor = common_ancestor.parent
return common_ancestor
-def get_dirs_from_args(args: Iterable[str]) -> List[py.path.local]:
+def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
def is_option(x: str) -> bool:
return x.startswith("-")
def get_file_part_from_node_id(x: str) -> str:
return x.split("::")[0]
- def get_dir_from_path(path: py.path.local) -> py.path.local:
- if path.isdir():
+ def get_dir_from_path(path: Path) -> Path:
+ if path.is_dir():
return path
- return py.path.local(path.dirname)
+ return path.parent
+
+ def safe_exists(path: Path) -> bool:
+ # This can throw on paths that contain characters unrepresentable at the OS level,
+ # or with invalid syntax on Windows (https://bugs.python.org/issue35306)
+ try:
+ return path.exists()
+ except OSError:
+ return False
# These look like paths but may not exist
possible_paths = (
- py.path.local(get_file_part_from_node_id(arg))
+ absolutepath(get_file_part_from_node_id(arg))
for arg in args
if not is_option(arg)
)
- return [get_dir_from_path(path) for path in possible_paths if path.exists()]
+ return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
def determine_setup(
inifile: Optional[str],
- args: List[str],
+ args: Sequence[str],
rootdir_cmd_arg: Optional[str] = None,
config: Optional["Config"] = None,
-) -> Tuple[py.path.local, Optional[py.path.local], Dict[str, Union[str, List[str]]]]:
+) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
rootdir = None
dirs = get_dirs_from_args(args)
if inifile:
- inipath_ = py.path.local(inifile)
- inipath = inipath_ # type: Optional[py.path.local]
+ inipath_ = absolutepath(inifile)
+ inipath = inipath_ # type: Optional[Path]
inicfg = load_config_dict_from_file(inipath_) or {}
if rootdir_cmd_arg is None:
rootdir = get_common_ancestor(dirs)
ancestor = get_common_ancestor(dirs)
rootdir, inipath, inicfg = locate_config([ancestor])
if rootdir is None and rootdir_cmd_arg is None:
- for possible_rootdir in ancestor.parts(reverse=True):
- if possible_rootdir.join("setup.py").exists():
+ for possible_rootdir in itertools.chain(
+ (ancestor,), reversed(ancestor.parents)
+ ):
+ if (possible_rootdir / "setup.py").is_file():
rootdir = possible_rootdir
break
else:
rootdir, inipath, inicfg = locate_config(dirs)
if rootdir is None:
if config is not None:
- cwd = config.invocation_dir
+ cwd = config.invocation_params.dir
else:
- cwd = py.path.local()
+ cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
if is_fs_root:
rootdir = ancestor
if rootdir_cmd_arg:
- rootdir = py.path.local(os.path.expandvars(rootdir_cmd_arg))
- if not rootdir.isdir():
+ rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
+ if not rootdir.is_dir():
raise UsageError(
"Directory '{}' not found. Check your '--rootdir' option.".format(
rootdir
-""" interactive debugging with PDB, the Python Debugger. """
+"""Interactive debugging with PDB, the Python Debugger."""
import argparse
import functools
import sys
import types
+from typing import Any
+from typing import Callable
from typing import Generator
+from typing import List
+from typing import Optional
from typing import Tuple
from typing import Union
from _pytest.reports import BaseReport
if TYPE_CHECKING:
+ from typing import Type
+
from _pytest.capture import CaptureManager
from _pytest.runner import CallInfo
class pytestPDB:
- """ Pseudo PDB that defers to the real pdb. """
+ """Pseudo PDB that defers to the real pdb."""
- _pluginmanager = None # type: PytestPluginManager
+ _pluginmanager = None # type: Optional[PytestPluginManager]
_config = None # type: Config
- _saved = [] # type: list
+ _saved = (
+ []
+ ) # type: List[Tuple[Callable[..., None], Optional[PytestPluginManager], Config]]
_recursive_debug = 0
- _wrapped_pdb_cls = None
+ _wrapped_pdb_cls = None # type: Optional[Tuple[Type[Any], Type[Any]]]
@classmethod
- def _is_capturing(cls, capman: "CaptureManager") -> Union[str, bool]:
+ def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
if capman:
return capman.is_capturing()
return False
@classmethod
- def _import_pdb_cls(cls, capman: "CaptureManager"):
+ def _import_pdb_cls(cls, capman: Optional["CaptureManager"]):
if not cls._config:
import pdb
return wrapped_cls
@classmethod
- def _get_pdb_wrapper_class(cls, pdb_cls, capman: "CaptureManager"):
+ def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
import _pytest.config
# Type ignored because mypy doesn't support "dynamic"
"PDB continue (IO-capturing resumed for %s)"
% capturing,
)
+ assert capman is not None
capman.resume()
else:
tw.sep(">", "PDB continue")
+ assert cls._pluginmanager is not None
cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
self._continued = True
return ret
@classmethod
def _init_pdb(cls, method, *args, **kwargs):
- """ Initialize PDB debugging, dropping any IO capturing. """
+ """Initialize PDB debugging, dropping any IO capturing."""
import _pytest.config
- if cls._pluginmanager is not None:
- capman = cls._pluginmanager.getplugin("capturemanager")
+ if cls._pluginmanager is None:
+ capman = None # type: Optional[CaptureManager]
else:
- capman = None
+ capman = cls._pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend(in_=True)
class PdbInvoke:
def pytest_exception_interact(
- self, node: Node, call: "CallInfo", report: BaseReport
+ self, node: Node, call: "CallInfo[Any]", report: BaseReport
) -> None:
capman = node.config.pluginmanager.getplugin("capturemanager")
if capman:
def wrap_pytest_function_for_tracing(pyfuncitem):
- """Changes the python function object of the given Function item by a wrapper which actually
- enters pdb before calling the python function itself, effectively leaving the user
- in the pdb prompt in the first statement of the function.
- """
+ """Change the Python function object of the given Function item by a
+ wrapper which actually enters pdb before calling the python function
+ itself, effectively leaving the user in the pdb prompt in the first
+ statement of the function."""
_pdb = pytestPDB._init_pdb("runcall")
testfunction = pyfuncitem.obj
# we can't just return `partial(pdb.runcall, testfunction)` because (on
# python < 3.7.4) runcall's first param is `func`, which means we'd get
- # an exception if one of the kwargs to testfunction was called `func`
+ # an exception if one of the kwargs to testfunction was called `func`.
@functools.wraps(testfunction)
def wrapper(*args, **kwargs):
func = functools.partial(testfunction, *args, **kwargs)
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
"""Wrap the given pytestfunct item for tracing support if --trace was given in
- the command line"""
+ the command line."""
if pyfuncitem.config.getvalue("trace"):
wrap_pytest_function_for_tracing(pyfuncitem)
-"""
-This module contains deprecation messages and bits of code used elsewhere in the codebase
-that is planned to be removed in the next pytest release.
+"""Deprecation messages and bits of code used elsewhere in the codebase that
+is planned to be removed in the next pytest release.
Keeping it in a central location makes it easy to track what is deprecated and should
be removed when the time comes.
"pytest_faulthandler",
}
-FUNCARGNAMES = PytestDeprecationWarning(
- "The `funcargnames` attribute was an alias for `fixturenames`, "
- "since pytest 2.3 - use the newer attribute instead."
-)
FILLFUNCARGS = PytestDeprecationWarning(
"The `_fillfuncargs` function is deprecated, use "
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
)
-RESULT_LOG = PytestDeprecationWarning(
- "--result-log is deprecated, please try the new pytest-reportlog plugin.\n"
- "See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information."
-)
-
-FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning(
- "Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them "
- "as a keyword argument instead."
-)
-
-NODE_USE_FROM_PARENT = UnformattedWarning(
- PytestDeprecationWarning,
- "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
- "See "
- "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
- " for more details.",
-)
-
-JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning(
- "The 'junit_family' default value will change to 'xunit2' in pytest 6.0. See:\n"
- " https://docs.pytest.org/en/stable/deprecations.html#junit-family-default-value-change-to-xunit2\n"
- "for more information."
-)
-
-COLLECT_DIRECTORY_HOOK = PytestDeprecationWarning(
- "The pytest_collect_directory hook is not working.\n"
- "Please use collect_ignore in conftests or pytest_collection_modifyitems."
-)
-
PYTEST_COLLECT_MODULE = UnformattedWarning(
PytestDeprecationWarning,
"pytest.collect.{name} was moved to pytest.{name}\n"
)
-TERMINALWRITER_WRITER = PytestDeprecationWarning(
- "The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n"
- "See https://docs.pytest.org/en/stable/deprecations.html#terminalreporter-writer for more information."
-)
-
-
MINUS_K_DASH = PytestDeprecationWarning(
"The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead."
)
"The pytest_warning_captured is deprecated and will be removed in a future release.\n"
"Please use pytest_warning_recorded instead."
)
+
+FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestDeprecationWarning(
+ "The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
+ "use self.session.gethookproxy() and self.session.isinitpath() instead. "
+)
-""" discover and run doctests in modules and test files."""
+"""Discover and run doctests in modules and test files."""
import bdb
import inspect
import platform
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureRequest
+from _pytest.nodes import Collector
from _pytest.outcomes import OutcomeException
from _pytest.pathlib import import_path
from _pytest.python_api import approx
def pytest_collect_file(
- path: py.path.local, parent
+ path: py.path.local, parent: Collector,
) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
config = parent.config
if path.ext == ".py":
import doctest
class PytestDoctestRunner(doctest.DebugRunner):
- """
- Runner to collect failures. Note that the out variable in this case is
- a list instead of a stdout-like object
+ """Runner to collect failures.
+
+ Note that the out variable in this case is a list instead of a
+ stdout-like object.
"""
def __init__(
dtest: "doctest.DocTest"
):
# incompatible signature due to to imposed limits on sublcass
- """
- the public named constructor
- """
+ """The public named constructor."""
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
def setup(self) -> None:
raise MultipleDoctestFailures(failures)
def _disable_output_capturing_for_darwin(self) -> None:
- """
- Disable output capturing. Otherwise, stdout is lost to doctest (#985)
- """
+ """Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
if platform.system() != "Darwin":
return
capman = self.config.pluginmanager.getplugin("capturemanager")
continue_on_failure = config.getvalue("doctest_continue_on_failure")
if continue_on_failure:
# We need to turn off this if we use pdb since we should stop at
- # the first failure
+ # the first failure.
if config.getvalue("usepdb"):
continue_on_failure = False
return continue_on_failure
def collect(self) -> Iterable[DoctestItem]:
import doctest
- # inspired by doctest.testfile; ideally we would use it directly,
- # but it doesn't support passing a custom checker
+ # Inspired by doctest.testfile; ideally we would use it directly,
+ # but it doesn't support passing a custom checker.
encoding = self.config.getini("doctest_encoding")
text = self.fspath.read_text(encoding)
filename = str(self.fspath)
def _check_all_skipped(test: "doctest.DocTest") -> None:
- """raises pytest.skip() if all examples in the given DocTest have the SKIP
- option set.
- """
+ """Raise pytest.skip() if all examples in the given DocTest have the SKIP
+ option set."""
import doctest
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
def _is_mocked(obj: object) -> bool:
- """
- returns if a object is possibly a mock object by checking the existence of a highly improbable attribute
- """
+ """Return if an object is possibly a mock object by checking the
+ existence of a highly improbable attribute."""
return (
safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
is not None
@contextmanager
def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
- """
- contextmanager which replaces ``inspect.unwrap`` with a version
- that's aware of mock objects and doesn't recurse on them
- """
+ """Context manager which replaces ``inspect.unwrap`` with a version
+ that's aware of mock objects and doesn't recurse into them."""
real_unwrap = inspect.unwrap
def _mock_aware_unwrap(
import doctest
class MockAwareDocTestFinder(doctest.DocTestFinder):
- """
- a hackish doctest finder that overrides stdlib internals to fix a stdlib bug
+ """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
https://github.com/pytest-dev/pytest/issues/3456
https://bugs.python.org/issue25532
"""
def _find_lineno(self, obj, source_lines):
- """
- Doctest code does not take into account `@property`, this is a hackish way to fix it.
+ """Doctest code does not take into account `@property`, this
+ is a hackish way to fix it.
https://bugs.python.org/issue17446
"""
pytest.skip("unable to import module %r" % self.fspath)
else:
raise
- # uses internal doctest module parsing mechanism
+ # Uses internal doctest module parsing mechanism.
finder = MockAwareDocTestFinder()
optionflags = get_optionflags(self)
runner = _get_runner(
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
- """
- Used by DoctestTextfile and DoctestItem to setup fixture information.
- """
+ """Used by DoctestTextfile and DoctestItem to setup fixture information."""
def func() -> None:
pass
import re
class LiteralsOutputChecker(doctest.OutputChecker):
- """
- Based on doctest_nose_plugin.py from the nltk project
- (https://github.com/nltk/nltk) and on the "numtest" doctest extension
- by Sebastien Boisgerault (https://github.com/boisgera/numtest).
- """
+ # Based on doctest_nose_plugin.py from the nltk project
+ # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
+ # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
_unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
_bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
return got
offset = 0
for w, g in zip(wants, gots):
- fraction = w.group("fraction")
- exponent = w.group("exponent1")
+ fraction = w.group("fraction") # type: Optional[str]
+ exponent = w.group("exponent1") # type: Optional[str]
if exponent is None:
exponent = w.group("exponent2")
if fraction is None:
def _get_checker() -> "doctest.OutputChecker":
- """
- Returns a doctest.OutputChecker subclass that supports some
+ """Return a doctest.OutputChecker subclass that supports some
additional options:
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
def _get_allow_unicode_flag() -> int:
- """
- Registers and returns the ALLOW_UNICODE flag.
- """
+ """Register and return the ALLOW_UNICODE flag."""
import doctest
return doctest.register_optionflag("ALLOW_UNICODE")
def _get_allow_bytes_flag() -> int:
- """
- Registers and returns the ALLOW_BYTES flag.
- """
+ """Register and return the ALLOW_BYTES flag."""
import doctest
return doctest.register_optionflag("ALLOW_BYTES")
def _get_number_flag() -> int:
- """
- Registers and returns the NUMBER flag.
- """
+ """Register and return the NUMBER flag."""
import doctest
return doctest.register_optionflag("NUMBER")
def _get_report_choice(key: str) -> int:
- """
- This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
- importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
+ """Return the actual `doctest` module flag value.
+
+ We want to do it as late as possible to avoid importing `doctest` and all
+ its dependencies when parsing options, as it adds overhead and breaks tests.
"""
import doctest
@pytest.fixture(scope="session")
def doctest_namespace() -> Dict[str, Any]:
- """
- Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.
- """
+ """Fixture that returns a :py:class:`dict` that will be injected into the
+ namespace of doctests."""
return dict()
# of enabling faulthandler before each test executes.
config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks")
else:
- from _pytest.warnings import _issue_warning_captured
-
# Do not handle dumping to stderr if faulthandler is already enabled, so warn
# users that the option is being ignored.
timeout = FaultHandlerHooks.get_timeout_config_value(config)
if timeout > 0:
- _issue_warning_captured(
+ config.issue_config_time_warning(
pytest.PytestConfigWarning(
"faulthandler module enabled before pytest configuration step, "
"'faulthandler_timeout' option ignored"
),
- config.hook,
stacklevel=2,
)
@pytest.hookimpl(tryfirst=True)
def pytest_enter_pdb(self) -> None:
- """Cancel any traceback dumping due to timeout before entering pdb.
- """
+ """Cancel any traceback dumping due to timeout before entering pdb."""
import faulthandler
faulthandler.cancel_dump_traceback_later()
@pytest.hookimpl(tryfirst=True)
def pytest_exception_interact(self) -> None:
"""Cancel any traceback dumping due to an interactive exception being
- raised.
- """
+ raised."""
import faulthandler
faulthandler.cancel_dump_traceback_later()
import functools
import inspect
-import itertools
+import os
import sys
import warnings
from collections import defaultdict
from _pytest._io import TerminalWriter
from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper
+from _pytest.compat import final
from _pytest.compat import get_real_func
from _pytest.compat import get_real_method
from _pytest.compat import getfuncargnames
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config.argparsing import Parser
-from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
-from _pytest.deprecated import FUNCARGNAMES
+from _pytest.deprecated import FILLFUNCARGS
+from _pytest.mark import Mark
from _pytest.mark import ParameterSet
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
+from _pytest.pathlib import absolutepath
if TYPE_CHECKING:
from typing import Deque
@attr.s(frozen=True)
-class PseudoFixtureDef:
- cached_result = attr.ib(type="_FixtureCachedResult")
+class PseudoFixtureDef(Generic[_FixtureValue]):
+ cached_result = attr.ib(type="_FixtureCachedResult[_FixtureValue]")
scope = attr.ib(type="_Scope")
scopename2class = {} # type: Dict[str, Type[nodes.Node]]
-scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]]
-scope2props["package"] = ("fspath",)
-scope2props["module"] = ("fspath", "module")
-scope2props["class"] = scope2props["module"] + ("cls",)
-scope2props["instance"] = scope2props["class"] + ("instance",)
-scope2props["function"] = scope2props["instance"] + ("function", "keywords")
-
-def scopeproperty(name=None, doc=None):
- def decoratescope(func):
- scopename = name or func.__name__
-
- def provide(self):
- if func.__name__ in scope2props[self.scope]:
- return func(self)
- raise AttributeError(
- "{} not available in {}-scoped context".format(scopename, self.scope)
- )
-
- return property(provide, None, None, func.__doc__)
-
- return decoratescope
-
-
-def get_scope_package(node, fixturedef: "FixtureDef"):
+def get_scope_package(node, fixturedef: "FixtureDef[object]"):
import pytest
cls = pytest.Package
def add_funcarg_pseudo_fixture_def(
collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
) -> None:
- # this function will transform all collected calls to a functions
+ # This function will transform all collected calls to functions
# if they use direct funcargs (i.e. direct parametrization)
# because we want later test execution to be able to rely on
# an existing FixtureDef structure for all arguments.
# XXX we can probably avoid this algorithm if we modify CallSpec2
# to directly care for creating the fixturedefs within its methods.
if not metafunc._calls[0].funcargs:
- return # this function call does not have direct parametrization
- # collect funcargs of all callspecs into a list of values
+ # This function call does not have direct parametrization.
+ return
+ # Collect funcargs of all callspecs into a list of values.
arg2params = {} # type: Dict[str, List[object]]
arg2scope = {} # type: Dict[str, _Scope]
for callspec in metafunc._calls:
arg2scope[argname] = scopes[scopenum]
callspec.funcargs.clear()
- # register artificial FixtureDef's so that later at test execution
+ # Register artificial FixtureDef's so that later at test execution
# time we can rely on a proper FixtureDef to exist for fixture setup.
arg2fixturedefs = metafunc._arg2fixturedefs
for argname, valuelist in arg2params.items():
- # if we have a scope that is higher than function we need
+ # If we have a scope that is higher than function, we need
# to make sure we only ever create an according fixturedef on
# a per-scope basis. We thus store and cache the fixturedef on the
# node related to the scope.
node = get_scope_node(collector, scope)
if node is None:
assert scope == "class" and isinstance(collector, _pytest.python.Module)
- # use module-level collector for class-scope (for now)
+ # Use module-level collector for class-scope (for now).
node = collector
if node and argname in node._name2pseudofixturedef:
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
- """ return fixturemarker or None if it doesn't exist or raised
+ """Return fixturemarker or None if it doesn't exist or raised
exceptions."""
try:
fixturemarker = getattr(
def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]:
- """ return list of keys for all parametrized arguments which match
+ """Return list of keys for all parametrized arguments which match
the specified scope. """
assert scopenum < scopenum_function # function
try:
yield key
-# algorithm for sorting on a per-parametrized resource setup basis
-# it is called for scopenum==0 (session) first and performs sorting
+# Algorithm for sorting on a per-parametrized resource setup basis.
+# It is called for scopenum==0 (session) first and performs sorting
# down to the lower scopes such as to minimize number of "high scope"
-# setups and teardowns
+# setups and teardowns.
def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]":
no_argkey_group[item] = None
else:
slicing_argkey, _ = argkeys.popitem()
- # we don't have to remove relevant items from later in the deque because they'll just be ignored
+ # We don't have to remove relevant items from later in the
+ # deque because they'll just be ignored.
matching_items = [
i for i in scoped_items_by_argkey[slicing_argkey] if i in items
]
def fillfixtures(function: "Function") -> None:
- """ fill missing funcargs for a test function. """
- # Uncomment this after 6.0 release (#7361)
- # warnings.warn(FILLFUNCARGS, stacklevel=2)
+ """Fill missing funcargs for a test function."""
+ warnings.warn(FILLFUNCARGS, stacklevel=2)
try:
request = function._request
except AttributeError:
function._fixtureinfo = fi
request = function._request = FixtureRequest(function)
request._fillfixtures()
- # prune out funcargs for jstests
+ # Prune out funcargs for jstests.
newfuncargs = {}
for name in fi.argnames:
newfuncargs[name] = function.funcargs[name]
@attr.s(slots=True)
class FuncFixtureInfo:
- # original function argument names
+ # Original function argument names.
argnames = attr.ib(type=Tuple[str, ...])
- # argnames that function immediately requires. These include argnames +
+ # Argnames that function immediately requires. These include argnames +
# fixture names specified via usefixtures and via autouse=True in fixture
# definitions.
initialnames = attr.ib(type=Tuple[str, ...])
names_closure = attr.ib(type=List[str])
- name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef"]])
+ name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef[Any]"]])
def prune_dependency_tree(self) -> None:
- """Recompute names_closure from initialnames and name2fixturedefs
+ """Recompute names_closure from initialnames and name2fixturedefs.
Can only reduce names_closure, which means that the new closure will
always be a subset of the old one. The order is preserved.
working_set = set(self.initialnames)
while working_set:
argname = working_set.pop()
- # argname may be smth not included in the original names_closure,
+ # Argname may be smth not included in the original names_closure,
# in which case we ignore it. This currently happens with pseudo
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
# So they introduce the new dependency 'request' which might have
class FixtureRequest:
- """ A request for a fixture from a test or fixture function.
+ """A request for a fixture from a test or fixture function.
- A request object gives access to the requesting test context
- and has an optional ``param`` attribute in case
- the fixture is parametrized indirectly.
+ A request object gives access to the requesting test context and has
+ an optional ``param`` attribute in case the fixture is parametrized
+ indirectly.
"""
def __init__(self, pyfuncitem) -> None:
self._pyfuncitem = pyfuncitem
- #: fixture for which this request is being performed
+ #: Fixture for which this request is being performed.
self.fixturename = None # type: Optional[str]
- #: Scope string, one of "function", "class", "module", "session"
+ #: Scope string, one of "function", "class", "module", "session".
self.scope = "function" # type: _Scope
- self._fixture_defs = {} # type: Dict[str, FixtureDef]
+ self._fixture_defs = {} # type: Dict[str, FixtureDef[Any]]
fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
self._arg2index = {} # type: Dict[str, int]
@property
def fixturenames(self) -> List[str]:
- """names of all active fixtures in this request"""
+ """Names of all active fixtures in this request."""
result = list(self._pyfuncitem._fixtureinfo.names_closure)
result.extend(set(self._fixture_defs).difference(result))
return result
- @property
- def funcargnames(self) -> List[str]:
- """ alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
- warnings.warn(FUNCARGNAMES, stacklevel=2)
- return self.fixturenames
-
@property
def node(self):
- """ underlying collection node (depends on current request scope)"""
+ """Underlying collection node (depends on current request scope)."""
return self._getscopeitem(self.scope)
- def _getnextfixturedef(self, argname: str) -> "FixtureDef":
+ def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None)
if fixturedefs is None:
- # we arrive here because of a dynamic call to
+ # We arrive here because of a dynamic call to
# getfixturevalue(argname) usage which was naturally
- # not known at parsing/collection time
+ # not known at parsing/collection time.
assert self._pyfuncitem.parent is not None
parentid = self._pyfuncitem.parent.nodeid
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
# TODO: Fix this type ignore. Either add assert or adjust types.
# Can this be None here?
self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment]
- # fixturedefs list is immutable so we maintain a decreasing index
+ # fixturedefs list is immutable so we maintain a decreasing index.
index = self._arg2index.get(argname, 0) - 1
if fixturedefs is None or (-index > len(fixturedefs)):
raise FixtureLookupError(argname, self)
@property
def config(self) -> Config:
- """ the pytest config object associated with this request. """
+ """The pytest config object associated with this request."""
return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723
- @scopeproperty()
+ @property
def function(self):
- """ test function object if the request has a per-function scope. """
+ """Test function object if the request has a per-function scope."""
+ if self.scope != "function":
+ raise AttributeError(
+ "function not available in {}-scoped context".format(self.scope)
+ )
return self._pyfuncitem.obj
- @scopeproperty("class")
+ @property
def cls(self):
- """ class (can be None) where the test function was collected. """
+ """Class (can be None) where the test function was collected."""
+ if self.scope not in ("class", "function"):
+ raise AttributeError(
+ "cls not available in {}-scoped context".format(self.scope)
+ )
clscol = self._pyfuncitem.getparent(_pytest.python.Class)
if clscol:
return clscol.obj
@property
def instance(self):
- """ instance (can be None) on which test function was collected. """
- # unittest support hack, see _pytest.unittest.TestCaseFunction
+ """Instance (can be None) on which test function was collected."""
+ # unittest support hack, see _pytest.unittest.TestCaseFunction.
try:
return self._pyfuncitem._testcase
except AttributeError:
function = getattr(self, "function", None)
return getattr(function, "__self__", None)
- @scopeproperty()
+ @property
def module(self):
- """ python module object where the test function was collected. """
+ """Python module object where the test function was collected."""
+ if self.scope not in ("function", "class", "module"):
+ raise AttributeError(
+ "module not available in {}-scoped context".format(self.scope)
+ )
return self._pyfuncitem.getparent(_pytest.python.Module).obj
- @scopeproperty()
+ @property
def fspath(self) -> py.path.local:
- """ the file system path of the test module which collected this test. """
+ """The file system path of the test module which collected this test."""
+ if self.scope not in ("function", "class", "module", "package"):
+ raise AttributeError(
+ "module not available in {}-scoped context".format(self.scope)
+ )
# TODO: Remove ignore once _pyfuncitem is properly typed.
return self._pyfuncitem.fspath # type: ignore
@property
def keywords(self):
- """ keywords/markers dictionary for the underlying node. """
+ """Keywords/markers dictionary for the underlying node."""
return self.node.keywords
@property
def session(self):
- """ pytest session object. """
+ """Pytest session object."""
return self._pyfuncitem.session
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
- """ add finalizer/teardown function to be called after the
- last test within the requesting test context finished
- execution. """
- # XXX usually this method is shadowed by fixturedef specific ones
+ """Add finalizer/teardown function to be called after the last test
+ within the requesting test context finished execution."""
+ # XXX usually this method is shadowed by fixturedef specific ones.
self._addfinalizer(finalizer, scope=self.scope)
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
)
def applymarker(self, marker) -> None:
- """ Apply a marker to a single test function invocation.
+ """Apply a marker to a single test function invocation.
+
This method is useful if you don't want to have a keyword/marker
on all function invocations.
- :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
- created by a call to ``pytest.mark.NAME(...)``.
+ :param marker:
+ A :py:class:`_pytest.mark.MarkDecorator` object created by a call
+ to ``pytest.mark.NAME(...)``.
"""
self.node.add_marker(marker)
def raiseerror(self, msg: Optional[str]) -> "NoReturn":
- """ raise a FixtureLookupError with the given message. """
+ """Raise a FixtureLookupError with the given message."""
raise self._fixturemanager.FixtureLookupError(None, self, msg)
def _fillfixtures(self) -> None:
item.funcargs[argname] = self.getfixturevalue(argname)
def getfixturevalue(self, argname: str) -> Any:
- """ Dynamically run a named fixture function.
+ """Dynamically run a named fixture function.
Declaring fixtures via function argument is recommended where possible.
But if you can only decide whether to use another fixture at test
setup time, you may use this function to retrieve it inside a fixture
or test function body.
- :raise pytest.FixtureLookupError:
+ :raises pytest.FixtureLookupError:
If the given fixture could not be found.
"""
fixturedef = self._get_active_fixturedef(argname)
def _get_active_fixturedef(
self, argname: str
- ) -> Union["FixtureDef", PseudoFixtureDef]:
+ ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
try:
return self._fixture_defs[argname]
except KeyError:
scope = "function" # type: _Scope
return PseudoFixtureDef(cached_result, scope)
raise
- # remove indent to prevent the python3 exception
- # from leaking into the call
+ # Remove indent to prevent the python3 exception
+ # from leaking into the call.
self._compute_fixture_value(fixturedef)
self._fixture_defs[argname] = fixturedef
return fixturedef
- def _get_fixturestack(self) -> List["FixtureDef"]:
+ def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
current = self
- values = [] # type: List[FixtureDef]
+ values = [] # type: List[FixtureDef[Any]]
while 1:
fixturedef = getattr(current, "_fixturedef", None)
if fixturedef is None:
assert isinstance(current, SubRequest)
current = current._parent_request
- def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None:
- """
- Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
- force the FixtureDef object to throw away any previous results and compute a new fixture value, which
- will be stored into the FixtureDef object itself.
+ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
+ """Create a SubRequest based on "self" and call the execute method
+ of the given FixtureDef object.
+
+ This will force the FixtureDef object to throw away any previous
+ results and compute a new fixture value, which will be stored into
+ the FixtureDef object itself.
"""
# prepare a subrequest object before calling fixture function
# (latter managed by fixturedef)
fail(msg, pytrace=False)
else:
param_index = funcitem.callspec.indices[argname]
- # if a parametrize invocation set a scope it will override
- # the static scope defined with the fixture function
+ # If a parametrize invocation set a scope it will override
+ # the static scope defined with the fixture function.
paramscopenum = funcitem.callspec._arg2scopenum.get(argname)
if paramscopenum is not None:
scope = scopes[paramscopenum]
subrequest = SubRequest(self, scope, param, param_index, fixturedef)
- # check if a higher-level scoped fixture accesses a lower level one
+ # Check if a higher-level scoped fixture accesses a lower level one.
subrequest._check_scope(argname, self.scope, scope)
try:
- # call the fixture function
+ # Call the fixture function.
fixturedef.execute(request=subrequest)
finally:
self._schedule_finalizers(fixturedef, subrequest)
def _schedule_finalizers(
- self, fixturedef: "FixtureDef", subrequest: "SubRequest"
+ self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
) -> None:
- # if fixture function failed it might have registered finalizers
+ # If fixture function failed it might have registered finalizers.
self.session._setupstate.addfinalizer(
functools.partial(fixturedef.finish, request=subrequest), subrequest.node
)
if argname == "request":
return
if scopemismatch(invoking_scope, requested_scope):
- # try to report something helpful
+ # Try to report something helpful.
lines = self._factorytraceback()
fail(
"ScopeMismatch: You tried to access the %r scoped "
def _getscopeitem(self, scope):
if scope == "function":
- # this might also be a non-function Item despite its attribute name
+ # This might also be a non-function Item despite its attribute name.
return self._pyfuncitem
if scope == "package":
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope == "class":
- # fallback to function item itself
+ # Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
return "<FixtureRequest for %r>" % (self.node)
+@final
class SubRequest(FixtureRequest):
- """ a sub request for handling getting a fixture from a
- test function/fixture. """
+ """A sub request for handling getting a fixture from a test function/fixture."""
def __init__(
self,
scope: "_Scope",
param,
param_index: int,
- fixturedef: "FixtureDef",
+ fixturedef: "FixtureDef[object]",
) -> None:
self._parent_request = request
- self.fixturename = fixturedef.argname # type: str
+ self.fixturename = fixturedef.argname
if param is not NOTSET:
self.param = param
self.param_index = param_index
self._fixturedef.addfinalizer(finalizer)
def _schedule_finalizers(
- self, fixturedef: "FixtureDef", subrequest: "SubRequest"
+ self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
) -> None:
- # if the executing fixturedef was not explicitly requested in the argument list (via
+ # If the executing fixturedef was not explicitly requested in the argument list (via
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
- # first
+ # first.
if fixturedef.argname not in self.fixturenames:
fixturedef.addfinalizer(
functools.partial(self._fixturedef.finish, request=self)
def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
"""Look up the index of ``scope`` and raise a descriptive value error
- if not defined.
- """
+ if not defined."""
strscopes = scopes # type: Sequence[str]
try:
return strscopes.index(scope)
)
+@final
class FixtureLookupError(LookupError):
- """ could not return a requested Fixture (missing or invalid). """
+ """Could not return a requested fixture (missing or invalid)."""
def __init__(
self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None
stack.extend(map(lambda x: x.func, self.fixturestack))
msg = self.msg
if msg is not None:
- # the last fixture raise an error, let's present
- # it at the requesting side
+ # The last fixture raise an error, let's present
+ # it at the requesting side.
stack = stack[:-1]
for function in stack:
fspath, lineno = getfslineno(function)
def _teardown_yield_fixture(fixturefunc, it) -> None:
- """Executes the teardown of a fixture function by advancing the iterator after the
- yield and ensure the iteration ends (if not it means there is more than one yield in the function)"""
+ """Execute the teardown of a fixture function by advancing the iterator
+ after the yield and ensure the iteration ends (if not it means there is
+ more than one yield in the function)."""
try:
next(it)
except StopIteration:
return result
+@final
class FixtureDef(Generic[_FixtureValue]):
- """ A container for a factory definition. """
+ """A container for a factory definition."""
def __init__(
self,
else:
scope_ = scope
self.scopenum = scope2index(
- scope_ or "function",
+ # TODO: Check if the `or` here is really necessary.
+ scope_ or "function", # type: ignore[unreachable]
descr="Fixture '{}'".format(func.__name__),
where=baseid,
)
finally:
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
- # even if finalization fails, we invalidate
- # the cached fixture value and remove
- # all finalizers because they may be bound methods which will
- # keep instances alive
+ # Even if finalization fails, we invalidate the cached fixture
+ # value and remove all finalizers because they may be bound methods
+ # which will keep instances alive.
self.cached_result = None
self._finalizers = []
def execute(self, request: SubRequest) -> _FixtureValue:
- # get required arguments and register our own finish()
- # with their finalization
+ # Get required arguments and register our own finish()
+ # with their finalization.
for argname in self.argnames:
fixturedef = request._get_active_fixturedef(argname)
if argname != "request":
my_cache_key = self.cache_key(request)
if self.cached_result is not None:
# note: comparison with `==` can fail (or be expensive) for e.g.
- # numpy arrays (#6497)
+ # numpy arrays (#6497).
cache_key = self.cached_result[1]
if my_cache_key is cache_key:
if self.cached_result[2] is not None:
else:
result = self.cached_result[0]
return result
- # we have a previous but differently parametrized fixture instance
- # so we need to tear it down before creating a new one
+ # We have a previous but differently parametrized fixture instance
+ # so we need to tear it down before creating a new one.
self.finish(request)
assert self.cached_result is None
def resolve_fixture_function(
fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest
) -> "_FixtureFunc[_FixtureValue]":
- """Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific
- instances and bound methods.
- """
+ """Get the actual callable that can be called to obtain the fixture
+ value, dealing with unittest-specific instances and bound methods."""
fixturefunc = fixturedef.func
if fixturedef.unittest:
if request.instance is not None:
- # bind the unbound method to the TestCase instance
+ # Bind the unbound method to the TestCase instance.
fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr]
else:
- # the fixture function needs to be bound to the actual
+ # The fixture function needs to be bound to the actual
# request.instance so that code working with "fixturedef" behaves
# as expected.
if request.instance is not None:
- # handle the case where fixture is defined not in a test class, but some other class
- # (for example a plugin class with a fixture), see #2270
+ # Handle the case where fixture is defined not in a test class, but some other class
+ # (for example a plugin class with a fixture), see #2270.
if hasattr(fixturefunc, "__self__") and not isinstance(
request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr]
):
def pytest_fixture_setup(
fixturedef: FixtureDef[_FixtureValue], request: SubRequest
) -> _FixtureValue:
- """ Execution of fixture setup. """
+ """Execution of fixture setup."""
kwargs = {}
for argname in fixturedef.argnames:
fixdef = request._get_active_fixturedef(argname)
def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
"""Wrap the given fixture function so we can raise an error about it being called directly,
- instead of used as an argument in a test function.
- """
+ instead of used as an argument in a test function."""
message = (
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
"but are created automatically when test functions request them as parameters.\n"
def result(*args, **kwargs):
fail(message, pytrace=False)
- # keep reference to the original function in our own custom attribute so we don't unwrap
- # further than this point and lose useful wrappings like @mock.patch (#3774)
+ # Keep reference to the original function in our own custom attribute so we don't unwrap
+ # further than this point and lose useful wrappings like @mock.patch (#3774).
result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
return result
+@final
@attr.s(frozen=True)
class FixtureFunctionMarker:
scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]")
] = ...,
name: Optional[str] = ...
) -> _FixtureFunction:
- raise NotImplementedError()
+ ...
@overload # noqa: F811
] = ...,
name: Optional[str] = None
) -> FixtureFunctionMarker:
- raise NotImplementedError()
+ ...
def fixture( # noqa: F811
fixture_function: Optional[_FixtureFunction] = None,
- *args: Any,
+ *,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
params: Optional[Iterable[object]] = None,
autouse: bool = False,
fixture function.
The name of the fixture function can later be referenced to cause its
- invocation ahead of running tests: test
- modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
- marker.
-
- Test functions can directly use fixture names as input
- arguments in which case the fixture instance returned from the fixture
- function will be injected.
-
- Fixtures can provide their values to test functions using ``return`` or ``yield``
- statements. When using ``yield`` the code block after the ``yield`` statement is executed
- as teardown code regardless of the test outcome, and must yield exactly once.
-
- :arg scope: the scope for which this fixture is shared, one of
- ``"function"`` (default), ``"class"``, ``"module"``,
- ``"package"`` or ``"session"``.
-
- This parameter may also be a callable which receives ``(fixture_name, config)``
- as parameters, and must return a ``str`` with one of the values mentioned above.
-
- See :ref:`dynamic scope` in the docs for more information.
-
- :arg params: an optional list of parameters which will cause multiple
- invocations of the fixture function and all of the tests
- using it.
- The current parameter is available in ``request.param``.
-
- :arg autouse: if True, the fixture func is activated for all tests that
- can see it. If False (the default) then an explicit
- reference is needed to activate the fixture.
-
- :arg ids: list of string ids each corresponding to the params
- so that they are part of the test id. If no ids are provided
- they will be generated automatically from the params.
-
- :arg name: the name of the fixture. This defaults to the name of the
- decorated function. If a fixture is used in the same module in
- which it is defined, the function name of the fixture will be
- shadowed by the function arg that requests the fixture; one way
- to resolve this is to name the decorated function
- ``fixture_<fixturename>`` and then use
- ``@pytest.fixture(name='<fixturename>')``.
+ invocation ahead of running tests: test modules or classes can use the
+ ``pytest.mark.usefixtures(fixturename)`` marker.
+
+ Test functions can directly use fixture names as input arguments in which
+ case the fixture instance returned from the fixture function will be
+ injected.
+
+ Fixtures can provide their values to test functions using ``return`` or
+ ``yield`` statements. When using ``yield`` the code block after the
+ ``yield`` statement is executed as teardown code regardless of the test
+ outcome, and must yield exactly once.
+
+ :param scope:
+ The scope for which this fixture is shared; one of ``"function"``
+ (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``.
+
+ This parameter may also be a callable which receives ``(fixture_name, config)``
+ as parameters, and must return a ``str`` with one of the values mentioned above.
+
+ See :ref:`dynamic scope` in the docs for more information.
+
+ :param params:
+ An optional list of parameters which will cause multiple invocations
+ of the fixture function and all of the tests using it. The current
+ parameter is available in ``request.param``.
+
+ :param autouse:
+ If True, the fixture func is activated for all tests that can see it.
+ If False (the default), an explicit reference is needed to activate
+ the fixture.
+
+ :param ids:
+ List of string ids each corresponding to the params so that they are
+ part of the test id. If no ids are provided they will be generated
+ automatically from the params.
+
+ :param name:
+ The name of the fixture. This defaults to the name of the decorated
+ function. If a fixture is used in the same module in which it is
+ defined, the function name of the fixture will be shadowed by the
+ function arg that requests the fixture; one way to resolve this is to
+ name the decorated function ``fixture_<fixturename>`` and then use
+ ``@pytest.fixture(name='<fixturename>')``.
"""
- # Positional arguments backward compatibility.
- # If a kwarg is equal to its default, assume it was not explicitly
- # passed, i.e. not duplicated. The more correct way is to use a
- # **kwargs and check `in`, but that obfuscates the function signature.
- if isinstance(fixture_function, str):
- # It's actually the first positional argument, scope.
- args = (fixture_function, *args)
- fixture_function = None
- duplicated_args = []
- if len(args) > 0:
- if scope == "function":
- scope = args[0]
- else:
- duplicated_args.append("scope")
- if len(args) > 1:
- if params is None:
- params = args[1]
- else:
- duplicated_args.append("params")
- if len(args) > 2:
- if autouse is False:
- autouse = args[2]
- else:
- duplicated_args.append("autouse")
- if len(args) > 3:
- if ids is None:
- ids = args[3]
- else:
- duplicated_args.append("ids")
- if len(args) > 4:
- if name is None:
- name = args[4]
- else:
- duplicated_args.append("name")
- if len(args) > 5:
- raise TypeError(
- "fixture() takes 5 positional arguments but {} were given".format(len(args))
- )
- if duplicated_args:
- raise TypeError(
- "The fixture arguments are defined as positional and keyword: {}. "
- "Use only keyword arguments.".format(", ".join(duplicated_args))
- )
- if args:
- warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2)
- # End backward compatiblity.
-
fixture_marker = FixtureFunctionMarker(
scope=scope, params=params, autouse=autouse, ids=ids, name=name,
)
ids=None,
name=None
):
- """ (return a) decorator to mark a yield-fixture factory function.
+ """(Return a) decorator to mark a yield-fixture factory function.
.. deprecated:: 3.0
Use :py:func:`pytest.fixture` directly instead.
class FixtureManager:
- """
- pytest fixtures definitions and information is stored and managed
+ """pytest fixture definitions and information is stored and managed
from this class.
During collection fm.parsefactories() is called multiple times to parse
which themselves offer a fixturenames attribute.
The FuncFixtureInfo object holds information about fixtures and FixtureDefs
- relevant for a particular function. An initial list of fixtures is
+ relevant for a particular function. An initial list of fixtures is
assembled like this:
- ini-defined usefixtures
Subsequently the funcfixtureinfo.fixturenames attribute is computed
as the closure of the fixtures needed to setup the initial fixtures,
- i. e. fixtures needed by fixture functions themselves are appended
+ i.e. fixtures needed by fixture functions themselves are appended
to the fixturenames list.
Upon the test-setup phases all fixturenames are instantiated, retrieved
def __init__(self, session: "Session") -> None:
self.session = session
self.config = session.config # type: Config
- self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef]]
- self._holderobjseen = set() # type: Set
+ self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef[Any]]]
+ self._holderobjseen = set() # type: Set[object]
self._nodeid_and_autousenames = [
("", self.config.getini("usefixtures"))
] # type: List[Tuple[str, List[str]]]
session.config.pluginmanager.register(self, "funcmanage")
def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]:
- """This function returns all the direct parametrization
- arguments of a node, so we don't mistake them for fixtures
+ """Return all direct parametrization arguments of a node, so we don't
+ mistake them for fixtures.
- Check https://github.com/pytest-dev/pytest/issues/5036
+ Check https://github.com/pytest-dev/pytest/issues/5036.
- This things are done later as well when dealing with parametrization
- so this could be improved
+ These things are done later as well when dealing with parametrization
+ so this could be improved.
"""
parametrize_argnames = [] # type: List[str]
for marker in node.iter_markers(name="parametrize"):
else:
argnames = ()
- usefixtures = itertools.chain.from_iterable(
- mark.args for mark in node.iter_markers(name="usefixtures")
+ usefixtures = tuple(
+ arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
)
- initialnames = tuple(usefixtures) + argnames
+ initialnames = usefixtures + argnames
fm = node.session._fixturemanager
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
nodeid = None
try:
- p = py.path.local(plugin.__file__) # type: ignore[attr-defined]
+ p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
except AttributeError:
pass
else:
from _pytest import nodes
- # construct the base nodeid which is later used to check
+ # Construct the base nodeid which is later used to check
# what fixtures are visible for particular tests (as denoted
- # by their test id)
- if p.basename.startswith("conftest.py"):
- nodeid = p.dirpath().relto(self.config.rootdir)
- if p.sep != nodes.SEP:
- nodeid = nodeid.replace(p.sep, nodes.SEP)
+ # by their test id).
+ if p.name.startswith("conftest.py"):
+ try:
+ nodeid = str(p.parent.relative_to(self.config.rootpath))
+ except ValueError:
+ nodeid = ""
+ if nodeid == ".":
+ nodeid = ""
+ if os.sep != nodes.SEP:
+ nodeid = nodeid.replace(os.sep, nodes.SEP)
self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid: str) -> List[str]:
- """ return a tuple of fixture names to be used. """
+ """Return a list of fixture names to be used."""
autousenames = [] # type: List[str]
for baseid, basenames in self._nodeid_and_autousenames:
if nodeid.startswith(baseid):
def getfixtureclosure(
self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = ()
- ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef]]]:
- # collect the closure of all fixtures , starting with the given
+ ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
+ # Collect the closure of all fixtures, starting with the given
# fixturenames as the initial set. As we have to visit all
# factory definitions anyway, we also return an arg2fixturedefs
# mapping so that the caller can reuse it and does not have
# to re-discover fixturedefs again for each fixturename
- # (discovering matching fixtures for a given name/node is expensive)
+ # (discovering matching fixtures for a given name/node is expensive).
parentid = parentnode.nodeid
fixturenames_closure = self._getautousenames(parentid)
merge(fixturenames)
- # at this point, fixturenames_closure contains what we call "initialnames",
+ # At this point, fixturenames_closure contains what we call "initialnames",
# which is a set of fixturenames the function immediately requests. We
# need to return it as well, so save this.
initialnames = tuple(fixturenames_closure)
- arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef]]
+ arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef[Any]]]
lastlen = -1
while lastlen != len(fixturenames_closure):
lastlen = len(fixturenames_closure)
return initialnames, fixturenames_closure, arg2fixturedefs
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
+ """Generate new tests based on parametrized fixtures used by the given metafunc"""
+
+ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
+ args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs)
+ return args
+
for argname in metafunc.fixturenames:
- faclist = metafunc._arg2fixturedefs.get(argname)
- if faclist:
- fixturedef = faclist[-1]
+ # Get the FixtureDefs for the argname.
+ fixture_defs = metafunc._arg2fixturedefs.get(argname)
+ if not fixture_defs:
+ # Will raise FixtureLookupError at setup time if not parametrized somewhere
+ # else (e.g @pytest.mark.parametrize)
+ continue
+
+ # If the test itself parametrizes using this argname, give it
+ # precedence.
+ if any(
+ argname in get_parametrize_mark_argnames(mark)
+ for mark in metafunc.definition.iter_markers("parametrize")
+ ):
+ continue
+
+ # In the common case we only look at the fixture def with the
+ # closest scope (last in the list). But if the fixture overrides
+ # another fixture, while requesting the super fixture, keep going
+ # in case the super fixture is parametrized (#1953).
+ for fixturedef in reversed(fixture_defs):
+ # Fixture is parametrized, apply it and stop.
if fixturedef.params is not None:
- markers = list(metafunc.definition.iter_markers("parametrize"))
- for parametrize_mark in markers:
- if "argnames" in parametrize_mark.kwargs:
- argnames = parametrize_mark.kwargs["argnames"]
- else:
- argnames = parametrize_mark.args[0]
-
- if not isinstance(argnames, (tuple, list)):
- argnames = [
- x.strip() for x in argnames.split(",") if x.strip()
- ]
- if argname in argnames:
- break
- else:
- metafunc.parametrize(
- argname,
- fixturedef.params,
- indirect=True,
- scope=fixturedef.scope,
- ids=fixturedef.ids,
- )
- else:
- continue # will raise FixtureLookupError at setup time
+ metafunc.parametrize(
+ argname,
+ fixturedef.params,
+ indirect=True,
+ scope=fixturedef.scope,
+ ids=fixturedef.ids,
+ )
+ break
+
+ # Not requesting the overridden super fixture, stop.
+ if argname not in fixturedef.argnames:
+ break
+
+ # Try next super fixture, if any.
def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None:
- # separate parametrized setups
+ # Separate parametrized setups.
items[:] = reorder_items(items)
def parsefactories(
obj = safe_getattr(holderobj, name, None)
marker = getfixturemarker(obj)
if not isinstance(marker, FixtureFunctionMarker):
- # magic globals with __getattr__ might have got us a wrong
- # fixture attribute
+ # Magic globals with __getattr__ might have got us a wrong
+ # fixture attribute.
continue
if marker.name:
name = marker.name
- # during fixture definition we wrap the original fixture function
- # to issue a warning if called directly, so here we unwrap it in order to not emit the warning
- # when pytest itself calls the fixture function
+ # During fixture definition we wrap the original fixture function
+ # to issue a warning if called directly, so here we unwrap it in
+ # order to not emit the warning when pytest itself calls the
+ # fixture function.
obj = get_real_method(obj, holderobj)
fixture_def = FixtureDef(
def getfixturedefs(
self, argname: str, nodeid: str
- ) -> Optional[Sequence[FixtureDef]]:
- """
- Gets a list of fixtures which are applicable to the given node id.
+ ) -> Optional[Sequence[FixtureDef[Any]]]:
+ """Get a list of fixtures which are applicable to the given node id.
- :param str argname: name of the fixture to search for
- :param str nodeid: full node id of the requesting test.
- :return: list[FixtureDef]
+ :param str argname: Name of the fixture to search for.
+ :param str nodeid: Full node id of the requesting test.
+ :rtype: Sequence[FixtureDef]
"""
try:
fixturedefs = self._arg2fixturedefs[argname]
return tuple(self._matchfactories(fixturedefs, nodeid))
def _matchfactories(
- self, fixturedefs: Iterable[FixtureDef], nodeid: str
- ) -> Iterator[FixtureDef]:
+ self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
+ ) -> Iterator[FixtureDef[Any]]:
from _pytest import nodes
for fixturedef in fixturedefs:
-"""
-Provides a function to report all internal modules for using freezing tools
-pytest
-"""
+"""Provides a function to report all internal modules for using freezing
+tools."""
import types
from typing import Iterator
from typing import List
def freeze_includes() -> List[str]:
- """
- Returns a list of module names used by pytest that should be
- included by cx_freeze.
- """
+ """Return a list of module names used by pytest that should be
+ included by cx_freeze."""
import py
import _pytest
def _iter_all_modules(
package: Union[str, types.ModuleType], prefix: str = "",
) -> Iterator[str]:
- """
- Iterates over the names of all modules that can be found in the given
+ """Iterate over the names of all modules that can be found in the given
package, recursively.
>>> import _pytest
-""" version info, help messages, tracing configuration. """
+"""Version info, help messages, tracing configuration."""
import os
import sys
from argparse import Action
class HelpAction(Action):
- """This is an argparse Action that will raise an exception in
- order to skip the rest of the argument parsing when --help is passed.
+ """An argparse Action that will raise an exception in order to skip the
+ rest of the argument parsing when --help is passed.
+
This prevents argparse from quitting due to missing required arguments
when any are defined, for example by ``pytest_addoption``.
This is similar to the way that the builtin argparse --help option is
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, self.const)
- # We should only skip the rest of the parsing after preparse is done
+ # We should only skip the rest of the parsing after preparse is done.
if getattr(parser._parser, "after_preparse", False):
raise PrintHelp
-""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
+"""Hook specifications for pytest plugins which are invoked by pytest itself
+and by builtin plugins."""
from typing import Any
from typing import Dict
from typing import List
import py.path
from pluggy import HookspecMarker
-from .deprecated import COLLECT_DIRECTORY_HOOK
from _pytest.compat import TYPE_CHECKING
+from _pytest.deprecated import WARNING_CAPTURED_HOOK
if TYPE_CHECKING:
import pdb
@hookspec(historic=True)
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
- """called at plugin registration time to allow adding new hooks via a call to
+ """Called at plugin registration time to allow adding new hooks via a call to
``pluginmanager.add_hookspecs(module_or_class, prefix)``.
-
- :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
+ :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager.
.. note::
This hook is incompatible with ``hookwrapper=True``.
def pytest_plugin_registered(
plugin: "_PluggyPlugin", manager: "PytestPluginManager"
) -> None:
- """ a new pytest plugin got registered.
+ """A new pytest plugin got registered.
- :param plugin: the plugin module or instance
- :param _pytest.config.PytestPluginManager manager: pytest plugin manager
+ :param plugin: The plugin module or instance.
+ :param _pytest.config.PytestPluginManager manager: pytest plugin manager.
.. note::
This hook is incompatible with ``hookwrapper=True``.
@hookspec(historic=True)
def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None:
- """register argparse-style options and ini-style config values,
+ """Register argparse-style options and ini-style config values,
called once at the beginning of a test run.
.. note::
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
- :arg _pytest.config.argparsing.Parser parser: To add command line options, call
+ :param _pytest.config.argparsing.Parser parser:
+ To add command line options, call
:py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`.
To add ini-file values call :py:func:`parser.addini(...)
<_pytest.config.argparsing.Parser.addini>`.
- :arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager,
- which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s
- and allow one plugin to call another plugin's hooks to change how
- command line options are added.
+ :param _pytest.config.PytestPluginManager pluginmanager:
+ pytest plugin manager, which can be used to install :py:func:`hookspec`'s
+ or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks
+ to change how command line options are added.
Options can later be accessed through the
:py:class:`config <_pytest.config.Config>` object, respectively:
@hookspec(historic=True)
def pytest_configure(config: "Config") -> None:
- """
- Allows plugins and conftest files to perform initial configuration.
+ """Allow plugins and conftest files to perform initial configuration.
This hook is called for every plugin and initial conftest file
after command line options have been parsed.
.. note::
This hook is incompatible with ``hookwrapper=True``.
- :arg _pytest.config.Config config: pytest config object
+ :param _pytest.config.Config config: The pytest config object.
"""
def pytest_cmdline_parse(
pluginmanager: "PytestPluginManager", args: List[str]
) -> Optional["Config"]:
- """return initialized config object, parsing the specified args.
+ """Return an initialized config object, parsing the specified args.
- Stops at first non-None result, see :ref:`firstresult`
+ Stops at first non-None result, see :ref:`firstresult`.
.. note::
- This hook will only be called for plugin classes passed to the ``plugins`` arg when using `pytest.main`_ to
- perform an in-process test run.
+ This hook will only be called for plugin classes passed to the
+ ``plugins`` arg when using `pytest.main`_ to perform an in-process
+ test run.
- :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
- :param list[str] args: list of arguments passed on the command line
+ :param _pytest.config.PytestPluginManager pluginmanager: Pytest plugin manager.
+ :param List[str] args: List of arguments passed on the command line.
"""
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
- :param _pytest.config.Config config: pytest config object
- :param list[str] args: list of arguments passed on the command line
+ :param _pytest.config.Config config: The pytest config object.
+ :param List[str] args: Arguments passed on the command line.
"""
@hookspec(firstresult=True)
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
- """ called for performing the main command line action. The default
+ """Called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
- Stops at first non-None result, see :ref:`firstresult`
+ Stops at first non-None result, see :ref:`firstresult`.
- :param _pytest.config.Config config: pytest config object
+ :param _pytest.config.Config config: The pytest config object.
"""
def pytest_load_initial_conftests(
early_config: "Config", parser: "Parser", args: List[str]
) -> None:
- """ implements the loading of initial conftest files ahead
+ """Called to implement the loading of initial conftest files ahead
of command line option parsing.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
- :param _pytest.config.Config early_config: pytest config object
- :param list[str] args: list of arguments passed on the command line
- :param _pytest.config.argparsing.Parser parser: to add command line options
+ :param _pytest.config.Config early_config: The pytest config object.
+ :param List[str] args: Arguments passed on the command line.
+ :param _pytest.config.argparsing.Parser parser: To add command line options.
"""
@hookspec(firstresult=True)
def pytest_collection(session: "Session") -> Optional[object]:
- """Perform the collection protocol for the given session.
+ """Perform the collection phase for the given session.
Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing.
- The hook is meant to set `session.items` to a sequence of items at least,
- but normally should follow this procedure:
+ The default collection phase is this (see individual hooks for full details):
+
+ 1. Starting from ``session`` as the initial collector:
+
+ 1. ``pytest_collectstart(collector)``
+ 2. ``report = pytest_make_collect_report(collector)``
+ 3. ``pytest_exception_interact(collector, call, report)`` if an interactive exception occurred
+ 4. For each collected node:
+
+ 1. If an item, ``pytest_itemcollected(item)``
+ 2. If a collector, recurse into it.
+
+ 5. ``pytest_collectreport(report)``
+
+ 2. ``pytest_collection_modifyitems(session, config, items)``
+
+ 1. ``pytest_deselected(items)`` for any deselected items (may be called multiple times)
- 1. Call the pytest_collectstart hook.
- 2. Call the pytest_collectreport hook.
- 3. Call the pytest_collection_modifyitems hook.
- 4. Call the pytest_collection_finish hook.
- 5. Set session.testscollected to the amount of collect items.
- 6. Set `session.items` to a list of items.
+ 3. ``pytest_collection_finish(session)``
+ 4. Set ``session.items`` to the list of collected items
+ 5. Set ``session.testscollected`` to the number of collected items
You can implement this hook to only perform some action before collection,
for example the terminal plugin uses it to start displaying the collection
counter (and returns `None`).
- :param _pytest.main.Session session: the pytest session object
+ :param pytest.Session session: The pytest session object.
"""
def pytest_collection_modifyitems(
session: "Session", config: "Config", items: List["Item"]
) -> None:
- """ called after collection has been performed, may filter or re-order
+ """Called after collection has been performed. May filter or re-order
the items in-place.
- :param _pytest.main.Session session: the pytest session object
- :param _pytest.config.Config config: pytest config object
- :param List[_pytest.nodes.Item] items: list of item objects
+ :param pytest.Session session: The pytest session object.
+ :param _pytest.config.Config config: The pytest config object.
+ :param List[pytest.Item] items: List of item objects.
"""
def pytest_collection_finish(session: "Session") -> None:
"""Called after collection has been performed and modified.
- :param _pytest.main.Session session: the pytest session object
+ :param pytest.Session session: The pytest session object.
"""
Stops at first non-None result, see :ref:`firstresult`.
- :param path: a :py:class:`py.path.local` - the path to analyze
- :param _pytest.config.Config config: pytest config object
- """
-
-
-@hookspec(firstresult=True, warn_on_impl=COLLECT_DIRECTORY_HOOK)
-def pytest_collect_directory(path: py.path.local, parent) -> Optional[object]:
- """Called before traversing a directory for collection files.
-
- Stops at first non-None result, see :ref:`firstresult`.
-
- :param path: a :py:class:`py.path.local` - the path to analyze
+ :param py.path.local path: The path to analyze.
+ :param _pytest.config.Config config: The pytest config object.
"""
-def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]":
- """Return collection Node or None for the given path.
+def pytest_collect_file(
+ path: py.path.local, parent: "Collector"
+) -> "Optional[Collector]":
+ """Create a Collector for the given path, or None if not relevant.
- Any new node needs to have the specified ``parent`` as a parent.
+ The new node needs to have the specified ``parent`` as a parent.
- :param path: a :py:class:`py.path.local` - the path to collect
+ :param py.path.local path: The path to collect.
"""
def pytest_collectstart(collector: "Collector") -> None:
- """ collector starts collecting. """
+ """Collector starts collecting."""
def pytest_itemcollected(item: "Item") -> None:
def pytest_collectreport(report: "CollectReport") -> None:
- """ collector finished collecting. """
+ """Collector finished collecting."""
def pytest_deselected(items: Sequence["Item"]) -> None:
- """Called for deselected test items, e.g. by keyword."""
+ """Called for deselected test items, e.g. by keyword.
+
+ May be called multiple times.
+ """
@hookspec(firstresult=True)
def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]":
- """ perform ``collector.collect()`` and return a CollectReport.
+ """Perform ``collector.collect()`` and return a CollectReport.
- Stops at first non-None result, see :ref:`firstresult` """
+ Stops at first non-None result, see :ref:`firstresult`.
+ """
# -------------------------------------------------------------------------
Stops at first non-None result, see :ref:`firstresult`.
- :param path: a :py:class:`py.path.local` - the path of module to collect
+ :param py.path.local path: The path of module to collect.
"""
@hookspec(firstresult=True)
def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
- """ call underlying test function.
+ """Call underlying test function.
- Stops at first non-None result, see :ref:`firstresult` """
+ Stops at first non-None result, see :ref:`firstresult`.
+ """
def pytest_generate_tests(metafunc: "Metafunc") -> None:
- """ generate (multiple) parametrized calls to a test function."""
+ """Generate (multiple) parametrized calls to a test function."""
@hookspec(firstresult=True)
def pytest_make_parametrize_id(
config: "Config", val: object, argname: str
) -> Optional[str]:
- """Return a user-friendly string representation of the given ``val`` that will be used
- by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
+ """Return a user-friendly string representation of the given ``val``
+ that will be used by @pytest.mark.parametrize calls, or None if the hook
+ doesn't know about ``val``.
+
The parameter name is available as ``argname``, if required.
- Stops at first non-None result, see :ref:`firstresult`
+ Stops at first non-None result, see :ref:`firstresult`.
- :param _pytest.config.Config config: pytest config object
- :param val: the parametrized value
- :param str argname: the automatic parameter name produced by pytest
+ :param _pytest.config.Config config: The pytest config object.
+ :param val: The parametrized value.
+ :param str argname: The automatic parameter name produced by pytest.
"""
@hookspec(firstresult=True)
def pytest_runtestloop(session: "Session") -> Optional[object]:
- """Performs the main runtest loop (after collection finished).
+ """Perform the main runtest loop (after collection finished).
The default hook implementation performs the runtest protocol for all items
collected in the session (``session.items``), unless the collection failed
If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the
loop is terminated after the runtest protocol for the current item is finished.
- :param _pytest.main.Session session: The pytest session object.
+ :param pytest.Session session: The pytest session object.
Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing.
def pytest_runtest_protocol(
item: "Item", nextitem: "Optional[Item]"
) -> Optional[object]:
- """Performs the runtest protocol for a single test item.
+ """Perform the runtest protocol for a single test item.
The default runtest protocol is this (see individual hooks for full details):
- ``pytest_runtest_logfinish(nodeid, location)``
- :arg item: Test item for which the runtest protocol is performed.
-
- :arg nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
+ :param item: Test item for which the runtest protocol is performed.
+ :param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing.
includes running the teardown phase of fixtures required by the item (if
they go out of scope).
- :arg nextitem: The scheduled-to-be-next test item (None if no further
- test item is scheduled). This argument can be used to
- perform exact teardowns, i.e. calling just enough finalizers
- so that nextitem only needs to call setup-functions.
+ :param nextitem:
+ The scheduled-to-be-next test item (None if no further test item is
+ scheduled). This argument can be used to perform exact teardowns,
+ i.e. calling just enough finalizers so that nextitem only needs to
+ call setup-functions.
"""
def pytest_report_to_serializable(
config: "Config", report: Union["CollectReport", "TestReport"],
) -> Optional[Dict[str, Any]]:
- """
- Serializes the given report object into a data structure suitable for sending
- over the wire, e.g. converted to JSON.
- """
+ """Serialize the given report object into a data structure suitable for
+ sending over the wire, e.g. converted to JSON."""
@hookspec(firstresult=True)
def pytest_report_from_serializable(
config: "Config", data: Dict[str, Any],
) -> Optional[Union["CollectReport", "TestReport"]]:
- """
- Restores a report object previously serialized with pytest_report_to_serializable().
- """
+ """Restore a report object previously serialized with pytest_report_to_serializable()."""
# -------------------------------------------------------------------------
@hookspec(firstresult=True)
def pytest_fixture_setup(
- fixturedef: "FixtureDef", request: "SubRequest"
+ fixturedef: "FixtureDef[Any]", request: "SubRequest"
) -> Optional[object]:
- """Performs fixture setup execution.
+ """Perform fixture setup execution.
- :return: The return value of the call to the fixture function.
+ :returns: The return value of the call to the fixture function.
Stops at first non-None result, see :ref:`firstresult`.
def pytest_fixture_post_finalizer(
- fixturedef: "FixtureDef", request: "SubRequest"
+ fixturedef: "FixtureDef[Any]", request: "SubRequest"
) -> None:
"""Called after fixture teardown, but before the cache is cleared, so
the fixture result ``fixturedef.cached_result`` is still available (not
"""Called after the ``Session`` object has been created and before performing collection
and entering the run test loop.
- :param _pytest.main.Session session: the pytest session object
+ :param pytest.Session session: The pytest session object.
"""
) -> None:
"""Called after whole test run finished, right before returning the exit status to the system.
- :param _pytest.main.Session session: the pytest session object
- :param int exitstatus: the status which pytest will return to the system
+ :param pytest.Session session: The pytest session object.
+ :param int exitstatus: The status which pytest will return to the system.
"""
def pytest_unconfigure(config: "Config") -> None:
"""Called before test process is exited.
- :param _pytest.config.Config config: pytest config object
+ :param _pytest.config.Config config: The pytest config object.
"""
"""Return explanation for comparisons in failing assert expressions.
Return None for no custom explanation, otherwise return a list
- of strings. The strings will be joined by newlines but any newlines
- *in* a string will be escaped. Note that all but the first line will
+ of strings. The strings will be joined by newlines but any newlines
+ *in* a string will be escaped. Note that all but the first line will
be indented slightly, the intention is for the first line to be a summary.
- :param _pytest.config.Config config: pytest config object
+ :param _pytest.config.Config config: The pytest config object.
"""
def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None:
- """
- **(Experimental)**
+ """**(Experimental)** Called whenever an assertion passes.
.. versionadded:: 5.0
- Hook called whenever an assertion *passes*.
-
Use this hook to do some processing after a passing assertion.
The original assertion information is available in the `orig` string
and the pytest introspected assertion information is available in the
You need to **clean the .pyc** files in your project directory and interpreter libraries
when enabling this option, as assertions will require to be re-written.
- :param _pytest.nodes.Item item: pytest item object of current test
- :param int lineno: line number of the assert statement
- :param string orig: string with original assertion
- :param string expl: string with assert explanation
+ :param pytest.Item item: pytest item object of current test.
+ :param int lineno: Line number of the assert statement.
+ :param str orig: String with the original assertion.
+ :param str expl: String with the assert explanation.
.. note::
This hook is **experimental**, so its parameters or even the hook itself might
be changed/removed without warning in any future pytest release.
- If you find this hook useful, please share your feedback opening an issue.
+ If you find this hook useful, please share your feedback in an issue.
"""
# -------------------------------------------------------------------------
-# hooks for influencing reporting (invoked from _pytest_terminal)
+# Hooks for influencing reporting (invoked from _pytest_terminal).
# -------------------------------------------------------------------------
def pytest_report_header(
config: "Config", startdir: py.path.local
) -> Union[str, List[str]]:
- """ return a string or list of strings to be displayed as header info for terminal reporting.
+ """Return a string or list of strings to be displayed as header info for terminal reporting.
- :param _pytest.config.Config config: pytest config object
- :param startdir: py.path object with the starting dir
+ :param _pytest.config.Config config: The pytest config object.
+ :param py.path.local startdir: The starting dir.
.. note::
def pytest_report_collectionfinish(
config: "Config", startdir: py.path.local, items: Sequence["Item"],
) -> Union[str, List[str]]:
- """
- .. versionadded:: 3.2
-
- Return a string or list of strings to be displayed after collection has finished successfully.
+ """Return a string or list of strings to be displayed after collection
+ has finished successfully.
These strings will be displayed after the standard "collected X items" message.
- :param _pytest.config.Config config: pytest config object
- :param startdir: py.path object with the starting dir
- :param items: list of pytest items that are going to be executed; this list should not be modified.
+ .. versionadded:: 3.2
+
+ :param _pytest.config.Config config: The pytest config object.
+ :param py.path.local startdir: The starting dir.
+ :param items: List of pytest items that are going to be executed; this list should not be modified.
.. note::
) -> None:
"""Add a section to terminal summary reporting.
- :param _pytest.terminal.TerminalReporter terminalreporter: the internal terminal reporter object
- :param int exitstatus: the exit status that will be reported back to the OS
- :param _pytest.config.Config config: pytest config object
+ :param _pytest.terminal.TerminalReporter terminalreporter: The internal terminal reporter object.
+ :param int exitstatus: The exit status that will be reported back to the OS.
+ :param _pytest.config.Config config: The pytest config object.
.. versionadded:: 4.2
The ``config`` parameter.
"""
-# Uncomment this after 6.0 release (#7361)
-# @hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
-@hookspec(historic=True)
+@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
def pytest_warning_captured(
warning_message: "warnings.WarningMessage",
when: "Literal['config', 'collect', 'runtest']",
nodeid: str,
location: Optional[Tuple[str, int, str]],
) -> None:
- """
- Process a warning captured by the internal pytest warnings plugin.
+ """Process a warning captured by the internal pytest warnings plugin.
:param warnings.WarningMessage warning_message:
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
* ``"collect"``: during test collection.
* ``"runtest"``: during test execution.
- :param str nodeid: full id of the item
+ :param str nodeid:
+ Full id of the item.
:param tuple|None location:
When available, holds information about the execution context of the captured
def pytest_keyboard_interrupt(
excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]",
) -> None:
- """ called for keyboard interrupt. """
+ """Called for keyboard interrupt."""
def pytest_exception_interact(
node: Union["Item", "Collector"],
- call: "CallInfo[object]",
+ call: "CallInfo[Any]",
report: Union["CollectReport", "TestReport"],
) -> None:
"""Called when an exception was raised which can potentially be
def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
- """ called upon pdb.set_trace(), can be used by plugins to take special
- action just before the python debugger enters in interactive mode.
+ """Called upon pdb.set_trace().
- :param _pytest.config.Config config: pytest config object
- :param pdb.Pdb pdb: Pdb instance
+ Can be used by plugins to take special action just before the python
+ debugger enters interactive mode.
+
+ :param _pytest.config.Config config: The pytest config object.
+ :param pdb.Pdb pdb: The Pdb instance.
"""
def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
- """ called when leaving pdb (e.g. with continue after pdb.set_trace()).
+ """Called when leaving pdb (e.g. with continue after pdb.set_trace()).
Can be used by plugins to take special action just after the python
debugger leaves interactive mode.
- :param _pytest.config.Config config: pytest config object
- :param pdb.Pdb pdb: Pdb instance
+ :param _pytest.config.Config config: The pytest config object.
+ :param pdb.Pdb pdb: The Pdb instance.
"""
-"""
- report test results in JUnit-XML format,
- for use with Jenkins and build integration servers.
-
+"""Report test results in JUnit-XML format, for use with Jenkins and build
+integration servers.
Based on initial code from Ross Lawley.
-Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/
-src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
+Output conforms to
+https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
"""
import functools
import os
import platform
import re
-import sys
+import xml.etree.ElementTree as ET
from datetime import datetime
from typing import Callable
from typing import Dict
from typing import Tuple
from typing import Union
-import py
-
import pytest
-from _pytest import deprecated
from _pytest import nodes
from _pytest import timing
from _pytest._code.code import ExceptionRepr
-from _pytest.compat import TYPE_CHECKING
+from _pytest._code.code import ReprFileLocation
from _pytest.config import Config
from _pytest.config import filename_arg
from _pytest.config.argparsing import Parser
from _pytest.reports import TestReport
from _pytest.store import StoreKey
from _pytest.terminal import TerminalReporter
-from _pytest.warnings import _issue_warning_captured
-
-if TYPE_CHECKING:
- from typing import Type
xml_key = StoreKey["LogXML"]()
-class Junit(py.xml.Namespace):
- pass
-
-
-# We need to get the subset of the invalid unicode ranges according to
-# XML 1.0 which are valid in this python build. Hence we calculate
-# this dynamically instead of hardcoding it. The spec range of valid
-# chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
-# | [#x10000-#x10FFFF]
-_legal_chars = (0x09, 0x0A, 0x0D)
-_legal_ranges = ((0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF))
-_legal_xml_re = [
- "{}-{}".format(chr(low), chr(high))
- for (low, high) in _legal_ranges
- if low < sys.maxunicode
-]
-_legal_xml_re = [chr(x) for x in _legal_chars] + _legal_xml_re
-illegal_xml_re = re.compile("[^%s]" % "".join(_legal_xml_re))
-del _legal_chars
-del _legal_ranges
-del _legal_xml_re
-
-_py_ext_re = re.compile(r"\.py$")
+def bin_xml_escape(arg: object) -> str:
+ r"""Visually escape invalid XML characters.
+ For example, transforms
+ 'hello\aworld\b'
+ into
+ 'hello#x07world#x08'
+ Note that the #xABs are *not* XML escapes - missing the ampersand «.
+ The idea is to escape visually for the user rather than for XML itself.
+ """
-def bin_xml_escape(arg: object) -> py.xml.raw:
def repl(matchobj: Match[str]) -> str:
i = ord(matchobj.group())
if i <= 0xFF:
else:
return "#x%04X" % i
- return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(str(arg))))
+ # The spec range of valid chars is:
+ # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ # For an unknown(?) reason, we disallow #x7F (DEL) as well.
+ illegal_xml_re = (
+ "[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]"
+ )
+ return re.sub(illegal_xml_re, repl, str(arg))
def merge_family(left, right) -> None:
families["_base"] = {"testcase": ["classname", "name"]}
families["_base_legacy"] = {"testcase": ["file", "line", "url"]}
-# xUnit 1.x inherits legacy attributes
+# xUnit 1.x inherits legacy attributes.
families["xunit1"] = families["_base"].copy()
merge_family(families["xunit1"], families["_base_legacy"])
-# xUnit 2.x uses strict base attributes
+# xUnit 2.x uses strict base attributes.
families["xunit2"] = families["_base"]
self.add_stats = self.xml.add_stats
self.family = self.xml.family
self.duration = 0
- self.properties = [] # type: List[Tuple[str, py.xml.raw]]
- self.nodes = [] # type: List[py.xml.Tag]
- self.attrs = {} # type: Dict[str, Union[str, py.xml.raw]]
+ self.properties = [] # type: List[Tuple[str, str]]
+ self.nodes = [] # type: List[ET.Element]
+ self.attrs = {} # type: Dict[str, str]
- def append(self, node: py.xml.Tag) -> None:
- self.xml.add_stats(type(node).__name__)
+ def append(self, node: ET.Element) -> None:
+ self.xml.add_stats(node.tag)
self.nodes.append(node)
def add_property(self, name: str, value: object) -> None:
def add_attribute(self, name: str, value: object) -> None:
self.attrs[str(name)] = bin_xml_escape(value)
- def make_properties_node(self) -> Union[py.xml.Tag, str]:
- """Return a Junit node containing custom properties, if any.
- """
+ def make_properties_node(self) -> Optional[ET.Element]:
+ """Return a Junit node containing custom properties, if any."""
if self.properties:
- return Junit.properties(
- [
- Junit.property(name=name, value=value)
- for name, value in self.properties
- ]
- )
- return ""
+ properties = ET.Element("properties")
+ for name, value in self.properties:
+ properties.append(ET.Element("property", name=name, value=value))
+ return properties
+ return None
def record_testreport(self, testreport: TestReport) -> None:
names = mangle_test_address(testreport.nodeid)
"classname": ".".join(classnames),
"name": bin_xml_escape(names[-1]),
"file": testreport.location[0],
- } # type: Dict[str, Union[str, py.xml.raw]]
+ } # type: Dict[str, str]
if testreport.location[1] is not None:
attrs["line"] = str(testreport.location[1])
if hasattr(testreport, "url"):
attrs["url"] = testreport.url
self.attrs = attrs
- self.attrs.update(existing_attrs) # restore any user-defined attributes
+ self.attrs.update(existing_attrs) # Restore any user-defined attributes.
- # Preserve legacy testcase behavior
+ # Preserve legacy testcase behavior.
if self.family == "xunit1":
return
temp_attrs[key] = self.attrs[key]
self.attrs = temp_attrs
- def to_xml(self) -> py.xml.Tag:
- testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs)
- testcase.append(self.make_properties_node())
- for node in self.nodes:
- testcase.append(node)
+ def to_xml(self) -> ET.Element:
+ testcase = ET.Element("testcase", self.attrs, time="%.3f" % self.duration)
+ properties = self.make_properties_node()
+ if properties is not None:
+ testcase.append(properties)
+ testcase.extend(self.nodes)
return testcase
- def _add_simple(self, kind: "Type[py.xml.Tag]", message: str, data=None) -> None:
- data = bin_xml_escape(data)
- node = kind(data, message=message)
+ def _add_simple(self, tag: str, message: str, data: Optional[str] = None) -> None:
+ node = ET.Element(tag, message=message)
+ node.text = bin_xml_escape(data)
self.append(node)
def write_captured_output(self, report: TestReport) -> None:
return "\n".join([header.center(80, "-"), content, ""])
def _write_content(self, report: TestReport, content: str, jheader: str) -> None:
- tag = getattr(Junit, jheader)
- self.append(tag(bin_xml_escape(content)))
+ tag = ET.Element(jheader)
+ tag.text = bin_xml_escape(content)
+ self.append(tag)
def append_pass(self, report: TestReport) -> None:
self.add_stats("passed")
def append_failure(self, report: TestReport) -> None:
# msg = str(report.longrepr.reprtraceback.extraline)
if hasattr(report, "wasxfail"):
- self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly")
+ self._add_simple("skipped", "xfail-marked test passes unexpectedly")
else:
assert report.longrepr is not None
- if getattr(report.longrepr, "reprcrash", None) is not None:
- message = report.longrepr.reprcrash.message
+ reprcrash = getattr(
+ report.longrepr, "reprcrash", None
+ ) # type: Optional[ReprFileLocation]
+ if reprcrash is not None:
+ message = reprcrash.message
else:
message = str(report.longrepr)
message = bin_xml_escape(message)
- fail = Junit.failure(message=message)
- fail.append(bin_xml_escape(report.longrepr))
- self.append(fail)
+ self._add_simple("failure", message, str(report.longrepr))
def append_collect_error(self, report: TestReport) -> None:
# msg = str(report.longrepr.reprtraceback.extraline)
assert report.longrepr is not None
- self.append(
- Junit.error(bin_xml_escape(report.longrepr), message="collection failure")
- )
+ self._add_simple("error", "collection failure", str(report.longrepr))
def append_collect_skipped(self, report: TestReport) -> None:
- self._add_simple(Junit.skipped, "collection skipped", report.longrepr)
+ self._add_simple("skipped", "collection skipped", str(report.longrepr))
def append_error(self, report: TestReport) -> None:
assert report.longrepr is not None
- if getattr(report.longrepr, "reprcrash", None) is not None:
- reason = report.longrepr.reprcrash.message
+ reprcrash = getattr(
+ report.longrepr, "reprcrash", None
+ ) # type: Optional[ReprFileLocation]
+ if reprcrash is not None:
+ reason = reprcrash.message
else:
reason = str(report.longrepr)
msg = 'failed on teardown with "{}"'.format(reason)
else:
msg = 'failed on setup with "{}"'.format(reason)
- self._add_simple(Junit.error, msg, report.longrepr)
+ self._add_simple("error", msg, str(report.longrepr))
def append_skipped(self, report: TestReport) -> None:
if hasattr(report, "wasxfail"):
xfailreason = report.wasxfail
if xfailreason.startswith("reason: "):
xfailreason = xfailreason[8:]
- self.append(
- Junit.skipped(
- "", type="pytest.xfail", message=bin_xml_escape(xfailreason)
- )
- )
+ xfailreason = bin_xml_escape(xfailreason)
+ skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason)
+ self.append(skipped)
else:
- assert report.longrepr is not None
+ assert isinstance(report.longrepr, tuple)
filename, lineno, skipreason = report.longrepr
if skipreason.startswith("Skipped: "):
skipreason = skipreason[9:]
details = "{}:{}: {}".format(filename, lineno, skipreason)
- self.append(
- Junit.skipped(
- bin_xml_escape(details),
- type="pytest.skip",
- message=bin_xml_escape(skipreason),
- )
- )
+ skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
+ skipped.text = bin_xml_escape(details)
+ self.append(skipped)
self.write_captured_output(report)
def finalize(self) -> None:
- data = self.to_xml().unicode(indent=0)
+ data = self.to_xml()
self.__dict__.clear()
# Type ignored becuase mypy doesn't like overriding a method.
# Also the return value doesn't match...
- self.to_xml = lambda: py.xml.raw(data) # type: ignore
+ self.to_xml = lambda: data # type: ignore[assignment]
def _warn_incompatibility_with_xunit2(
request: FixtureRequest, fixture_name: str
) -> None:
- """Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions"""
+ """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
from _pytest.warning_types import PytestWarning
xml = request.config._store.get(xml_key, None)
def _check_record_param_type(param: str, v: str) -> None:
"""Used by record_testsuite_property to check that the given parameter name is of the proper
- type"""
+ type."""
__tracebackhide__ = True
if not isinstance(v, str):
- msg = "{param} parameter needs to be a string, but {g} given"
+ msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable]
raise TypeError(msg.format(param=param, g=type(v).__name__))
@pytest.fixture(scope="session")
def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]:
- """
- Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
- writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.
+ """Record a new ``<property>`` tag as child of the root ``<testsuite>``.
+
+ This is suitable to writing global information regarding the entire test
+ suite, and is compatible with ``xunit2`` JUnit family.
This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
record_testsuite_property("STORAGE_TYPE", "CEPH")
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
+
+ .. warning::
+
+ Currently this fixture **does not work** with the
+ `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See issue
+ `#7767 <https://github.com/pytest-dev/pytest/issues/7767>`__ for details.
"""
__tracebackhide__ = True
def record_func(name: str, value: object) -> None:
- """noop function in case --junitxml was not passed in the command-line"""
+ """No-op function in case --junitxml was not passed in the command-line."""
__tracebackhide__ = True
_check_record_param_type("name", name)
default="total",
) # choices=['total', 'call'])
parser.addini(
- "junit_family", "Emit XML for schema: one of legacy|xunit1|xunit2", default=None
+ "junit_family",
+ "Emit XML for schema: one of legacy|xunit1|xunit2",
+ default="xunit2",
)
def pytest_configure(config: Config) -> None:
xmlpath = config.option.xmlpath
- # prevent opening xmllog on worker nodes (xdist)
+ # Prevent opening xmllog on worker nodes (xdist).
if xmlpath and not hasattr(config, "workerinput"):
junit_family = config.getini("junit_family")
- if not junit_family:
- _issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2)
- junit_family = "xunit1"
config._store[xml_key] = LogXML(
xmlpath,
config.option.junitprefix,
names.remove("()")
except ValueError:
pass
- # convert file path to dotted path
+ # Convert file path to dotted path.
names[0] = names[0].replace(nodes.SEP, ".")
- names[0] = _py_ext_re.sub("", names[0])
- # put any params back
+ names[0] = re.sub(r"\.py$", "", names[0])
+ # Put any params back.
names[-1] += possible_open_bracket + params
return names
{}
) # type: Dict[Tuple[Union[str, TestReport], object], _NodeReporter]
self.node_reporters_ordered = [] # type: List[_NodeReporter]
- self.global_properties = [] # type: List[Tuple[str, py.xml.raw]]
+ self.global_properties = [] # type: List[Tuple[str, str]]
# List of reports that failed on call but teardown is pending.
self.open_reports = [] # type: List[TestReport]
self.cnt_double_fail_tests = 0
- # Replaces convenience family with real family
+ # Replaces convenience family with real family.
if self.family == "legacy":
self.family = "xunit1"
def finalize(self, report: TestReport) -> None:
nodeid = getattr(report, "nodeid", report)
- # local hack to handle xdist report order
+ # Local hack to handle xdist report order.
workernode = getattr(report, "node", None)
reporter = self.node_reporters.pop((nodeid, workernode))
if reporter is not None:
def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport]
- # local hack to handle xdist report order
+ # Local hack to handle xdist report order.
workernode = getattr(report, "node", None)
key = nodeid, workernode
return reporter
def pytest_runtest_logreport(self, report: TestReport) -> None:
- """handle a setup/call/teardown report, generating the appropriate
- xml tags as necessary.
+ """Handle a setup/call/teardown report, generating the appropriate
+ XML tags as necessary.
- note: due to plugins like xdist, this hook may be called in interlaced
- order with reports from other nodes. for example:
+ Note: due to plugins like xdist, this hook may be called in interlaced
+ order with reports from other nodes. For example:
- usual call order:
+ Usual call order:
-> setup node1
-> call node1
-> teardown node1
-> call node2
-> teardown node2
- possible call order in xdist:
+ Possible call order in xdist:
-> setup node1
-> call node1
-> setup node2
reporter.append_pass(report)
elif report.failed:
if report.when == "teardown":
- # The following vars are needed when xdist plugin is used
+ # The following vars are needed when xdist plugin is used.
report_wid = getattr(report, "worker_id", None)
report_ii = getattr(report, "item_index", None)
close_report = next(
if close_report:
# We need to open new testcase in case we have failure in
# call and error in teardown in order to follow junit
- # schema
+ # schema.
self.finalize(close_report)
self.cnt_double_fail_tests += 1
reporter = self._opentestcase(report)
self.open_reports.remove(close_report)
def update_testcase_duration(self, report: TestReport) -> None:
- """accumulates total duration for nodeid from given report and updates
- the Junit.testcase with the new total if already created.
- """
+ """Accumulate total duration for nodeid from given report and update
+ the Junit.testcase with the new total if already created."""
if self.report_duration == "total" or report.when == self.report_duration:
reporter = self.node_reporter(report)
reporter.duration += getattr(report, "duration", 0.0)
def pytest_internalerror(self, excrepr: ExceptionRepr) -> None:
reporter = self.node_reporter("internal")
reporter.attrs.update(classname="pytest", name="internal")
- reporter._add_simple(Junit.error, "internal error", excrepr)
+ reporter._add_simple("error", "internal error", str(excrepr))
def pytest_sessionstart(self) -> None:
self.suite_start_time = timing.time()
)
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
- suite_node = Junit.testsuite(
- self._get_global_properties_node(),
- [x.to_xml() for x in self.node_reporters_ordered],
+ suite_node = ET.Element(
+ "testsuite",
name=self.suite_name,
errors=str(self.stats["error"]),
failures=str(self.stats["failure"]),
timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(),
hostname=platform.node(),
)
- logfile.write(Junit.testsuites([suite_node]).unicode(indent=0))
+ global_properties = self._get_global_properties_node()
+ if global_properties is not None:
+ suite_node.append(global_properties)
+ for node_reporter in self.node_reporters_ordered:
+ suite_node.append(node_reporter.to_xml())
+ testsuites = ET.Element("testsuites")
+ testsuites.append(suite_node)
+ logfile.write(ET.tostring(testsuites, encoding="unicode"))
logfile.close()
def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
_check_record_param_type("name", name)
self.global_properties.append((name, bin_xml_escape(value)))
- def _get_global_properties_node(self) -> Union[py.xml.Tag, str]:
- """Return a Junit node containing custom properties, if any.
- """
+ def _get_global_properties_node(self) -> Optional[ET.Element]:
+ """Return a Junit node containing custom properties, if any."""
if self.global_properties:
- return Junit.properties(
- [
- Junit.property(name=name, value=value)
- for name, value in self.global_properties
- ]
- )
- return ""
+ properties = ET.Element("properties")
+ for name, value in self.global_properties:
+ properties.append(ET.Element("property", name=name, value=value))
+ return properties
+ return None
-""" Access and control log capturing. """
+"""Access and control log capturing."""
import logging
import os
import re
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.capture import CaptureManager
+from _pytest.compat import final
from _pytest.compat import nullcontext
from _pytest.config import _strtobool
from _pytest.config import Config
class ColoredLevelFormatter(logging.Formatter):
- """
- Colorize the %(levelname)..s part of the log format passed to __init__.
- """
+ """A logging formatter which colorizes the %(levelname)..s part of the
+ log format passed to __init__."""
LOGLEVEL_COLOROPTS = {
logging.CRITICAL: {"red"},
@staticmethod
def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int:
- """Determines the current auto indentation setting
+ """Determine the current auto indentation setting.
Specify auto indent behavior (on/off/fixed) by passing in
extra={"auto_indent": [value]} to the call to logging.log() or
Any other values for the option are invalid, and will silently be
converted to the default.
- :param any auto_indent_option: User specified option for indentation
- from command line, config or extra kwarg. Accepts int, bool or str.
- str option accepts the same range of values as boolean config options,
- as well as positive integers represented in str form.
+ :param None|bool|int|str auto_indent_option:
+ User specified option for indentation from command line, config
+ or extra kwarg. Accepts int, bool or str. str option accepts the
+ same range of values as boolean config options, as well as
+ positive integers represented in str form.
- :returns: indentation value, which can be
+ :returns:
+ Indentation value, which can be
-1 (automatically determine indentation) or
0 (auto-indent turned off) or
>0 (explicitly set indentation position).
def format(self, record: logging.LogRecord) -> str:
if "\n" in record.message:
if hasattr(record, "auto_indent"):
- # passed in from the "extra={}" kwarg on the call to logging.log()
+ # Passed in from the "extra={}" kwarg on the call to logging.log().
auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined]
else:
auto_indent = self._auto_indent
lines[0]
)
else:
- # optimizes logging by allowing a fixed indentation
+ # Optimizes logging by allowing a fixed indentation.
indentation = auto_indent
lines[0] = formatted
return ("\n" + " " * indentation).join(lines)
stream = None # type: StringIO
def __init__(self) -> None:
- """Creates a new log handler."""
+ """Create a new log handler."""
super().__init__(StringIO())
self.records = [] # type: List[logging.LogRecord]
raise
+@final
class LogCaptureFixture:
"""Provides access and control of log capturing."""
def __init__(self, item: nodes.Node) -> None:
- """Creates a new funcarg."""
self._item = item
- # dict of log name -> log level
self._initial_handler_level = None # type: Optional[int]
+ # Dict of log name -> log level.
self._initial_logger_levels = {} # type: Dict[Optional[str], int]
def _finalize(self) -> None:
- """Finalizes the fixture.
+ """Finalize the fixture.
This restores the log levels changed by :meth:`set_level`.
"""
- # restore log levels
+ # Restore log levels.
if self._initial_handler_level is not None:
self.handler.setLevel(self._initial_handler_level)
for logger_name, level in self._initial_logger_levels.items():
@property
def handler(self) -> LogCaptureHandler:
- """
+ """Get the logging handler used by the fixture.
+
:rtype: LogCaptureHandler
"""
return self._item._store[caplog_handler_key]
def get_records(self, when: str) -> List[logging.LogRecord]:
- """
- Get the logging records for one of the possible test phases.
+ """Get the logging records for one of the possible test phases.
:param str when:
Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
+ :returns: The list of captured records at the given stage.
:rtype: List[logging.LogRecord]
- :return: the list of captured records at the given stage
.. versionadded:: 3.4
"""
@property
def text(self) -> str:
- """Returns the formatted log text."""
+ """The formatted log text."""
return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
@property
def records(self) -> List[logging.LogRecord]:
- """Returns the list of log records."""
+ """The list of log records."""
return self.handler.records
@property
def record_tuples(self) -> List[Tuple[str, int, str]]:
- """Returns a list of a stripped down version of log records intended
+ """A list of a stripped down version of log records intended
for use in assertion comparison.
The format of the tuple is:
@property
def messages(self) -> List[str]:
- """Returns a list of format-interpolated log messages.
+ """A list of format-interpolated log messages.
+
+ Unlike 'records', which contains the format string and parameters for
+ interpolation, log messages in this list are all interpolated.
- Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
- are all interpolated.
- Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
- levels, timestamps, etc, making exact comparisons more reliable.
+ Unlike 'text', which contains the output from the handler, log
+ messages in this list are unadorned with levels, timestamps, etc,
+ making exact comparisons more reliable.
- Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
- to the logging functions) is not included, as this is added by the formatter in the handler.
+ Note that traceback or stack info (from :func:`logging.exception` or
+ the `exc_info` or `stack_info` arguments to the logging functions) is
+ not included, as this is added by the formatter in the handler.
.. versionadded:: 3.7
"""
self.handler.reset()
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
- """Sets the level for capturing of logs. The level will be restored to its previous value at the end of
- the test.
-
- :param int level: the logger to level.
- :param str logger: the logger to update the level. If not given, the root logger level is updated.
+ """Set the level of a logger for the duration of a test.
.. versionchanged:: 3.4
- The levels of the loggers changed by this function will be restored to their initial values at the
- end of the test.
+ The levels of the loggers changed by this function will be
+ restored to their initial values at the end of the test.
+
+ :param int level: The level.
+ :param str logger: The logger to update. If not given, the root logger.
"""
logger_obj = logging.getLogger(logger)
- # save the original log-level to restore it during teardown
+ # Save the original log-level to restore it during teardown.
self._initial_logger_levels.setdefault(logger, logger_obj.level)
logger_obj.setLevel(level)
if self._initial_handler_level is None:
def at_level(
self, level: int, logger: Optional[str] = None
) -> Generator[None, None, None]:
- """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
- level is restored to its original value.
+ """Context manager that sets the level for capturing of logs. After
+ the end of the 'with' statement the level is restored to its original
+ value.
- :param int level: the logger to level.
- :param str logger: the logger to update the level. If not given, the root logger level is updated.
+ :param int level: The level.
+ :param str logger: The logger to update. If not given, the root logger.
"""
logger_obj = logging.getLogger(logger)
orig_level = logger_obj.level
class LoggingPlugin:
- """Attaches to the logging module and captures log messages for each test.
- """
+ """Attaches to the logging module and captures log messages for each test."""
def __init__(self, config: Config) -> None:
- """Creates a new plugin to capture log messages.
+ """Create a new plugin to capture log messages.
The formatter can be safely shared across all handlers so
create a single one for the entire test session here.
self.log_cli_handler.setFormatter(log_cli_formatter)
def _create_formatter(self, log_format, log_date_format, auto_indent):
- # color option doesn't exist if terminal plugin is disabled
+ # Color option doesn't exist if terminal plugin is disabled.
color = getattr(self._config.option, "color", "no")
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
log_format
return formatter
def set_log_path(self, fname: str) -> None:
- """Public method, which can set filename parameter for
- Logging.FileHandler(). Also creates parent directory if
- it does not exist.
+ """Set the filename parameter for Logging.FileHandler().
+
+ Creates parent directory if it does not exist.
.. warning::
- Please considered as an experimental API.
+ This is an experimental API.
"""
fpath = Path(fname)
if not fpath.is_absolute():
- fpath = Path(str(self._config.rootdir), fpath)
+ fpath = self._config.rootpath / fpath
if not fpath.parent.exists():
fpath.parent.mkdir(exist_ok=True, parents=True)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
- """Runs all collected test items."""
-
if session.config.option.collectonly:
yield
return
if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
- # setting verbose flag is needed to avoid messy test progress output
+ # The verbose flag is needed to avoid messy test progress output.
self._config.option.verbose = 1
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
- yield # run all the tests
+ yield # Run all the tests.
@pytest.hookimpl
def pytest_runtest_logstart(self) -> None:
self.log_cli_handler.set_when("logreport")
def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]:
- """Implements the internals of pytest_runtest_xxx() hook."""
+ """Implement the internals of the pytest_runtest_xxx() hooks."""
with catching_logs(
self.caplog_handler, level=self.log_level,
) as caplog_handler, catching_logs(
class _FileHandler(logging.FileHandler):
- """
- Custom FileHandler with pytest tweaks.
- """
+ """A logging FileHandler with pytest tweaks."""
def handleError(self, record: logging.LogRecord) -> None:
# Handled by LogCaptureHandler.
class _LiveLoggingStreamHandler(logging.StreamHandler):
- """
- Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
- in each test.
+ """A logging StreamHandler used by the live logging feature: it will
+ write a newline before the first log message in each test.
- During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
- and won't appear in the terminal.
+ During live logging we must also explicitly disable stdout/stderr
+ capturing otherwise it will get captured and won't appear in the
+ terminal.
"""
# Officially stream needs to be a IO[str], but TerminalReporter
terminal_reporter: TerminalReporter,
capture_manager: Optional[CaptureManager],
) -> None:
- """
- :param _pytest.terminal.TerminalReporter terminal_reporter:
- :param _pytest.capture.CaptureManager capture_manager:
- """
logging.StreamHandler.__init__(self, stream=terminal_reporter) # type: ignore[arg-type]
self.capture_manager = capture_manager
self.reset()
self._test_outcome_written = False
def reset(self) -> None:
- """Reset the handler; should be called before the start of each test"""
+ """Reset the handler; should be called before the start of each test."""
self._first_record_emitted = False
def set_when(self, when: Optional[str]) -> None:
- """Prepares for the given test phase (setup/call/teardown)"""
+ """Prepare for the given test phase (setup/call/teardown)."""
self._when = when
self._section_name_shown = False
if when == "start":
class _LiveLoggingNullHandler(logging.NullHandler):
- """A handler used when live logging is disabled."""
+ """A logging handler used when live logging is disabled."""
def reset(self) -> None:
pass
-""" core implementation of testing process: init, session, runtest loop. """
+"""Core implementation of the testing process: init, session, runtest loop."""
import argparse
import fnmatch
import functools
import _pytest._code
from _pytest import nodes
+from _pytest.compat import final
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import directory_arg
from _pytest.config import ExitCode
from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
from _pytest.pathlib import Path
+from _pytest.pathlib import visit
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
from _pytest.runner import collect_one_node
from typing import Type
from typing_extensions import Literal
- from _pytest.python import Package
-
def pytest_addoption(parser: Parser) -> None:
parser.addini(
const=1,
help="exit instantly on first error or failed test.",
)
+ group = parser.getgroup("pytest-warnings")
+ group.addoption(
+ "-W",
+ "--pythonwarnings",
+ action="append",
+ help="set which warnings to report, see -W option of python itself.",
+ )
+ parser.addini(
+ "filterwarnings",
+ type="linelist",
+ help="Each line specifies a pattern for "
+ "warnings.filterwarnings. "
+ "Processed after -W/--pythonwarnings.",
+ )
group._addoption(
"--maxfail",
metavar="num",
raise argparse.ArgumentTypeError(msg)
def is_ancestor(base: Path, query: Path) -> bool:
- """ return True if query is an ancestor of base, else False."""
+ """Return whether query is an ancestor of base."""
if base == query:
return True
for parent in base.parents:
def wrap_session(
config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
) -> Union[int, ExitCode]:
- """Skeleton command line program"""
+ """Skeleton command line program."""
session = Session.from_config(config)
session.exitstatus = ExitCode.OK
initstate = 0
def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
- """ default command line protocol for initialization, session,
- running tests and reporting. """
+ """Default command line protocol for initialization, session,
+ running tests and reporting."""
config.hook.pytest_collection(session=session)
config.hook.pytest_runtestloop(session=session)
def _in_venv(path: py.path.local) -> bool:
- """Attempts to detect if ``path`` is the root of a Virtual Environment by
- checking for the existence of the appropriate activate script"""
+ """Attempt to detect if ``path`` is the root of a Virtual Environment by
+ checking for the existence of the appropriate activate script."""
bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin")
if not bindir.isdir():
return False
items[:] = remaining
-class NoMatch(Exception):
- """ raised if matching cannot locate a matching names. """
+class FSHookProxy:
+ def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
+ self.pm = pm
+ self.remove_mods = remove_mods
+
+ def __getattr__(self, name: str):
+ x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
+ self.__dict__[name] = x
+ return x
class Interrupted(KeyboardInterrupt):
- """ signals an interrupted test run. """
+ """Signals that the test run was interrupted."""
- __module__ = "builtins" # for py3
+ __module__ = "builtins" # For py3.
class Failed(Exception):
- """ signals a stop as failed test run. """
+ """Signals a stop as failed test run."""
@attr.s
-class _bestrelpath_cache(dict):
- path = attr.ib(type=py.path.local)
+class _bestrelpath_cache(Dict[Path, str]):
+ path = attr.ib(type=Path)
- def __missing__(self, path: py.path.local) -> str:
- r = self.path.bestrelpath(path) # type: str
+ def __missing__(self, path: Path) -> str:
+ r = bestrelpath(self.path, path)
self[path] = r
return r
+@final
class Session(nodes.FSCollector):
Interrupted = Interrupted
Failed = Failed
exitstatus = None # type: Union[int, ExitCode]
def __init__(self, config: Config) -> None:
- nodes.FSCollector.__init__(
- self, config.rootdir, parent=None, config=config, session=self, nodeid=""
+ super().__init__(
+ config.rootdir, parent=None, config=config, session=self, nodeid=""
)
self.testsfailed = 0
self.testscollected = 0
self.startdir = config.invocation_dir
self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
- # Keep track of any collected nodes in here, so we don't duplicate fixtures
- self._collection_node_cache1 = (
- {}
- ) # type: Dict[py.path.local, Sequence[nodes.Collector]]
- self._collection_node_cache2 = (
- {}
- ) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector]
- self._collection_node_cache3 = (
- {}
- ) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport]
-
- # Dirnames of pkgs with dunder-init files.
- self._collection_pkg_roots = {} # type: Dict[str, Package]
-
self._bestrelpathcache = _bestrelpath_cache(
- config.rootdir
- ) # type: Dict[py.path.local, str]
+ config.rootpath
+ ) # type: Dict[Path, str]
self.config.pluginmanager.register(self, name="session")
self.testscollected,
)
- def _node_location_to_relpath(self, node_path: py.path.local) -> str:
- # bestrelpath is a quite slow function
+ def _node_location_to_relpath(self, node_path: Path) -> str:
+ # bestrelpath is a quite slow function.
return self._bestrelpathcache[node_path]
@hookimpl(tryfirst=True)
return path in self._initialpaths
def gethookproxy(self, fspath: py.path.local):
- return super()._gethookproxy(fspath)
+ # Check if we have the common case of running
+ # hooks with all conftest.py files.
+ pm = self.config.pluginmanager
+ my_conftestmodules = pm._getconftestmodules(
+ fspath, self.config.getoption("importmode")
+ )
+ remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
+ if remove_mods:
+ # One or more conftests are not in use at this fspath.
+ proxy = FSHookProxy(pm, remove_mods)
+ else:
+ # All plugins are active for this fspath.
+ proxy = self.config.hook
+ return proxy
+
+ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
+ if direntry.name == "__pycache__":
+ return False
+ path = py.path.local(direntry.path)
+ ihook = self.gethookproxy(path.dirpath())
+ if ihook.pytest_ignore_collect(path=path, config=self.config):
+ return False
+ norecursepatterns = self.config.getini("norecursedirs")
+ if any(path.check(fnmatch=pat) for pat in norecursepatterns):
+ return False
+ return True
+
+ def _collectfile(
+ self, path: py.path.local, handle_dupes: bool = True
+ ) -> Sequence[nodes.Collector]:
+ assert (
+ path.isfile()
+ ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
+ path, path.isdir(), path.exists(), path.islink()
+ )
+ ihook = self.gethookproxy(path)
+ if not self.isinitpath(path):
+ if ihook.pytest_ignore_collect(path=path, config=self.config):
+ return ()
+
+ if handle_dupes:
+ keepduplicates = self.config.getoption("keepduplicates")
+ if not keepduplicates:
+ duplicate_paths = self.config.pluginmanager._duplicatepaths
+ if path in duplicate_paths:
+ return ()
+ else:
+ duplicate_paths.add(path)
+
+ return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return]
@overload
def perform_collect(
self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
) -> Sequence[nodes.Item]:
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def perform_collect( # noqa: F811
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
- raise NotImplementedError()
+ ...
def perform_collect( # noqa: F811
self, args: Optional[Sequence[str]] = None, genitems: bool = True
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
+ """Perform the collection phase for this session.
+
+ This is called by the default
+ :func:`pytest_collection <_pytest.hookspec.pytest_collection>` hook
+ implementation; see the documentation of this hook for more details.
+ For testing purposes, it may also be called directly on a fresh
+ ``Session``.
+
+ This function normally recursively expands any collectors collected
+ from the session to their items, and only items are returned. For
+ testing purposes, this may be suppressed by passing ``genitems=False``,
+ in which case the return value contains these collectors unexpanded,
+ and ``session.items`` is empty.
+ """
+ if args is None:
+ args = self.config.args
+
+ self.trace("perform_collect", self, args)
+ self.trace.root.indent += 1
+
+ self._notfound = [] # type: List[Tuple[str, Sequence[nodes.Collector]]]
+ self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]]
+ self.items = [] # type: List[nodes.Item]
+
hook = self.config.hook
+
+ items = self.items # type: Sequence[Union[nodes.Item, nodes.Collector]]
try:
- items = self._perform_collect(args, genitems)
+ initialpaths = [] # type: List[py.path.local]
+ for arg in args:
+ fspath, parts = resolve_collection_argument(
+ self.config.invocation_params.dir,
+ arg,
+ as_pypath=self.config.option.pyargs,
+ )
+ self._initial_parts.append((fspath, parts))
+ initialpaths.append(fspath)
+ self._initialpaths = frozenset(initialpaths)
+ rep = collect_one_node(self)
+ self.ihook.pytest_collectreport(report=rep)
+ self.trace.root.indent -= 1
+ if self._notfound:
+ errors = []
+ for arg, cols in self._notfound:
+ line = "(no name {!r} in any of {!r})".format(arg, cols)
+ errors.append("not found: {}\n{}".format(arg, line))
+ raise UsageError(*errors)
+ if not genitems:
+ items = rep.result
+ else:
+ if rep.passed:
+ for node in rep.result:
+ self.items.extend(self.genitems(node))
+
self.config.pluginmanager.check_pending()
hook.pytest_collection_modifyitems(
session=self, config=self.config, items=items
)
finally:
hook.pytest_collection_finish(session=self)
+
self.testscollected = len(items)
return items
- @overload
- def _perform_collect(
- self, args: Optional[Sequence[str]], genitems: "Literal[True]"
- ) -> List[nodes.Item]:
- raise NotImplementedError()
+ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
+ from _pytest.python import Package
- @overload # noqa: F811
- def _perform_collect( # noqa: F811
- self, args: Optional[Sequence[str]], genitems: bool
- ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]:
- raise NotImplementedError()
-
- def _perform_collect( # noqa: F811
- self, args: Optional[Sequence[str]], genitems: bool
- ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]:
- if args is None:
- args = self.config.args
- self.trace("perform_collect", self, args)
- self.trace.root.indent += 1
- self._notfound = [] # type: List[Tuple[str, NoMatch]]
- initialpaths = [] # type: List[py.path.local]
- self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]]
- self.items = items = [] # type: List[nodes.Item]
- for arg in args:
- fspath, parts = self._parsearg(arg)
- self._initial_parts.append((fspath, parts))
- initialpaths.append(fspath)
- self._initialpaths = frozenset(initialpaths)
- rep = collect_one_node(self)
- self.ihook.pytest_collectreport(report=rep)
- self.trace.root.indent -= 1
- if self._notfound:
- errors = []
- for arg, exc in self._notfound:
- line = "(no name {!r} in any of {!r})".format(arg, exc.args[0])
- errors.append("not found: {}\n{}".format(arg, line))
- raise UsageError(*errors)
- if not genitems:
- return rep.result
- else:
- if rep.passed:
- for node in rep.result:
- self.items.extend(self.genitems(node))
- return items
+ # Keep track of any collected nodes in here, so we don't duplicate fixtures.
+ node_cache1 = {} # type: Dict[py.path.local, Sequence[nodes.Collector]]
+ node_cache2 = (
+ {}
+ ) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector]
- def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
- for fspath, parts in self._initial_parts:
- self.trace("processing argument", (fspath, parts))
- self.trace.root.indent += 1
- try:
- yield from self._collect(fspath, parts)
- except NoMatch as exc:
- report_arg = "::".join((str(fspath), *parts))
- # we are inside a make_report hook so
- # we cannot directly pass through the exception
- self._notfound.append((report_arg, exc))
+ # Keep track of any collected collectors in matchnodes paths, so they
+ # are not collected more than once.
+ matchnodes_cache = (
+ {}
+ ) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport]
- self.trace.root.indent -= 1
- self._collection_node_cache1.clear()
- self._collection_node_cache2.clear()
- self._collection_node_cache3.clear()
- self._collection_pkg_roots.clear()
-
- def _collect(
- self, argpath: py.path.local, names: List[str]
- ) -> Iterator[Union[nodes.Item, nodes.Collector]]:
- from _pytest.python import Package
+ # Dirnames of pkgs with dunder-init files.
+ pkg_roots = {} # type: Dict[str, Package]
+
+ for argpath, names in self._initial_parts:
+ self.trace("processing argument", (argpath, names))
+ self.trace.root.indent += 1
- # Start with a Session root, and delve to argpath item (dir or file)
- # and stack all Packages found on the way.
- # No point in finding packages when collecting doctests
- if not self.config.getoption("doctestmodules", False):
- pm = self.config.pluginmanager
- for parent in reversed(argpath.parts()):
- if pm._confcutdir and pm._confcutdir.relto(parent):
- break
-
- if parent.isdir():
- pkginit = parent.join("__init__.py")
- if pkginit.isfile():
- if pkginit not in self._collection_node_cache1:
+ # Start with a Session root, and delve to argpath item (dir or file)
+ # and stack all Packages found on the way.
+ # No point in finding packages when collecting doctests.
+ if not self.config.getoption("doctestmodules", False):
+ pm = self.config.pluginmanager
+ for parent in reversed(argpath.parts()):
+ if pm._confcutdir and pm._confcutdir.relto(parent):
+ break
+
+ if parent.isdir():
+ pkginit = parent.join("__init__.py")
+ if pkginit.isfile() and pkginit not in node_cache1:
col = self._collectfile(pkginit, handle_dupes=False)
if col:
if isinstance(col[0], Package):
- self._collection_pkg_roots[str(parent)] = col[0]
- # always store a list in the cache, matchnodes expects it
- self._collection_node_cache1[col[0].fspath] = [col[0]]
-
- # If it's a directory argument, recurse and look for any Subpackages.
- # Let the Package collector deal with subnodes, don't collect here.
- if argpath.check(dir=1):
- assert not names, "invalid arg {!r}".format((argpath, names))
-
- seen_dirs = set() # type: Set[py.path.local]
- for path in argpath.visit(
- fil=self._visit_filter, rec=self._recurse, bf=True, sort=True
- ):
- dirpath = path.dirpath()
- if dirpath not in seen_dirs:
- # Collect packages first.
- seen_dirs.add(dirpath)
- pkginit = dirpath.join("__init__.py")
- if pkginit.exists():
- for x in self._collectfile(pkginit):
+ pkg_roots[str(parent)] = col[0]
+ node_cache1[col[0].fspath] = [col[0]]
+
+ # If it's a directory argument, recurse and look for any Subpackages.
+ # Let the Package collector deal with subnodes, don't collect here.
+ if argpath.check(dir=1):
+ assert not names, "invalid arg {!r}".format((argpath, names))
+
+ seen_dirs = set() # type: Set[py.path.local]
+ for direntry in visit(str(argpath), self._recurse):
+ if not direntry.is_file():
+ continue
+
+ path = py.path.local(direntry.path)
+ dirpath = path.dirpath()
+
+ if dirpath not in seen_dirs:
+ # Collect packages first.
+ seen_dirs.add(dirpath)
+ pkginit = dirpath.join("__init__.py")
+ if pkginit.exists():
+ for x in self._collectfile(pkginit):
+ yield x
+ if isinstance(x, Package):
+ pkg_roots[str(dirpath)] = x
+ if str(dirpath) in pkg_roots:
+ # Do not collect packages here.
+ continue
+
+ for x in self._collectfile(path):
+ key = (type(x), x.fspath)
+ if key in node_cache2:
+ yield node_cache2[key]
+ else:
+ node_cache2[key] = x
yield x
- if isinstance(x, Package):
- self._collection_pkg_roots[str(dirpath)] = x
- if str(dirpath) in self._collection_pkg_roots:
- # Do not collect packages here.
+ else:
+ assert argpath.check(file=1)
+
+ if argpath in node_cache1:
+ col = node_cache1[argpath]
+ else:
+ collect_root = pkg_roots.get(argpath.dirname, self)
+ col = collect_root._collectfile(argpath, handle_dupes=False)
+ if col:
+ node_cache1[argpath] = col
+
+ matching = []
+ work = [
+ (col, names)
+ ] # type: List[Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]]
+ while work:
+ self.trace("matchnodes", col, names)
+ self.trace.root.indent += 1
+
+ matchnodes, matchnames = work.pop()
+ for node in matchnodes:
+ if not matchnames:
+ matching.append(node)
+ continue
+ if not isinstance(node, nodes.Collector):
+ continue
+ key = (type(node), node.nodeid)
+ if key in matchnodes_cache:
+ rep = matchnodes_cache[key]
+ else:
+ rep = collect_one_node(node)
+ matchnodes_cache[key] = rep
+ if rep.passed:
+ submatchnodes = []
+ for r in rep.result:
+ # TODO: Remove parametrized workaround once collection structure contains
+ # parametrization.
+ if (
+ r.name == matchnames[0]
+ or r.name.split("[")[0] == matchnames[0]
+ ):
+ submatchnodes.append(r)
+ if submatchnodes:
+ work.append((submatchnodes, matchnames[1:]))
+ # XXX Accept IDs that don't have "()" for class instances.
+ elif len(rep.result) == 1 and rep.result[0].name == "()":
+ work.append((rep.result, matchnames))
+ else:
+ # Report collection failures here to avoid failing to run some test
+ # specified in the command line because the module could not be
+ # imported (#134).
+ node.ihook.pytest_collectreport(report=rep)
+
+ self.trace("matchnodes finished -> ", len(matching), "nodes")
+ self.trace.root.indent -= 1
+
+ if not matching:
+ report_arg = "::".join((str(argpath), *names))
+ self._notfound.append((report_arg, col))
continue
- for x in self._collectfile(path):
- key = (type(x), x.fspath)
- if key in self._collection_node_cache2:
- yield self._collection_node_cache2[key]
- else:
- self._collection_node_cache2[key] = x
- yield x
- else:
- assert argpath.check(file=1)
+ # If __init__.py was the only file requested, then the matched node will be
+ # the corresponding Package, and the first yielded item will be the __init__
+ # Module itself, so just use that. If this special case isn't taken, then all
+ # the files in the package will be yielded.
+ if argpath.basename == "__init__.py":
+ assert isinstance(matching[0], nodes.Collector)
+ try:
+ yield next(iter(matching[0].collect()))
+ except StopIteration:
+ # The package collects nothing with only an __init__.py
+ # file in it, which gets ignored by the default
+ # "python_files" option.
+ pass
+ continue
- if argpath in self._collection_node_cache1:
- col = self._collection_node_cache1[argpath]
- else:
- collect_root = self._collection_pkg_roots.get(argpath.dirname, self)
- col = collect_root._collectfile(argpath, handle_dupes=False)
- if col:
- self._collection_node_cache1[argpath] = col
- m = self.matchnodes(col, names)
- # If __init__.py was the only file requested, then the matched node will be
- # the corresponding Package, and the first yielded item will be the __init__
- # Module itself, so just use that. If this special case isn't taken, then all
- # the files in the package will be yielded.
- if argpath.basename == "__init__.py":
- assert isinstance(m[0], nodes.Collector)
- try:
- yield next(iter(m[0].collect()))
- except StopIteration:
- # The package collects nothing with only an __init__.py
- # file in it, which gets ignored by the default
- # "python_files" option.
- pass
- return
- yield from m
-
- @staticmethod
- def _visit_filter(f: py.path.local) -> bool:
- # TODO: Remove type: ignore once `py` is typed.
- return f.check(file=1) # type: ignore
-
- def _tryconvertpyarg(self, x: str) -> str:
- """Convert a dotted module name to path."""
- try:
- spec = importlib.util.find_spec(x)
- # AttributeError: looks like package module, but actually filename
- # ImportError: module does not exist
- # ValueError: not a module name
- except (AttributeError, ImportError, ValueError):
- return x
- if spec is None or spec.origin is None or spec.origin == "namespace":
- return x
- elif spec.submodule_search_locations:
- return os.path.dirname(spec.origin)
- else:
- return spec.origin
-
- def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]:
- """ return (fspath, names) tuple after checking the file exists. """
- strpath, *parts = str(arg).split("::")
- if self.config.option.pyargs:
- strpath = self._tryconvertpyarg(strpath)
- relpath = strpath.replace("/", os.sep)
- fspath = self.config.invocation_dir.join(relpath, abs=True)
- if not fspath.check():
- if self.config.option.pyargs:
- raise UsageError(
- "file or package not found: " + arg + " (missing __init__.py?)"
- )
- raise UsageError("file not found: " + arg)
- return (fspath, parts)
+ yield from matching
- def matchnodes(
- self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str],
- ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
- self.trace("matchnodes", matching, names)
- self.trace.root.indent += 1
- nodes = self._matchnodes(matching, names)
- num = len(nodes)
- self.trace("matchnodes finished -> ", num, "nodes")
- self.trace.root.indent -= 1
- if num == 0:
- raise NoMatch(matching, names[:1])
- return nodes
-
- def _matchnodes(
- self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str],
- ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
- if not matching or not names:
- return matching
- name = names[0]
- assert name
- nextnames = names[1:]
- resultnodes = [] # type: List[Union[nodes.Item, nodes.Collector]]
- for node in matching:
- if isinstance(node, nodes.Item):
- if not names:
- resultnodes.append(node)
- continue
- assert isinstance(node, nodes.Collector)
- key = (type(node), node.nodeid)
- if key in self._collection_node_cache3:
- rep = self._collection_node_cache3[key]
- else:
- rep = collect_one_node(node)
- self._collection_node_cache3[key] = rep
- if rep.passed:
- has_matched = False
- for x in rep.result:
- # TODO: remove parametrized workaround once collection structure contains parametrization
- if x.name == name or x.name.split("[")[0] == name:
- resultnodes.extend(self.matchnodes([x], nextnames))
- has_matched = True
- # XXX accept IDs that don't have "()" for class instances
- if not has_matched and len(rep.result) == 1 and x.name == "()":
- nextnames.insert(0, name)
- resultnodes.extend(self.matchnodes([x], nextnames))
- else:
- # report collection failures here to avoid failing to run some test
- # specified in the command line because the module could not be
- # imported (#134)
- node.ihook.pytest_collectreport(report=rep)
- return resultnodes
+ self.trace.root.indent -= 1
def genitems(
self, node: Union[nodes.Item, nodes.Collector]
for subnode in rep.result:
yield from self.genitems(subnode)
node.ihook.pytest_collectreport(report=rep)
+
+
+def search_pypath(module_name: str) -> str:
+ """Search sys.path for the given a dotted module name, and return its file system path."""
+ try:
+ spec = importlib.util.find_spec(module_name)
+ # AttributeError: looks like package module, but actually filename
+ # ImportError: module does not exist
+ # ValueError: not a module name
+ except (AttributeError, ImportError, ValueError):
+ return module_name
+ if spec is None or spec.origin is None or spec.origin == "namespace":
+ return module_name
+ elif spec.submodule_search_locations:
+ return os.path.dirname(spec.origin)
+ else:
+ return spec.origin
+
+
+def resolve_collection_argument(
+ invocation_path: Path, arg: str, *, as_pypath: bool = False
+) -> Tuple[py.path.local, List[str]]:
+ """Parse path arguments optionally containing selection parts and return (fspath, names).
+
+ Command-line arguments can point to files and/or directories, and optionally contain
+ parts for specific tests selection, for example:
+
+ "pkg/tests/test_foo.py::TestClass::test_foo"
+
+ This function ensures the path exists, and returns a tuple:
+
+ (py.path.path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
+
+ When as_pypath is True, expects that the command-line argument actually contains
+ module paths instead of file-system paths:
+
+ "pkg.tests.test_foo::TestClass::test_foo"
+
+ In which case we search sys.path for a matching module, and then return the *path* to the
+ found module.
+
+ If the path doesn't exist, raise UsageError.
+ If the path is a directory and selection parts are present, raise UsageError.
+ """
+ strpath, *parts = str(arg).split("::")
+ if as_pypath:
+ strpath = search_pypath(strpath)
+ fspath = invocation_path / strpath
+ fspath = absolutepath(fspath)
+ if not fspath.exists():
+ msg = (
+ "module or package not found: {arg} (missing __init__.py?)"
+ if as_pypath
+ else "file or directory not found: {arg}"
+ )
+ raise UsageError(msg.format(arg=arg))
+ if parts and fspath.is_dir():
+ msg = (
+ "package argument cannot contain :: selection parts: {arg}"
+ if as_pypath
+ else "directory argument cannot contain :: selection parts: {arg}"
+ )
+ raise UsageError(msg.format(arg=arg))
+ return py.path.local(str(fspath)), parts
-""" generic mechanism for marking and selecting python functions. """
+"""Generic mechanism for marking and selecting python functions."""
import typing
+import warnings
from typing import AbstractSet
from typing import List
from typing import Optional
from _pytest.config import hookimpl
from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
+from _pytest.deprecated import MINUS_K_COLON
+from _pytest.deprecated import MINUS_K_DASH
from _pytest.store import StoreKey
if TYPE_CHECKING:
from _pytest.nodes import Item
-__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
+__all__ = [
+ "MARK_GEN",
+ "Mark",
+ "MarkDecorator",
+ "MarkGenerator",
+ "ParameterSet",
+ "get_empty_parameterset_mark",
+]
old_mark_config_key = StoreKey[Optional[Config]]()
def test_eval(test_input, expected):
assert eval(test_input) == expected
- :param values: variable args of the values of the parameter set, in order.
- :keyword marks: a single mark or a list of marks to be applied to this parameter set.
- :keyword str id: the id to attribute to this parameter set.
+ :param values: Variable args of the values of the parameter set, in order.
+ :keyword marks: A single mark or a list of marks to be applied to this parameter set.
+ :keyword str id: The id to attribute to this parameter set.
"""
return ParameterSet.param(*values, marks=marks, id=id)
def from_item(cls, item: "Item") -> "KeywordMatcher":
mapped_names = set()
- # Add the names of the current item and any parent items
+ # Add the names of the current item and any parent items.
import pytest
for node in item.listchain():
if not isinstance(node, (pytest.Instance, pytest.Session)):
mapped_names.add(node.name)
- # Add the names added as extra keywords to current or parent items
+ # Add the names added as extra keywords to current or parent items.
mapped_names.update(item.listextrakeywords())
- # Add the names attached to the current function through direct assignment
+ # Add the names attached to the current function through direct assignment.
function_obj = getattr(item, "function", None)
if function_obj:
mapped_names.update(function_obj.__dict__)
- # add the markers to the keywords as we no longer handle them correctly
+ # Add the markers to the keywords as we no longer handle them correctly.
mapped_names.update(mark.name for mark in item.iter_markers())
return cls(mapped_names)
if keywordexpr.startswith("-"):
# To be removed in pytest 7.0.0.
- # Uncomment this after 6.0 release (#7361)
- # warnings.warn(MINUS_K_DASH, stacklevel=2)
+ warnings.warn(MINUS_K_DASH, stacklevel=2)
keywordexpr = "not " + keywordexpr[1:]
selectuntil = False
if keywordexpr[-1:] == ":":
# To be removed in pytest 7.0.0.
- # Uncomment this after 6.0 release (#7361)
- # warnings.warn(MINUS_K_COLON, stacklevel=2)
+ warnings.warn(MINUS_K_COLON, stacklevel=2)
selectuntil = True
keywordexpr = keywordexpr[:-1]
-r"""
-Evaluate match expressions, as used by `-k` and `-m`.
+r"""Evaluate match expressions, as used by `-k` and `-m`.
The grammar is:
def evaluate(self, matcher: Callable[[str], bool]) -> bool:
"""Evaluate the match expression.
- :param matcher: Given an identifier, should return whether it matches or not.
- Should be prepared to handle arbitrary strings as input.
+ :param matcher:
+ Given an identifier, should return whether it matches or not.
+ Should be prepared to handle arbitrary strings as input.
- Returns whether the expression matches or not.
+ :returns: Whether the expression matches or not.
"""
ret = eval(
self.code, {"__builtins__": {}}, MatcherAdapter(matcher)
from typing import Any
from typing import Callable
from typing import Iterable
+from typing import Iterator
from typing import List
from typing import Mapping
from typing import NamedTuple
from .._code import getfslineno
from ..compat import ascii_escaped
+from ..compat import final
from ..compat import NOTSET
from ..compat import NotSetType
from ..compat import overload
if TYPE_CHECKING:
from typing import Type
+ from ..nodes import Node
+
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
parameterset: Union["ParameterSet", Sequence[object], object],
force_tuple: bool = False,
) -> "ParameterSet":
- """
+ """Extract from an object or objects.
+
:param parameterset:
- a legacy style parameterset that may or may not be a tuple,
- and may or may not be wrapped into a mess of mark objects
+ A legacy style parameterset that may or may not be a tuple,
+ and may or may not be wrapped into a mess of mark objects.
:param force_tuple:
- enforce tuple wrapping so single argument tuple values
- don't get decomposed and break tests
+ Enforce tuple wrapping so single argument tuple values
+ don't get decomposed and break tests.
"""
if isinstance(parameterset, cls):
del argvalues
if parameters:
- # check all parameter sets have the correct number of values
+ # Check all parameter sets have the correct number of values.
for param in parameters:
if len(param.values) != len(argnames):
msg = (
pytrace=False,
)
else:
- # empty parameter set (likely computed at runtime): create a single
- # parameter set with NOTSET values, with the "empty parameter set" mark applied to it
+ # Empty parameter set (likely computed at runtime): create a single
+ # parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
mark = get_empty_parameterset_mark(config, argnames, func)
parameters.append(
ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
return argnames, parameters
+@final
@attr.s(frozen=True)
class Mark:
#: Name of the mark.
Combines by appending args and merging kwargs.
- :param other: The mark to combine with.
- :type other: Mark
+ :param Mark other: The mark to combine with.
:rtype: Mark
"""
assert self.name == other.name
Unlike calling the MarkDecorator, with_args() can be used even
if the sole argument is a callable/class.
- :return: MarkDecorator
+ :rtype: MarkDecorator
"""
mark = Mark(self.name, args, kwargs)
return self.__class__(self.mark.combined_with(mark))
# the first match so it works out even if we break the rules.
@overload
def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc]
- raise NotImplementedError()
+ pass
@overload # noqa: F811
def __call__( # noqa: F811
self, *args: object, **kwargs: object
) -> "MarkDecorator":
- raise NotImplementedError()
+ pass
def __call__(self, *args: object, **kwargs: object): # noqa: F811
"""Call the MarkDecorator."""
def get_unpacked_marks(obj) -> List[Mark]:
- """
- obtain the unpacked marks that are stored on an object
- """
+ """Obtain the unpacked marks that are stored on an object."""
mark_list = getattr(obj, "pytestmark", [])
if not isinstance(mark_list, list):
mark_list = [mark_list]
def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]:
- """
- normalizes marker decorating helpers to mark objects
+ """Normalize marker decorating helpers to mark objects.
- :type mark_list: List[Union[Mark, Markdecorator]]
+ :type List[Union[Mark, Markdecorator]] mark_list:
:rtype: List[Mark]
"""
extracted = [
class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc]
def __call__(self, arg: _Markable) -> _Markable:
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def __call__(self, reason: str = ...) -> "MarkDecorator": # noqa: F811
- raise NotImplementedError()
+ ...
class _SkipifMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
*conditions: Union[str, bool],
reason: str = ...
) -> MarkDecorator:
- raise NotImplementedError()
+ ...
class _XfailMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc]
def __call__(self, arg: _Markable) -> _Markable:
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def __call__( # noqa: F811
] = ...,
strict: bool = ...
) -> MarkDecorator:
- raise NotImplementedError()
+ ...
class _ParametrizeMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
] = ...,
scope: Optional[_Scope] = ...
) -> MarkDecorator:
- raise NotImplementedError()
+ ...
class _UsefixturesMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
self, *fixtures: str
) -> MarkDecorator:
- raise NotImplementedError()
+ ...
class _FilterwarningsMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
self, *filters: str
) -> MarkDecorator:
- raise NotImplementedError()
+ ...
+@final
class MarkGenerator:
"""Factory for :class:`MarkDecorator` objects - exposed as
a ``pytest.mark`` singleton instance.
MARK_GEN = MarkGenerator()
-class NodeKeywords(collections.abc.MutableMapping):
- def __init__(self, node):
+# TODO(py36): inherit from typing.MutableMapping[str, Any].
+@final
+class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg]
+ def __init__(self, node: "Node") -> None:
self.node = node
self.parent = node.parent
self._markers = {node.name: True}
- def __getitem__(self, key):
+ def __getitem__(self, key: str) -> Any:
try:
return self._markers[key]
except KeyError:
raise
return self.parent.keywords[key]
- def __setitem__(self, key, value):
+ def __setitem__(self, key: str, value: Any) -> None:
self._markers[key] = value
- def __delitem__(self, key):
+ def __delitem__(self, key: str) -> None:
raise ValueError("cannot delete key in keywords dict")
- def __iter__(self):
+ def __iter__(self) -> Iterator[str]:
seen = self._seen()
return iter(seen)
- def _seen(self):
+ def _seen(self) -> Set[str]:
seen = set(self._markers)
if self.parent is not None:
seen.update(self.parent.keywords)
-""" monkeypatching and mocking functionality. """
+"""Monkeypatching and mocking functionality."""
import os
import re
import sys
from typing import Union
import pytest
+from _pytest.compat import final
from _pytest.compat import overload
from _pytest.fixtures import fixture
from _pytest.pathlib import Path
@fixture
def monkeypatch() -> Generator["MonkeyPatch", None, None]:
- """The returned ``monkeypatch`` fixture provides these
- helper methods to modify objects, dictionaries or os.environ::
+ """A convenient fixture for monkey-patching.
+
+ The fixture provides these methods to modify objects, dictionaries or
+ os.environ::
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
- All modifications will be undone after the requesting
- test function or fixture has finished. The ``raising``
- parameter determines if a KeyError or AttributeError
- will be raised if the set/deletion operation has no target.
+ All modifications will be undone after the requesting test function or
+ fixture has finished. The ``raising`` parameter determines if a KeyError
+ or AttributeError will be raised if the set/deletion operation has no target.
"""
mpatch = MonkeyPatch()
yield mpatch
def resolve(name: str) -> object:
- # simplified from zope.dottedname
+ # Simplified from zope.dottedname.
parts = name.split(".")
used = parts.pop(0)
pass
else:
continue
- # we use explicit un-nesting of the handling block in order
- # to avoid nested exceptions on python 3
+ # We use explicit un-nesting of the handling block in order
+ # to avoid nested exceptions.
try:
__import__(used)
except ImportError as ex:
- # str is used for py2 vs py3
expected = str(ex).split()[-1]
if expected == used:
raise
def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
- if not isinstance(import_path, str) or "." not in import_path:
+ if not isinstance(import_path, str) or "." not in import_path: # type: ignore[unreachable]
raise TypeError(
"must be absolute import path string, not {!r}".format(import_path)
)
notset = Notset()
+@final
class MonkeyPatch:
- """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
- """
+ """Object returned by the ``monkeypatch`` fixture keeping a record of
+ setattr/item/env/syspath changes."""
def __init__(self) -> None:
self._setattr = [] # type: List[Tuple[object, str, object]]
@contextmanager
def context(self) -> Generator["MonkeyPatch", None, None]:
- """
- Context manager that returns a new :class:`MonkeyPatch` object which
- undoes any patching done inside the ``with`` block upon exit:
+ """Context manager that returns a new :class:`MonkeyPatch` object
+ which undoes any patching done inside the ``with`` block upon exit.
+
+ Example:
.. code-block:: python
def setattr(
self, target: str, name: object, value: Notset = ..., raising: bool = ...,
) -> None:
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def setattr( # noqa: F811
self, target: object, name: str, value: object, raising: bool = ...,
) -> None:
- raise NotImplementedError()
+ ...
def setattr( # noqa: F811
self,
value: object = notset,
raising: bool = True,
) -> None:
- """ Set attribute value on target, memorizing the old value.
- By default raise AttributeError if the attribute did not exist.
+ """Set attribute value on target, memorizing the old value.
For convenience you can specify a string as ``target`` which
will be interpreted as a dotted import path, with the last part
- being the attribute name. Example:
+ being the attribute name. For example,
``monkeypatch.setattr("os.getcwd", lambda: "/")``
would set the ``getcwd`` function of the ``os`` module.
- The ``raising`` value determines if the setattr should fail
- if the attribute is not already present (defaults to True
- which means it will raise).
+ Raises AttributeError if the attribute does not exist, unless
+ ``raising`` is set to False.
"""
__tracebackhide__ = True
import inspect
name: Union[str, Notset] = notset,
raising: bool = True,
) -> None:
- """ Delete attribute ``name`` from ``target``, by default raise
- AttributeError it the attribute did not previously exist.
+ """Delete attribute ``name`` from ``target``.
If no ``name`` is specified and ``target`` is a string
it will be interpreted as a dotted import path with the
last part being the attribute name.
- If ``raising`` is set to False, no exception will be raised if the
- attribute is missing.
+ Raises AttributeError it the attribute does not exist, unless
+ ``raising`` is set to False.
"""
__tracebackhide__ = True
import inspect
delattr(target, name)
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
- """ Set dictionary entry ``name`` to value. """
+ """Set dictionary entry ``name`` to value."""
self._setitem.append((dic, name, dic.get(name, notset)))
dic[name] = value
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
- """ Delete ``name`` from dict. Raise KeyError if it doesn't exist.
+ """Delete ``name`` from dict.
- If ``raising`` is set to False, no exception will be raised if the
- key is missing.
+ Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
+ False.
"""
if name not in dic:
if raising:
del dic[name]
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
- """ Set environment variable ``name`` to ``value``. If ``prepend``
- is a character, read the current environment variable value
- and prepend the ``value`` adjoined with the ``prepend`` character."""
+ """Set environment variable ``name`` to ``value``.
+
+ If ``prepend`` is a character, read the current environment variable
+ value and prepend the ``value`` adjoined with the ``prepend``
+ character.
+ """
if not isinstance(value, str):
- warnings.warn(
+ warnings.warn( # type: ignore[unreachable]
pytest.PytestWarning(
"Value of environment variable {name} type should be str, but got "
"{value!r} (type: {type}); converted to str implicitly".format(
self.setitem(os.environ, name, value)
def delenv(self, name: str, raising: bool = True) -> None:
- """ Delete ``name`` from the environment. Raise KeyError if it does
- not exist.
+ """Delete ``name`` from the environment.
- If ``raising`` is set to False, no exception will be raised if the
- environment variable is missing.
+ Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
+ False.
"""
environ = os.environ # type: MutableMapping[str, str]
self.delitem(environ, name, raising=raising)
def syspath_prepend(self, path) -> None:
- """ Prepend ``path`` to ``sys.path`` list of import locations. """
+ """Prepend ``path`` to ``sys.path`` list of import locations."""
from pkg_resources import fixup_namespace_packages
if self._savesyspath is None:
invalidate_caches()
def chdir(self, path) -> None:
- """ Change the current working directory to the specified path.
+ """Change the current working directory to the specified path.
+
Path can be a string or a py.path.local object.
"""
if self._cwd is None:
if hasattr(path, "chdir"):
path.chdir()
elif isinstance(path, Path):
- # modern python uses the fspath protocol here LEGACY
+ # Modern python uses the fspath protocol here LEGACY
os.chdir(str(path))
else:
os.chdir(path)
def undo(self) -> None:
- """ Undo previous changes. This call consumes the
- undo stack. Calling it a second time has no effect unless
- you do more monkeypatching after the undo call.
+ """Undo previous changes.
+
+ This call consumes the undo stack. Calling it a second time has no
+ effect unless you do more monkeypatching after the undo call.
There is generally no need to call `undo()`, since it is
called automatically during tear-down.
try:
del dictionary[key]
except KeyError:
- pass # was already deleted, so we have the desired state
+ pass # Was already deleted, so we have the desired state.
else:
dictionary[key] = value
self._setitem[:] = []
import os
import warnings
from functools import lru_cache
+from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
-from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TypeVar
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
-from _pytest.config import PytestPluginManager
-from _pytest.deprecated import NODE_USE_FROM_PARENT
+from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureLookupError
from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
from _pytest.pathlib import Path
from _pytest.store import Store
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
"""
if nodeid == "":
- # If there is no root node at all, return an empty list so the caller's logic can remain sane
+ # If there is no root node at all, return an empty list so the caller's
+ # logic can remain sane.
return ()
parts = nodeid.split(SEP)
- # Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar'
+ # Replace single last element 'test_foo.py::Bar' with multiple elements
+ # 'test_foo.py', 'Bar'.
parts[-1:] = parts[-1].split("::")
- # Convert parts into a tuple to avoid possible errors with caching of a mutable type
+ # Convert parts into a tuple to avoid possible errors with caching of a
+ # mutable type.
return tuple(parts)
def ischildnode(baseid: str, nodeid: str) -> bool:
"""Return True if the nodeid is a child node of the baseid.
- E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp'
+ E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz',
+ but not of 'foo/blorp'.
"""
base_parts = _splitnode(baseid)
node_parts = _splitnode(nodeid)
class NodeMeta(type):
def __call__(self, *k, **kw):
- warnings.warn(NODE_USE_FROM_PARENT.format(name=self.__name__), stacklevel=2)
- return super().__call__(*k, **kw)
+ msg = (
+ "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
+ "See "
+ "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
+ " for more details."
+ ).format(name=self.__name__)
+ fail(msg, pytrace=False)
def _create(self, *k, **kw):
return super().__call__(*k, **kw)
class Node(metaclass=NodeMeta):
- """ base class for Collector and Item the test collection tree.
- Collector subclasses have children, Items are terminal nodes."""
+ """Base class for Collector and Item, the components of the test
+ collection tree.
+
+ Collector subclasses have children; Items are leaf nodes.
+ """
# Use __slots__ to make attribute access faster.
# Note that __dict__ is still available.
fspath: Optional[py.path.local] = None,
nodeid: Optional[str] = None,
) -> None:
- #: a unique name within the scope of the parent node
+ #: A unique name within the scope of the parent node.
self.name = name
- #: the parent collector node.
+ #: The parent collector node.
self.parent = parent
- #: the pytest config object
+ #: The pytest config object.
if config:
self.config = config # type: Config
else:
raise TypeError("config or parent must be provided")
self.config = parent.config
- #: the session this node is part of
+ #: The pytest session this node is part of.
if session:
self.session = session
else:
raise TypeError("session or parent must be provided")
self.session = parent.session
- #: filesystem path where this node was collected from (can be None)
+ #: Filesystem path where this node was collected from (can be None).
self.fspath = fspath or getattr(parent, "fspath", None)
- #: keywords/markers collected from all scopes
+ #: Keywords/markers collected from all scopes.
self.keywords = NodeKeywords(self)
- #: the marker objects belonging to this node
+ #: The marker objects belonging to this node.
self.own_markers = [] # type: List[Mark]
- #: allow adding of extra keywords to use for matching
+ #: Allow adding of extra keywords to use for matching.
self.extra_keyword_matches = set() # type: Set[str]
- # used for storing artificial fixturedefs for direct parametrization
- self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef]
+ # Used for storing artificial fixturedefs for direct parametrization.
+ self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef[Any]]
if nodeid is not None:
assert "::()" not in nodeid
@classmethod
def from_parent(cls, parent: "Node", **kw):
- """
- Public Constructor for Nodes
+ """Public constructor for Nodes.
This indirection got introduced in order to enable removing
the fragile logic from the node constructors.
- Subclasses can use ``super().from_parent(...)`` when overriding the construction
+ Subclasses can use ``super().from_parent(...)`` when overriding the
+ construction.
- :param parent: the parent node of this test Node
+ :param parent: The parent node of this Node.
"""
if "config" in kw:
raise TypeError("config is not a valid argument for from_parent")
@property
def ihook(self):
- """ fspath sensitive hook proxy used to call pytest hooks"""
+ """fspath-sensitive hook proxy used to call pytest hooks."""
return self.session.gethookproxy(self.fspath)
def __repr__(self) -> str:
return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
def warn(self, warning: "PytestWarning") -> None:
- """Issue a warning for this item.
+ """Issue a warning for this Node.
- Warnings will be displayed after the test session, unless explicitly suppressed
+ Warnings will be displayed after the test session, unless explicitly suppressed.
- :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
+ :param Warning warning:
+ The warning instance to issue. Must be a subclass of PytestWarning.
- :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
+ :raises ValueError: If ``warning`` instance is not a subclass of PytestWarning.
Example usage:
.. code-block:: python
node.warn(PytestWarning("some message"))
-
"""
from _pytest.warning_types import PytestWarning
warning, category=None, filename=str(path), lineno=lineno + 1,
)
- # methods for ordering nodes
+ # Methods for ordering nodes.
+
@property
def nodeid(self) -> str:
- """ a ::-separated string denoting its collection tree address. """
+ """A ::-separated string denoting its collection tree address."""
return self._nodeid
def __hash__(self) -> int:
pass
def listchain(self) -> List["Node"]:
- """ return list of all parent collectors up to self,
- starting from root of collection tree. """
+ """Return list of all parent collectors up to self, starting from
+ the root of collection tree."""
chain = []
item = self # type: Optional[Node]
while item is not None:
def add_marker(
self, marker: Union[str, MarkDecorator], append: bool = True
) -> None:
- """dynamically add a marker object to the node.
+ """Dynamically add a marker object to the node.
- :type marker: ``str`` or ``pytest.mark.*`` object
- :param marker:
- ``append=True`` whether to append the marker,
- if ``False`` insert at position ``0``.
+ :param append:
+ Whether to append the marker, or prepend it.
"""
from _pytest.mark import MARK_GEN
self.own_markers.insert(0, marker_.mark)
def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
- """
- :param name: if given, filter the results by the name attribute
+ """Iterate over all markers of the node.
- iterate over all markers of the node
+ :param name: If given, filter the results by the name attribute.
"""
return (x[1] for x in self.iter_markers_with_node(name=name))
def iter_markers_with_node(
self, name: Optional[str] = None
) -> Iterator[Tuple["Node", Mark]]:
- """
- :param name: if given, filter the results by the name attribute
+ """Iterate over all markers of the node.
- iterate over all markers of the node
- returns sequence of tuples (node, mark)
+ :param name: If given, filter the results by the name attribute.
+ :returns: An iterator of (node, mark) tuples.
"""
for node in reversed(self.listchain()):
for mark in node.own_markers:
@overload
def get_closest_marker(self, name: str) -> Optional[Mark]:
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811
- raise NotImplementedError()
+ ...
def get_closest_marker( # noqa: F811
self, name: str, default: Optional[Mark] = None
) -> Optional[Mark]:
- """return the first marker matching the name, from closest (for example function) to farther level (for example
- module level).
+ """Return the first marker matching the name, from closest (for
+ example function) to farther level (for example module level).
- :param default: fallback return value of no marker was found
- :param name: name to filter by
+ :param default: Fallback return value if no marker was found.
+ :param name: Name to filter by.
"""
return next(self.iter_markers(name=name), default)
def listextrakeywords(self) -> Set[str]:
- """ Return a set of all extra keywords in self and any parents."""
+ """Return a set of all extra keywords in self and any parents."""
extra_keywords = set() # type: Set[str]
for item in self.listchain():
extra_keywords.update(item.extra_keyword_matches)
return [x.name for x in self.listchain()]
def addfinalizer(self, fin: Callable[[], object]) -> None:
- """ register a function to be called when this node is finalized.
+ """Register a function to be called when this node is finalized.
This method can only be called when this node is active
in a setup chain, for example during self.setup().
self.session._setupstate.addfinalizer(fin, self)
def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]:
- """ get the next parent node (including ourself)
- which is an instance of the given class"""
+ """Get the next parent node (including self) which is an instance of
+ the given class."""
current = self # type: Optional[Node]
while current and not isinstance(current, cls):
current = current.parent
assert current is None or isinstance(current, cls)
return current
- def _prunetraceback(self, excinfo):
+ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
pass
def _repr_failure_py(
# It will be better to just always display paths relative to invocation_dir, but
# this requires a lot of plumbing (#6428).
try:
- abspath = Path(os.getcwd()) != Path(str(self.config.invocation_dir))
+ abspath = Path(os.getcwd()) != self.config.invocation_params.dir
except OSError:
abspath = True
excinfo: ExceptionInfo[BaseException],
style: "Optional[_TracebackStyle]" = None,
) -> Union[str, TerminalRepr]:
- """
- Return a representation of a collection or test failure.
+ """Return a representation of a collection or test failure.
:param excinfo: Exception information for the failure.
"""
def get_fslocation_from_item(
node: "Node",
) -> Tuple[Union[str, py.path.local], Optional[int]]:
- """Tries to extract the actual location from a node, depending on available attributes:
+ """Try to extract the actual location from a node, depending on available attributes:
* "location": a pair (path, lineno)
* "obj": a Python object that the node wraps.
* "fspath": just a path
- :rtype: a tuple of (str|LocalPath, int) with filename and line number.
+ :rtype: A tuple of (str|py.path.local, int) with filename and line number.
"""
# See Item.location.
location = getattr(
class Collector(Node):
- """ Collector instances create children through collect()
- and thus iteratively build a tree.
- """
+ """Collector instances create children through collect() and thus
+ iteratively build a tree."""
class CollectError(Exception):
- """ an error during collection, contains a custom message. """
+ """An error during collection, contains a custom message."""
def collect(self) -> Iterable[Union["Item", "Collector"]]:
- """ returns a list of children (items and collectors)
- for this collection node.
- """
+ """Return a list of children (items and collectors) for this
+ collection node."""
raise NotImplementedError("abstract")
# TODO: This omits the style= parameter which breaks Liskov Substitution.
def repr_failure( # type: ignore[override]
self, excinfo: ExceptionInfo[BaseException]
) -> Union[str, TerminalRepr]:
- """
- Return a representation of a collection failure.
+ """Return a representation of a collection failure.
:param excinfo: Exception information for the failure.
"""
return self._repr_failure_py(excinfo, style=tbstyle)
- def _prunetraceback(self, excinfo):
+ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
if hasattr(self, "fspath"):
traceback = excinfo.traceback
ntraceback = traceback.cut(path=self.fspath)
return fspath.relto(initial_path)
-class FSHookProxy:
- def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
- self.pm = pm
- self.remove_mods = remove_mods
-
- def __getattr__(self, name: str):
- x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
- self.__dict__[name] = x
- return x
-
-
class FSCollector(Collector):
def __init__(
self,
super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath)
- self._norecursepatterns = self.config.getini("norecursedirs")
-
@classmethod
def from_parent(cls, parent, *, fspath, **kw):
- """
- The public constructor
- """
+ """The public constructor."""
return super().from_parent(parent=parent, fspath=fspath, **kw)
- def _gethookproxy(self, fspath: py.path.local):
- # check if we have the common case of running
- # hooks with all conftest.py files
- pm = self.config.pluginmanager
- my_conftestmodules = pm._getconftestmodules(
- fspath, self.config.getoption("importmode")
- )
- remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
- if remove_mods:
- # one or more conftests are not in use at this fspath
- proxy = FSHookProxy(pm, remove_mods)
- else:
- # all plugins are active for this fspath
- proxy = self.config.hook
- return proxy
-
def gethookproxy(self, fspath: py.path.local):
- raise NotImplementedError()
-
- def _recurse(self, dirpath: py.path.local) -> bool:
- if dirpath.basename == "__pycache__":
- return False
- ihook = self._gethookproxy(dirpath.dirpath())
- if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
- return False
- for pat in self._norecursepatterns:
- if dirpath.check(fnmatch=pat):
- return False
- ihook = self._gethookproxy(dirpath)
- ihook.pytest_collect_directory(path=dirpath, parent=self)
- return True
+ warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+ return self.session.gethookproxy(fspath)
def isinitpath(self, path: py.path.local) -> bool:
- raise NotImplementedError()
-
- def _collectfile(
- self, path: py.path.local, handle_dupes: bool = True
- ) -> Sequence[Collector]:
- assert (
- path.isfile()
- ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
- path, path.isdir(), path.exists(), path.islink()
- )
- ihook = self.gethookproxy(path)
- if not self.isinitpath(path):
- if ihook.pytest_ignore_collect(path=path, config=self.config):
- return ()
-
- if handle_dupes:
- keepduplicates = self.config.getoption("keepduplicates")
- if not keepduplicates:
- duplicate_paths = self.config.pluginmanager._duplicatepaths
- if path in duplicate_paths:
- return ()
- else:
- duplicate_paths.add(path)
-
- return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return]
+ warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+ return self.session.isinitpath(path)
class File(FSCollector):
class Item(Node):
- """ a basic test invocation item. Note that for a single function
- there might be multiple test invocation items.
+ """A basic test invocation item.
+
+ Note that for a single function there might be multiple test invocation items.
"""
nextitem = None
super().__init__(name, parent, config, session, nodeid=nodeid)
self._report_sections = [] # type: List[Tuple[str, str, str]]
- #: user properties is a list of tuples (name, value) that holds user
- #: defined properties for this test.
+ #: A list of tuples (name, value) that holds user defined properties
+ #: for this test.
self.user_properties = [] # type: List[Tuple[str, object]]
def runtest(self) -> None:
raise NotImplementedError("runtest must be implemented by Item subclass")
def add_report_section(self, when: str, key: str, content: str) -> None:
- """
- Adds a new report section, similar to what's done internally to add stdout and
- stderr captured output::
+ """Add a new report section, similar to what's done internally to add
+ stdout and stderr captured output::
item.add_report_section("call", "stdout", "report section contents")
:param str key:
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
``"stderr"`` internally.
-
:param str content:
The full contents as a string.
"""
@cached_property
def location(self) -> Tuple[str, Optional[int], str]:
location = self.reportinfo()
- if isinstance(location[0], py.path.local):
- fspath = location[0]
- else:
- fspath = py.path.local(location[0])
+ fspath = absolutepath(str(location[0]))
relfspath = self.session._node_location_to_relpath(fspath)
assert type(location[2]) is str
return (relfspath, location[1], location[2])
-""" run test suites written for nose. """
+"""Run testsuites written for nose."""
from _pytest import python
from _pytest import unittest
from _pytest.config import hookimpl
def pytest_runtest_setup(item):
if is_potential_nosetest(item):
if not call_optional(item.obj, "setup"):
- # call module level setup if there is no object level one
+ # Call module level setup if there is no object level one.
call_optional(item.parent.obj, "setup")
- # XXX this implies we only call teardown when setup worked
+ # XXX This implies we only call teardown when setup worked.
item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item)
def is_potential_nosetest(item: Item) -> bool:
- # extra check needed since we do not do nose style setup/teardown
- # on direct unittest style classes
+ # Extra check needed since we do not do nose style setup/teardown
+ # on direct unittest style classes.
return isinstance(item, python.Function) and not isinstance(
item, unittest.TestCaseFunction
)
isfixture = hasattr(method, "_pytestfixturefunction")
if method is not None and not isfixture and callable(method):
# If there's any problems allow the exception to raise rather than
- # silently ignoring them
+ # silently ignoring them.
method()
return True
-"""
-exception classes and constants handling test outcomes
-as well as functions creating them
-"""
+"""Exception classes and constants handling test outcomes as well as
+functions creating them."""
import sys
from typing import Any
from typing import Callable
from typing import Optional
from typing import TypeVar
-TYPE_CHECKING = False # avoid circular import through compat
+TYPE_CHECKING = False # Avoid circular import through compat.
if TYPE_CHECKING:
from typing import NoReturn
class OutcomeException(BaseException):
- """ OutcomeException and its subclass instances indicate and
- contain info about test and collection outcomes.
- """
+ """OutcomeException and its subclass instances indicate and contain info
+ about test and collection outcomes."""
def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
if msg is not None and not isinstance(msg, str):
- error_msg = (
+ error_msg = ( # type: ignore[unreachable]
"{} expected string as 'msg' parameter, got '{}' instead.\n"
"Perhaps you meant to use a mark?"
)
class Failed(OutcomeException):
- """ raised from an explicit call to pytest.fail() """
+ """Raised from an explicit call to pytest.fail()."""
__module__ = "builtins"
class Exit(Exception):
- """ raised for immediate program exits (no tracebacks/summaries)"""
+ """Raised for immediate program exits (no tracebacks/summaries)."""
def __init__(
self, msg: str = "unknown reason", returncode: Optional[int] = None
# Elaborate hack to work around https://github.com/python/mypy/issues/2087.
# Ideally would just be `exit.Exception = Exit` etc.
-_F = TypeVar("_F", bound=Callable)
+_F = TypeVar("_F", bound=Callable[..., object])
_ET = TypeVar("_ET", bound="Type[BaseException]")
return decorate
-# exposed helper methods
+# Exposed helper methods.
@_with_exception(Exit)
def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn":
- """
- Exit testing process.
+ """Exit testing process.
- :param str msg: message to display upon exit.
- :param int returncode: return code to be used when exiting pytest.
+ :param str msg: Message to display upon exit.
+ :param int returncode: Return code to be used when exiting pytest.
"""
__tracebackhide__ = True
raise Exit(msg, returncode)
@_with_exception(Skipped)
def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
- """
- Skip an executing test with the given message.
+ """Skip an executing test with the given message.
This function should be called only during testing (setup, call or teardown) or
during collection by using the ``allow_module_level`` flag. This function can
be called in doctests as well.
- :kwarg bool allow_module_level: allows this function to be called at
- module level, skipping the rest of the module. Default to False.
+ :param bool allow_module_level:
+ Allows this function to be called at module level, skipping the rest
+ of the module. Defaults to False.
.. note::
- It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be
- skipped under certain conditions like mismatching platforms or
- dependencies.
+ It is better to use the :ref:`pytest.mark.skipif ref` marker when
+ possible to declare a test to be skipped under certain conditions
+ like mismatching platforms or dependencies.
Similarly, use the ``# doctest: +SKIP`` directive (see `doctest.SKIP
<https://docs.python.org/3/library/doctest.html#doctest.SKIP>`_)
to skip a doctest statically.
@_with_exception(Failed)
def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
- """
- Explicitly fail an executing test with the given message.
+ """Explicitly fail an executing test with the given message.
- :param str msg: the message to show the user as reason for the failure.
- :param bool pytrace: if false the msg represents the full failure information and no
+ :param str msg:
+ The message to show the user as reason for the failure.
+ :param bool pytrace:
+ If False, msg represents the full failure information and no
python traceback will be reported.
"""
__tracebackhide__ = True
class XFailed(Failed):
- """ raised from an explicit call to pytest.xfail() """
+ """Raised from an explicit call to pytest.xfail()."""
@_with_exception(XFailed)
def xfail(reason: str = "") -> "NoReturn":
- """
- Imperatively xfail an executing test or setup functions with the given reason.
+ """Imperatively xfail an executing test or setup function with the given reason.
This function should be called only during testing (setup, call or teardown).
.. note::
- It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be
- xfailed under certain conditions like known bugs or missing features.
+ It is better to use the :ref:`pytest.mark.xfail ref` marker when
+ possible to declare a test to be xfailed under certain conditions
+ like known bugs or missing features.
"""
__tracebackhide__ = True
raise XFailed(reason)
def importorskip(
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
) -> Any:
- """Imports and returns the requested module ``modname``, or skip the
+ """Import and return the requested module ``modname``, or skip the
current test if the module cannot be imported.
- :param str modname: the name of the module to import
- :param str minversion: if given, the imported module's ``__version__``
- attribute must be at least this minimal version, otherwise the test is
- still skipped.
- :param str reason: if given, this reason is shown as the message when the
- module cannot be imported.
- :returns: The imported module. This should be assigned to its canonical
- name.
+ :param str modname:
+ The name of the module to import.
+ :param str minversion:
+ If given, the imported module's ``__version__`` attribute must be at
+ least this minimal version, otherwise the test is still skipped.
+ :param str reason:
+ If given, this reason is shown as the message when the module cannot
+ be imported.
+
+ :returns:
+ The imported module. This should be assigned to its canonical name.
Example::
compile(modname, "", "eval") # to catch syntaxerrors
with warnings.catch_warnings():
- # make sure to ignore ImportWarnings that might happen because
+ # Make sure to ignore ImportWarnings that might happen because
# of existing directories with the same name we're trying to
- # import but without a __init__.py file
+ # import but without a __init__.py file.
warnings.simplefilter("ignore")
try:
__import__(modname)
-""" submit failure or test session information to a pastebin service. """
+"""Submit failure or test session information to a pastebin service."""
import tempfile
from io import StringIO
from typing import IO
def pytest_configure(config: Config) -> None:
if config.option.pastebin == "all":
tr = config.pluginmanager.getplugin("terminalreporter")
- # if no terminal reporter plugin is present, nothing we can do here;
+ # If no terminal reporter plugin is present, nothing we can do here;
# this can happen when this function executes in a worker node
- # when using pytest-xdist, for example
+ # when using pytest-xdist, for example.
if tr is not None:
- # pastebin file will be utf-8 encoded binary file
+ # pastebin file will be UTF-8 encoded binary file.
config._store[pastebinfile_key] = tempfile.TemporaryFile("w+b")
oldwrite = tr._tw.write
def pytest_unconfigure(config: Config) -> None:
if pastebinfile_key in config._store:
pastebinfile = config._store[pastebinfile_key]
- # get terminal contents and delete file
+ # Get terminal contents and delete file.
pastebinfile.seek(0)
sessionlog = pastebinfile.read()
pastebinfile.close()
del config._store[pastebinfile_key]
- # undo our patching in the terminal reporter
+ # Undo our patching in the terminal reporter.
tr = config.pluginmanager.getplugin("terminalreporter")
del tr._tw.__dict__["write"]
- # write summary
+ # Write summary.
tr.write_sep("=", "Sending information to Paste Service")
pastebinurl = create_new_paste(sessionlog)
tr.write_line("pastebin session-log: %s\n" % pastebinurl)
def create_new_paste(contents: Union[str, bytes]) -> str:
- """
- Creates a new paste using bpaste.net service.
+ """Create a new paste using the bpaste.net service.
- :contents: paste contents string
- :returns: url to the pasted contents or error message
+ :contents: Paste contents string.
+ :returns: URL to the pasted contents, or an error message.
"""
import re
from urllib.request import urlopen
from os.path import sep
from posixpath import sep as posix_sep
from types import ModuleType
+from typing import Callable
from typing import Iterable
from typing import Iterator
from typing import Optional
def ensure_reset_dir(path: Path) -> None:
- """
- ensures the given path is an empty directory
- """
+ """Ensure the given path is an empty directory."""
if path.exists():
rm_rf(path)
path.mkdir()
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
- """Handles known read-only errors during rmtree.
+ """Handle known read-only errors during rmtree.
The returned value is used only by our own tests.
"""
exctype, excvalue = exc[:2]
- # another process removed the file in the middle of the "rm_rf" (xdist for example)
- # more context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
+ # Another process removed the file in the middle of the "rm_rf" (xdist for example).
+ # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
if isinstance(excvalue, FileNotFoundError):
return False
if p.is_file():
for parent in p.parents:
chmod_rw(str(parent))
- # stop when we reach the original path passed to rm_rf
+ # Stop when we reach the original path passed to rm_rf.
if parent == start_path:
break
chmod_rw(str(path))
def get_extended_length_path_str(path: str) -> str:
- """Converts to extended length path as a str"""
+ """Convert a path to a Windows extended length path."""
long_path_prefix = "\\\\?\\"
unc_long_path_prefix = "\\\\?\\UNC\\"
if path.startswith((long_path_prefix, unc_long_path_prefix)):
def rm_rf(path: Path) -> None:
"""Remove the path contents recursively, even if some elements
- are read-only.
- """
+ are read-only."""
path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_path=path)
shutil.rmtree(str(path), onerror=onerror)
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
- """finds all elements in root that begin with the prefix, case insensitive"""
+ """Find all elements in root that begin with the prefix, case insensitive."""
l_prefix = prefix.lower()
for x in root.iterdir():
if x.name.lower().startswith(l_prefix):
def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
- """
- :param iter: iterator over path names
- :param prefix: expected prefix of the path names
- :returns: the parts of the paths following the prefix
+ """Return the parts of the paths following the prefix.
+
+ :param iter: Iterator over path names.
+ :param prefix: Expected prefix of the path names.
"""
p_len = len(prefix)
for p in iter:
def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
- """combines find_prefixes and extract_suffixes
- """
+ """Combine find_prefixes and extract_suffixes."""
return extract_suffixes(find_prefixed(root, prefix), prefix)
def parse_num(maybe_num) -> int:
- """parses number path suffixes, returns -1 on error"""
+ """Parse number path suffixes, returns -1 on error."""
try:
return int(maybe_num)
except ValueError:
def _force_symlink(
root: Path, target: Union[str, PurePath], link_to: Union[str, Path]
) -> None:
- """helper to create the current symlink
+ """Helper to create the current symlink.
- it's full of race conditions that are reasonably ok to ignore
- for the context of best effort linking to the latest test run
+ It's full of race conditions that are reasonably OK to ignore
+ for the context of best effort linking to the latest test run.
- the presumption being that in case of much parallelism
- the inaccuracy is going to be acceptable
+ The presumption being that in case of much parallelism
+ the inaccuracy is going to be acceptable.
"""
current_symlink = root.joinpath(target)
try:
def make_numbered_dir(root: Path, prefix: str) -> Path:
- """create a directory with an increased number as suffix for the given prefix"""
+ """Create a directory with an increased number as suffix for the given prefix."""
for i in range(10):
# try up to 10 times to create the folder
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
def create_cleanup_lock(p: Path) -> Path:
- """crates a lock to prevent premature folder cleanup"""
+ """Create a lock to prevent premature folder cleanup."""
lock_path = get_lock_path(p)
try:
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
- """registers a cleanup function for removing a lock, by default on atexit"""
+ """Register a cleanup function for removing a lock, by default on atexit."""
pid = os.getpid()
def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
def maybe_delete_a_numbered_dir(path: Path) -> None:
- """removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
+ """Remove a numbered directory if its lock can be obtained and it does
+ not seem to be in use."""
path = ensure_extended_length_path(path)
lock_path = None
try:
# * process cwd (Windows)
return
finally:
- # if we created the lock, ensure we remove it even if we failed
- # to properly remove the numbered dir
+ # If we created the lock, ensure we remove it even if we failed
+ # to properly remove the numbered dir.
if lock_path is not None:
try:
lock_path.unlink()
def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
- """checks if `path` is deletable based on whether the lock file is expired"""
+ """Check if `path` is deletable based on whether the lock file is expired."""
if path.is_symlink():
return False
lock = get_lock_path(path)
return False
else:
if lock_time < consider_lock_dead_if_created_before:
- # wa want to ignore any errors while trying to remove the lock such as:
- # - PermissionDenied, like the file permissions have changed since the lock creation
- # - FileNotFoundError, in case another pytest process got here first.
+ # We want to ignore any errors while trying to remove the lock such as:
+ # - PermissionDenied, like the file permissions have changed since the lock creation;
+ # - FileNotFoundError, in case another pytest process got here first;
# and any other cause of failure.
with contextlib.suppress(OSError):
lock.unlink()
def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
- """tries to cleanup a folder if we can ensure it's deletable"""
+ """Try to cleanup a folder if we can ensure it's deletable."""
if ensure_deletable(path, consider_lock_dead_if_created_before):
maybe_delete_a_numbered_dir(path)
def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
- """lists candidates for numbered directories to be removed - follows py.path"""
+ """List candidates for numbered directories to be removed - follows py.path."""
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
max_delete = max_existing - keep
paths = find_prefixed(root, prefix)
def cleanup_numbered_dir(
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
) -> None:
- """cleanup for lock driven numbered directories"""
+ """Cleanup for lock driven numbered directories."""
for path in cleanup_candidates(root, prefix, keep):
try_cleanup(path, consider_lock_dead_if_created_before)
for path in root.glob("garbage-*"):
def make_numbered_dir_with_cleanup(
root: Path, prefix: str, keep: int, lock_timeout: float
) -> Path:
- """creates a numbered dir with a cleanup lock and removes old ones"""
+ """Create a numbered dir with a cleanup lock and remove old ones."""
e = None
for i in range(10):
try:
raise e
-def resolve_from_str(input: str, root: py.path.local) -> Path:
- assert not isinstance(input, Path), "would break on py2"
- rootpath = Path(root)
+def resolve_from_str(input: str, rootpath: Path) -> Path:
input = expanduser(input)
input = expandvars(input)
if isabs(input):
def fnmatch_ex(pattern: str, path) -> bool:
- """FNMatcher port from py.path.common which works with PurePath() instances.
+ """A port of FNMatcher from py.path.common which works with PurePath() instances.
- The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
- for each part of the path, while this algorithm uses the whole path instead.
+ The difference between this algorithm and PurePath.match() is that the
+ latter matches "**" glob expressions for each part of the path, while
+ this algorithm uses the whole path instead.
For example:
- "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with
- PurePath.match().
+ "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py"
+ with this algorithm, but not with PurePath.match().
- This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according
- this logic.
+ This algorithm was ported to keep backward-compatibility with existing
+ settings which assume paths match according this logic.
References:
* https://bugs.python.org/issue29249
def symlink_or_skip(src, dst, **kwargs):
- """Makes a symlink or skips the test in case symlinks are not supported."""
+ """Make a symlink, or skip the test in case symlinks are not supported."""
try:
os.symlink(str(src), str(dst), **kwargs)
except OSError as e:
class ImportMode(Enum):
- """Possible values for `mode` parameter of `import_path`"""
+ """Possible values for `mode` parameter of `import_path`."""
prepend = "prepend"
append = "append"
*,
mode: Union[str, ImportMode] = ImportMode.prepend
) -> ModuleType:
- """
- Imports and returns a module from the given path, which can be a file (a module) or
+ """Import and return a module from the given path, which can be a file (a module) or
a directory (a package).
The import mechanism used is controlled by the `mode` parameter:
to import the module, which avoids having to use `__import__` and muck with `sys.path`
at all. It effectively allows having same-named test modules in different places.
- :raise ImportPathMismatchError: if after importing the given `path` and the module `__file__`
+ :raises ImportPathMismatchError:
+ If after importing the given `path` and the module `__file__`
are different. Only raised in `prepend` and `append` modes.
"""
mode = ImportMode(mode)
pkg_root = path.parent
module_name = path.stem
- # change sys.path permanently: restoring it at the end of this function would cause surprising
+ # Change sys.path permanently: restoring it at the end of this function would cause surprising
# problems because of delayed imports: for example, a conftest.py file imported by this function
# might have local imports, which would fail at runtime if we restored sys.path.
if mode is ImportMode.append:
def resolve_package_path(path: Path) -> Optional[Path]:
"""Return the Python package path by looking for the last
directory upwards which still contains an __init__.py.
- Return None if it can not be determined.
+
+ Returns None if it can not be determined.
"""
result = None
for parent in itertools.chain((path,), path.parents):
break
result = parent
return result
+
+
+def visit(
+ path: str, recurse: Callable[["os.DirEntry[str]"], bool]
+) -> Iterator["os.DirEntry[str]"]:
+ """Walk a directory recursively, in breadth-first order.
+
+ Entries at each directory level are sorted.
+ """
+ entries = sorted(os.scandir(path), key=lambda entry: entry.name)
+ yield from entries
+ for entry in entries:
+ if entry.is_dir(follow_symlinks=False) and recurse(entry):
+ yield from visit(entry.path, recurse)
+
+
+def absolutepath(path: Union[Path, str]) -> Path:
+ """Convert a path to an absolute path using os.path.abspath.
+
+ Prefer this over Path.resolve() (see #6523).
+ Prefer this over Path.absolute() (not public, doesn't normalize).
+ """
+ return Path(os.path.abspath(str(path)))
+
+
+def commonpath(path1: Path, path2: Path) -> Optional[Path]:
+ """Return the common part shared with the other path, or None if there is
+ no common part."""
+ try:
+ return Path(os.path.commonpath((str(path1), str(path2))))
+ except ValueError:
+ return None
+
+
+def bestrelpath(directory: Path, dest: Path) -> str:
+ """Return a string which is a relative path from directory to dest such
+ that directory/bestrelpath == dest.
+
+ If no such path can be determined, returns dest.
+ """
+ if dest == directory:
+ return os.curdir
+ # Find the longest common directory.
+ base = commonpath(directory, dest)
+ # Can be the case on Windows.
+ if not base:
+ return str(dest)
+ reldirectory = directory.relative_to(base)
+ reldest = dest.relative_to(base)
+ return os.path.join(
+ # Back from directory to base.
+ *([os.pardir] * len(reldirectory.parts)),
+ # Forward from base to dest.
+ *reldest.parts,
+ )
-"""(disabled by default) support for testing pytest and pytest plugins."""
+"""(Disabled by default) support for testing pytest and pytest plugins."""
import collections.abc
import gc
import importlib
from _pytest import timing
from _pytest._code import Source
from _pytest.capture import _get_multicapture
+from _pytest.compat import final
+from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.pathlib import make_numbered_dir
from _pytest.pathlib import Path
from _pytest.python import Module
+from _pytest.reports import CollectReport
from _pytest.reports import TestReport
from _pytest.tmpdir import TempdirFactory
if TYPE_CHECKING:
from typing import Type
+ from typing_extensions import Literal
import pexpect
def _pytest(request: FixtureRequest) -> "PytestArg":
"""Return a helper which offers a gethookrecorder(hook) method which
returns a HookRecorder instance which helps to make assertions about called
- hooks.
-
- """
+ hooks."""
return PytestArg(request)
return hookrecorder
-def get_public_names(values):
+def get_public_names(values: Iterable[str]) -> List[str]:
"""Only return names from iterator values without a leading underscore."""
return [x for x in values if x[0] != "_"]
class ParsedCall:
- def __init__(self, name, kwargs):
+ def __init__(self, name: str, kwargs) -> None:
self.__dict__.update(kwargs)
self._name = name
- def __repr__(self):
+ def __repr__(self) -> str:
d = self.__dict__.copy()
del d["_name"]
return "<ParsedCall {!r}(**{!r})>".format(self._name, d)
if TYPE_CHECKING:
# The class has undetermined attributes, this tells mypy about it.
- def __getattr__(self, key):
- raise NotImplementedError()
+ def __getattr__(self, key: str):
+ ...
class HookRecorder:
This wraps all the hook calls in the plugin manager, recording each call
before propagating the normal calls.
-
"""
def __init__(self, pluginmanager: PytestPluginManager) -> None:
self._pluginmanager = pluginmanager
self.calls = [] # type: List[ParsedCall]
+ self.ret = None # type: Optional[Union[int, ExitCode]]
def before(hook_name: str, hook_impls, kwargs) -> None:
self.calls.append(ParsedCall(hook_name, kwargs))
names = names.split()
return [call for call in self.calls if call._name in names]
- def assert_contains(self, entries) -> None:
+ def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None:
__tracebackhide__ = True
i = 0
entries = list(entries)
# functionality for test reports
+ @overload
def getreports(
+ self, names: "Literal['pytest_collectreport']",
+ ) -> Sequence[CollectReport]:
+ ...
+
+ @overload # noqa: F811
+ def getreports( # noqa: F811
+ self, names: "Literal['pytest_runtest_logreport']",
+ ) -> Sequence[TestReport]:
+ ...
+
+ @overload # noqa: F811
+ def getreports( # noqa: F811
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ ...
+
+ def getreports( # noqa: F811
self,
- names: Union[
- str, Iterable[str]
- ] = "pytest_runtest_logreport pytest_collectreport",
- ) -> List[TestReport]:
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
return [x.report for x in self.getcalls(names)]
def matchreport(
self,
inamepart: str = "",
- names: Union[
- str, Iterable[str]
- ] = "pytest_runtest_logreport pytest_collectreport",
- when=None,
- ):
- """return a testreport whose dotted import path matches"""
+ names: Union[str, Iterable[str]] = (
+ "pytest_runtest_logreport",
+ "pytest_collectreport",
+ ),
+ when: Optional[str] = None,
+ ) -> Union[CollectReport, TestReport]:
+ """Return a testreport whose dotted import path matches."""
values = []
for rep in self.getreports(names=names):
if not when and rep.when != "call" and rep.passed:
)
return values[0]
+ @overload
def getfailures(
+ self, names: "Literal['pytest_collectreport']",
+ ) -> Sequence[CollectReport]:
+ ...
+
+ @overload # noqa: F811
+ def getfailures( # noqa: F811
+ self, names: "Literal['pytest_runtest_logreport']",
+ ) -> Sequence[TestReport]:
+ ...
+
+ @overload # noqa: F811
+ def getfailures( # noqa: F811
self,
- names: Union[
- str, Iterable[str]
- ] = "pytest_runtest_logreport pytest_collectreport",
- ) -> List[TestReport]:
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ ...
+
+ def getfailures( # noqa: F811
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
return [rep for rep in self.getreports(names) if rep.failed]
- def getfailedcollections(self) -> List[TestReport]:
+ def getfailedcollections(self) -> Sequence[CollectReport]:
return self.getfailures("pytest_collectreport")
def listoutcomes(
self,
- ) -> Tuple[List[TestReport], List[TestReport], List[TestReport]]:
+ ) -> Tuple[
+ Sequence[TestReport],
+ Sequence[Union[CollectReport, TestReport]],
+ Sequence[Union[CollectReport, TestReport]],
+ ]:
passed = []
skipped = []
failed = []
- for rep in self.getreports("pytest_collectreport pytest_runtest_logreport"):
+ for rep in self.getreports(
+ ("pytest_collectreport", "pytest_runtest_logreport")
+ ):
if rep.passed:
if rep.when == "call":
+ assert isinstance(rep, TestReport)
passed.append(rep)
elif rep.skipped:
skipped.append(rep)
@pytest.fixture
def linecomp() -> "LineComp":
- """
- A :class: `LineComp` instance for checking that an input linearly
- contains a sequence of strings.
- """
+ """A :class: `LineComp` instance for checking that an input linearly
+ contains a sequence of strings."""
return LineComp()
@pytest.fixture(name="LineMatcher")
def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]":
- """
- A reference to the :class: `LineMatcher`.
+ """A reference to the :class: `LineMatcher`.
This is instantiable with a list of lines (without their trailing newlines).
This is useful for testing large texts, such as the output of commands.
@pytest.fixture
def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir":
- """
- A :class: `TestDir` instance, that can be used to run and test pytest itself.
+ """A :class: `TestDir` instance, that can be used to run and test pytest itself.
It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture
but provides methods which aid in testing pytest itself.
-
"""
return Testdir(request, tmpdir_factory)
config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
-# regex to match the session duration string in the summary: "74.34s"
+# Regex to match the session duration string in the summary: "74.34s".
rex_session_duration = re.compile(r"\d+\.\d\ds")
-# regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped"
+# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped".
rex_outcome = re.compile(r"(\d+) (\w+)")
) -> None:
try:
self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode]
- """the return value"""
+ """The return value."""
except ValueError:
self.ret = ret
self.outlines = outlines
- """list of lines captured from stdout"""
+ """List of lines captured from stdout."""
self.errlines = errlines
- """list of lines captured from stderr"""
+ """List of lines captured from stderr."""
self.stdout = LineMatcher(outlines)
""":class:`LineMatcher` of stdout.
:func:`stdout.fnmatch_lines() <LineMatcher.fnmatch_lines()>` method.
"""
self.stderr = LineMatcher(errlines)
- """:class:`LineMatcher` of stderr"""
+ """:class:`LineMatcher` of stderr."""
self.duration = duration
- """duration in seconds"""
+ """Duration in seconds."""
def __repr__(self) -> str:
return (
======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
- Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
"""
return self.parse_summary_nouns(self.outlines)
@classmethod
def parse_summary_nouns(cls, lines) -> Dict[str, int]:
- """Extracts the nouns from a pytest terminal summary line.
+ """Extract the nouns from a pytest terminal summary line.
It always returns the plural noun for consistency::
======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
- Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
"""
for line in reversed(lines):
if rex_session_duration.search(line):
xfailed: int = 0,
) -> None:
"""Assert that the specified outcomes appear with the respective
- numbers (0 means it didn't occur) in the text output from a test run.
- """
+ numbers (0 means it didn't occur) in the text output from a test run."""
__tracebackhide__ = True
d = self.parseoutcomes()
sys.path[:], sys.meta_path[:] = self.__saved
+@final
class Testdir:
"""Temporary test directory with tools to test/run pytest itself.
- This is based on the ``tmpdir`` fixture but provides a number of methods
+ This is based on the :fixture:`tmpdir` fixture but provides a number of methods
which aid with testing pytest itself. Unless :py:meth:`chdir` is used all
methods will use :py:attr:`tmpdir` as their current working directory.
:ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
- :ivar plugins: A list of plugins to use with :py:meth:`parseconfig` and
+ :ivar plugins:
+ A list of plugins to use with :py:meth:`parseconfig` and
:py:meth:`runpytest`. Initially this is an empty list but plugins can
be added to the list. The type of items to add to the list depends on
the method using them so refer to them for details.
-
"""
__test__ = False
Some methods modify the global interpreter state and this tries to
clean this up. It does not remove the temporary directory however so
it can be looked at after the test run has finished.
-
"""
self._sys_modules_snapshot.restore()
self._sys_path_snapshot.restore()
self.monkeypatch.undo()
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
- # some zope modules used by twisted-related tests keep internal state
+ # Some zope modules used by twisted-related tests keep internal state
# and can't be deleted; we had some trouble in the past with
- # `zope.interface` for example
+ # `zope.interface` for example.
def preserve_module(name):
return name.startswith("zope")
"""Cd into the temporary directory.
This is done automatically upon instantiation.
-
"""
self.tmpdir.chdir()
def makefile(self, ext: str, *args: str, **kwargs):
r"""Create new file(s) in the testdir.
- :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`.
- :param list[str] args: All args will be treated as strings and joined using newlines.
- The result will be written as contents to the file. The name of the
- file will be based on the test function requesting this fixture.
- :param kwargs: Each keyword is the name of a file, while the value of it will
- be written as contents of the file.
+ :param str ext:
+ The extension the file(s) should use, including the dot, e.g. `.py`.
+ :param args:
+ All args are treated as strings and joined using newlines.
+ The result is written as contents to the file. The name of the
+ file is based on the test function requesting this fixture.
+ :param kwargs:
+ Each keyword is the name of a file, while the value of it will
+ be written as contents of the file.
Examples:
def makepyfile(self, *args, **kwargs):
r"""Shortcut for .makefile() with a .py extension.
+
Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
existing files.
.. code-block:: python
def test_something(testdir):
- # initial file is created test_something.py
+ # Initial file is created test_something.py.
testdir.makepyfile("foobar")
- # to create multiple files, pass kwargs accordingly
+ # To create multiple files, pass kwargs accordingly.
testdir.makepyfile(custom="foobar")
- # at this point, both 'test_something.py' & 'custom.py' exist in the test directory
+ # At this point, both 'test_something.py' & 'custom.py' exist in the test directory.
"""
return self._makefile(".py", args, kwargs)
def maketxtfile(self, *args, **kwargs):
r"""Shortcut for .makefile() with a .txt extension.
+
Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
existing files.
.. code-block:: python
def test_something(testdir):
- # initial file is created test_something.txt
+ # Initial file is created test_something.txt.
testdir.maketxtfile("foobar")
- # to create multiple files, pass kwargs accordingly
+ # To create multiple files, pass kwargs accordingly.
testdir.maketxtfile(custom="foobar")
- # at this point, both 'test_something.txt' & 'custom.txt' exist in the test directory
+ # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory.
"""
return self._makefile(".txt", args, kwargs)
return self.tmpdir.mkdir(name)
def mkpydir(self, name) -> py.path.local:
- """Create a new python package.
+ """Create a new Python package.
This creates a (sub)directory with an empty ``__init__.py`` file so it
- gets recognised as a python package.
-
+ gets recognised as a Python package.
"""
p = self.mkdir(name)
p.ensure("__init__.py")
"""Copy file from project's directory into the testdir.
:param str name: The name of the file to copy.
- :return: path to the copied directory (inside ``self.tmpdir``).
-
+ :returns: Path to the copied directory (inside ``self.tmpdir``).
"""
import warnings
from _pytest.warning_types import PYTESTER_COPY_EXAMPLE
def getnode(self, config: Config, arg):
"""Return the collection node of a file.
- :param config: :py:class:`_pytest.config.Config` instance, see
- :py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the
- configuration
-
- :param arg: a :py:class:`py.path.local` instance of the file
-
+ :param _pytest.config.Config config:
+ A pytest config.
+ See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it.
+ :param py.path.local arg:
+ Path to the file.
"""
session = Session.from_config(config)
assert "::" not in str(arg)
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
create the (configured) pytest Config instance.
- :param path: a :py:class:`py.path.local` instance of the file
-
+ :param py.path.local path: Path to the file.
"""
config = self.parseconfigure(path)
session = Session.from_config(config)
This recurses into the collection node and returns a list of all the
test items contained within.
-
"""
session = colitems[0].session
result = [] # type: List[Item]
provide a ``.getrunner()`` method which should return a runner which
can run the test protocol for a single item, e.g.
:py:func:`_pytest.runner.runtestprotocol`.
-
"""
# used from runner functional tests
item = self.getitem(source)
runner = testclassinstance.getrunner()
return runner(item)
- def inline_runsource(self, source, *cmdlineargs):
+ def inline_runsource(self, source, *cmdlineargs) -> HookRecorder:
"""Run a test module in process using ``pytest.main()``.
This run writes "source" into a temporary file and runs
``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
for the result.
- :param source: the source code of the test module
+ :param source: The source code of the test module.
- :param cmdlineargs: any extra command line arguments to use
-
- :return: :py:class:`HookRecorder` instance of the result
+ :param cmdlineargs: Any extra command line arguments to use.
+ :returns: :py:class:`HookRecorder` instance of the result.
"""
p = self.makepyfile(source)
values = list(cmdlineargs) + [p]
return self.inline_run(*values)
- def inline_genitems(self, *args):
+ def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
"""Run ``pytest.main(['--collectonly'])`` in-process.
Runs the :py:func:`pytest.main` function to run all of pytest inside
the test process itself like :py:meth:`inline_run`, but returns a
tuple of the collected items and a :py:class:`HookRecorder` instance.
-
"""
rec = self.inline_run("--collect-only", *args)
items = [x.item for x in rec.getcalls("pytest_itemcollected")]
return items, rec
- def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
+ def inline_run(
+ self, *args, plugins=(), no_reraise_ctrlc: bool = False
+ ) -> HookRecorder:
"""Run ``pytest.main()`` in-process, returning a HookRecorder.
Runs the :py:func:`pytest.main` function to run all of pytest inside
from that run than can be done by matching stdout/stderr from
:py:meth:`runpytest`.
- :param args: command line arguments to pass to :py:func:`pytest.main`
-
- :kwarg plugins: extra plugin instances the ``pytest.main()`` instance should use.
-
- :kwarg no_reraise_ctrlc: typically we reraise keyboard interrupts from the child run. If
+ :param args:
+ Command line arguments to pass to :py:func:`pytest.main`.
+ :param plugins:
+ Extra plugin instances the ``pytest.main()`` instance should use.
+ :param no_reraise_ctrlc:
+ Typically we reraise keyboard interrupts from the child run. If
True, the KeyboardInterrupt exception is captured.
- :return: a :py:class:`HookRecorder` instance
+ :returns: A :py:class:`HookRecorder` instance.
"""
# (maybe a cpython bug?) the importlib cache sometimes isn't updated
# properly between file creation and inline_run (especially if imports
class reprec: # type: ignore
pass
- reprec.ret = ret # type: ignore[attr-defined]
+ reprec.ret = ret
- # typically we reraise keyboard interrupts from the child run
- # because it's our user requesting interruption of the testing
+ # Typically we reraise keyboard interrupts from the child run
+ # because it's our user requesting interruption of the testing.
if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
calls = reprec.getcalls("pytest_keyboard_interrupt")
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
"""Return result of running pytest in-process, providing a similar
- interface to what self.runpytest() provides.
- """
+ interface to what self.runpytest() provides."""
syspathinsert = kwargs.pop("syspathinsert", False)
if syspathinsert:
sys.stdout.write(out)
sys.stderr.write(err)
+ assert reprec.ret is not None
res = RunResult(
reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now
)
def runpytest(self, *args, **kwargs) -> RunResult:
"""Run pytest inline or in a subprocess, depending on the command line
- option "--runpytest" and return a :py:class:`RunResult`.
-
- """
+ option "--runpytest" and return a :py:class:`RunResult`."""
args = self._ensure_basetemp(args)
if self._method == "inprocess":
return self.runpytest_inprocess(*args, **kwargs)
If :py:attr:`plugins` has been populated they should be plugin modules
to be registered with the PluginManager.
-
"""
args = self._ensure_basetemp(args)
def parseconfigure(self, *args) -> Config:
"""Return a new pytest configured Config instance.
- This returns a new :py:class:`_pytest.config.Config` instance like
+ Returns a new :py:class:`_pytest.config.Config` instance like
:py:meth:`parseconfig`, but also calls the pytest_configure hook.
"""
config = self.parseconfig(*args)
def getitem(self, source, funcname: str = "test_func") -> Item:
"""Return the test item for a test function.
- This writes the source to a python file and runs pytest's collection on
+ Writes the source to a python file and runs pytest's collection on
the resulting module, returning the test item for the requested
function name.
- :param source: the module source
-
- :param funcname: the name of the test function for which to return a
- test item
-
+ :param source:
+ The module source.
+ :param funcname:
+ The name of the test function for which to return a test item.
"""
items = self.getitems(source)
for item in items:
def getitems(self, source) -> List[Item]:
"""Return all test items collected from the module.
- This writes the source to a python file and runs pytest's collection on
+ Writes the source to a Python file and runs pytest's collection on
the resulting module, returning all test items contained within.
-
"""
modcol = self.getmodulecol(source)
return self.genitems([modcol])
def getmodulecol(self, source, configargs=(), withinit: bool = False):
"""Return the module collection node for ``source``.
- This writes ``source`` to a file using :py:meth:`makepyfile` and then
+ Writes ``source`` to a file using :py:meth:`makepyfile` and then
runs the pytest collection on it, returning the collection node for the
test module.
- :param source: the source code of the module to collect
-
- :param configargs: any extra arguments to pass to
- :py:meth:`parseconfigure`
+ :param source:
+ The source code of the module to collect.
- :param withinit: whether to also write an ``__init__.py`` file to the
- same directory to ensure it is a package
+ :param configargs:
+ Any extra arguments to pass to :py:meth:`parseconfigure`.
+ :param withinit:
+ Whether to also write an ``__init__.py`` file to the same
+ directory to ensure it is a package.
"""
if isinstance(source, Path):
path = self.tmpdir.join(str(source))
) -> Optional[Union[Item, Collector]]:
"""Return the collection node for name from the module collection.
- This will search a module collection node for a collection node
- matching the given name.
+ Searchs a module collection node for a collection node matching the
+ given name.
- :param modcol: a module collection node; see :py:meth:`getmodulecol`
-
- :param name: the name of the node to return
+ :param modcol: A module collection node; see :py:meth:`getmodulecol`.
+ :param name: The name of the node to return.
"""
if modcol not in self._mod_collections:
self._mod_collections[modcol] = list(modcol.collect())
):
"""Invoke subprocess.Popen.
- This calls subprocess.Popen making sure the current working directory
- is in the PYTHONPATH.
+ Calls subprocess.Popen making sure the current working directory is
+ in the PYTHONPATH.
You probably want to use :py:meth:`run` instead.
-
"""
env = os.environ.copy()
env["PYTHONPATH"] = os.pathsep.join(
Run a process using subprocess.Popen saving the stdout and stderr.
- :param args: the sequence of arguments to pass to `subprocess.Popen()`
- :kwarg timeout: the period in seconds after which to timeout and raise
- :py:class:`Testdir.TimeoutExpired`
- :kwarg stdin: optional standard input. Bytes are being send, closing
+ :param args:
+ The sequence of arguments to pass to `subprocess.Popen()`.
+ :param timeout:
+ The period in seconds after which to timeout and raise
+ :py:class:`Testdir.TimeoutExpired`.
+ :param stdin:
+ Optional standard input. Bytes are being send, closing
the pipe, otherwise it is passed through to ``popen``.
Defaults to ``CLOSE_STDIN``, which translates to using a pipe
(``subprocess.PIPE``) that gets closed.
- Returns a :py:class:`RunResult`.
-
+ :rtype: RunResult
"""
__tracebackhide__ = True
def runpython(self, script) -> RunResult:
"""Run a python script using sys.executable as interpreter.
- Returns a :py:class:`RunResult`.
-
+ :rtype: RunResult
"""
return self.run(sys.executable, script)
def runpython_c(self, command):
- """Run python -c "command", return a :py:class:`RunResult`."""
+ """Run python -c "command".
+
+ :rtype: RunResult
+ """
return self.run(sys.executable, "-c", command)
def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunResult:
with "runpytest-" to not conflict with the normal numbered pytest
location for temporary files and directories.
- :param args: the sequence of arguments to pass to the pytest subprocess
- :param timeout: the period in seconds after which to timeout and raise
- :py:class:`Testdir.TimeoutExpired`
+ :param args:
+ The sequence of arguments to pass to the pytest subprocess.
+ :param timeout:
+ The period in seconds after which to timeout and raise
+ :py:class:`Testdir.TimeoutExpired`.
- Returns a :py:class:`RunResult`.
+ :rtype: RunResult
"""
__tracebackhide__ = True
p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-")
directory locations.
The pexpect child is returned.
-
"""
basetemp = self.tmpdir.mkdir("temp-pexpect")
invoke = " ".join(map(str, self._getpytestargs()))
"""Run a command using pexpect.
The pexpect child is returned.
-
"""
pexpect = pytest.importorskip("pexpect", "3.0")
if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
return lines2
def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
- """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).
- """
+ """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`)."""
__tracebackhide__ = True
self._match_lines_random(lines2, fnmatch)
def re_match_lines_random(self, lines2: Sequence[str]) -> None:
- """Check lines exist in the output in any order (using :func:`python:re.match`).
- """
+ """Check lines exist in the output in any order (using :func:`python:re.match`)."""
__tracebackhide__ = True
self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
wildcards. If they do not match a pytest.fail() is called. The
matches and non-matches are also shown as part of the error message.
- :param lines2: string patterns to match.
- :param consecutive: match lines consecutive?
+ :param lines2: String patterns to match.
+ :param consecutive: Match lines consecutively?
"""
__tracebackhide__ = True
self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
) -> None:
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
- :param list[str] lines2: list of string patterns to match. The actual
- format depends on ``match_func``
- :param match_func: a callable ``match_func(line, pattern)`` where line
- is the captured line from stdout/stderr and pattern is the matching
- pattern
- :param str match_nickname: the nickname for the match function that
- will be logged to stdout when a match occurs
- :param consecutive: match lines consecutively?
+ :param Sequence[str] lines2:
+ List of string patterns to match. The actual format depends on
+ ``match_func``.
+ :param match_func:
+ A callable ``match_func(line, pattern)`` where line is the
+ captured line from stdout/stderr and pattern is the matching
+ pattern.
+ :param str match_nickname:
+ The nickname for the match function that will be logged to stdout
+ when a match occurs.
+ :param consecutive:
+ Match lines consecutively?
"""
if not isinstance(lines2, collections.abc.Sequence):
raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__))
def no_fnmatch_line(self, pat: str) -> None:
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
- :param str pat: the pattern to match lines.
+ :param str pat: The pattern to match lines.
"""
__tracebackhide__ = True
self._no_match_line(pat, fnmatch, "fnmatch")
def no_re_match_line(self, pat: str) -> None:
"""Ensure captured lines do not match the given pattern, using ``re.match``.
- :param str pat: the regular expression to match lines.
+ :param str pat: The regular expression to match lines.
"""
__tracebackhide__ = True
self._no_match_line(
def _no_match_line(
self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
) -> None:
- """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``
+ """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``.
- :param str pat: the pattern to match lines
+ :param str pat: The pattern to match lines.
"""
__tracebackhide__ = True
nomatch_printed = False
-""" Python test discovery, setup and run of test functions. """
+"""Python test discovery, setup and run of test functions."""
import enum
import fnmatch
import inspect
import itertools
import os
import sys
+import types
import typing
import warnings
from collections import Counter
from _pytest._code import filter_traceback
from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped
+from _pytest.compat import final
from _pytest.compat import get_default_arg_names
from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
-from _pytest.deprecated import FUNCARGNAMES
+from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.fixtures import FuncFixtureInfo
from _pytest.main import Session
from _pytest.mark import MARK_GEN
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import parts
-from _pytest.reports import TerminalRepr
+from _pytest.pathlib import visit
from _pytest.warning_types import PytestCollectionWarning
from _pytest.warning_types import PytestUnhandledCoroutineWarning
msg += (
"You need to install a suitable plugin for your async framework, for example:\n"
)
+ msg += " - anyio\n"
msg += " - pytest-asyncio\n"
- msg += " - pytest-trio\n"
msg += " - pytest-tornasync\n"
+ msg += " - pytest-trio\n"
msg += " - pytest-twisted"
warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))
skip(msg="async def function and no async plugin installed (see warnings)")
return True
-def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]:
+def pytest_collect_file(
+ path: py.path.local, parent: nodes.Collector
+) -> Optional["Module"]:
ext = path.ext
if ext == ".py":
if not parent.session.isinitpath(path):
def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool:
- """Returns True if path matches any of the patterns in the list of globs given."""
+ """Return whether path matches any of the patterns in the list of globs given."""
return any(path.fnmatch(pattern) for pattern in patterns)
@hookimpl(trylast=True)
def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
- # nothing was collected elsewhere, let's do it here
+ # Nothing was collected elsewhere, let's do it here.
if safe_isclass(obj):
if collector.istestclass(obj, name):
return Class.from_parent(collector, name=name, obj=obj)
elif collector.istestfunction(obj, name):
- # mock seems to store unbound methods (issue473), normalize it
+ # mock seems to store unbound methods (issue473), normalize it.
obj = getattr(obj, "__func__", obj)
# We need to try and unwrap the function if it's a functools.partial
# or a functools.wrapped.
- # We mustn't if it's been wrapped with mock.patch (python 2 only)
+ # We mustn't if it's been wrapped with mock.patch (python 2 only).
if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))):
filename, lineno = getfslineno(obj)
warnings.warn_explicit(
self._obj = value
def _getobj(self):
- """Gets the underlying Python object. May be overwritten by subclasses."""
+ """Get the underlying Python object. May be overwritten by subclasses."""
# TODO: Improve the type of `parent` such that assert/ignore aren't needed.
assert self.parent is not None
obj = self.parent.obj # type: ignore[attr-defined]
return getattr(obj, self.name)
def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
- """ return python path relative to the containing module. """
+ """Return Python path relative to the containing module."""
chain = self.listchain()
chain.reverse()
parts = []
return fspath, lineno, modpath
+# As an optimization, these builtin attribute names are pre-ignored when
+# iterating over an object during collection -- the pytest_pycollect_makeitem
+# hook is not called for them.
+# fmt: off
+class _EmptyClass: pass # noqa: E701
+IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305
+ frozenset(),
+ # Module.
+ dir(types.ModuleType("empty_module")),
+ # Some extra module attributes the above doesn't catch.
+ {"__builtins__", "__file__", "__cached__"},
+ # Class.
+ dir(_EmptyClass),
+ # Instance.
+ dir(_EmptyClass()),
+)
+del _EmptyClass
+# fmt: on
+
+
class PyCollector(PyobjMixin, nodes.Collector):
def funcnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_functions", name)
def isnosetest(self, obj: object) -> bool:
- """ Look for the __test__ attribute, which is applied by the
- @nose.tools.istest decorator
+ """Look for the __test__ attribute, which is applied by the
+ @nose.tools.istest decorator.
"""
# We explicitly check for "is True" here to not mistakenly treat
# classes with a custom __getattr__ returning something truthy (like a
def istestfunction(self, obj: object, name: str) -> bool:
if self.funcnamefilter(name) or self.isnosetest(obj):
if isinstance(obj, staticmethod):
- # static methods need to be unwrapped
+ # staticmethods need to be unwrapped.
obj = safe_getattr(obj, "__func__", False)
return (
safe_getattr(obj, "__call__", False)
return self.classnamefilter(name) or self.isnosetest(obj)
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
- """
- checks if the given name matches the prefix or glob-pattern defined
- in ini configuration.
- """
+ """Check if the given name matches the prefix or glob-pattern defined
+ in ini configuration."""
for option in self.config.getini(option_name):
if name.startswith(option):
return True
- # check that name looks like a glob-string before calling fnmatch
+ # Check that name looks like a glob-string before calling fnmatch
# because this is called for every name in each collected module,
- # and fnmatch is somewhat expensive to call
+ # and fnmatch is somewhat expensive to call.
elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
name, option
):
dicts.append(basecls.__dict__)
seen = set() # type: Set[str]
values = [] # type: List[Union[nodes.Item, nodes.Collector]]
+ ihook = self.ihook
for dic in dicts:
# Note: seems like the dict can change during iteration -
# be careful not to remove the list() without consideration.
for name, obj in list(dic.items()):
+ if name in IGNORED_ATTRIBUTES:
+ continue
if name in seen:
continue
seen.add(name)
- res = self._makeitem(name, obj)
+ res = ihook.pytest_pycollect_makeitem(
+ collector=self, name=name, obj=obj
+ )
if res is None:
continue
- if not isinstance(res, list):
- res = [res]
- values.extend(res)
+ elif isinstance(res, list):
+ values.extend(res)
+ else:
+ values.append(res)
def sort_key(item):
fspath, lineno, _ = item.reportinfo()
values.sort(key=sort_key)
return values
- def _makeitem(
- self, name: str, obj: object
- ) -> Union[
- None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]
- ]:
- # assert self.ihook.fspath == self.fspath, self
- item = self.ihook.pytest_pycollect_makeitem(
- collector=self, name=name, obj=obj
- ) # type: Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]
- return item
-
def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
modulecol = self.getparent(Module)
assert modulecol is not None
if not metafunc._calls:
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
else:
- # add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
+ # Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
- # add_funcarg_pseudo_fixture_def may have shadowed some fixtures
+ # Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
# with direct parametrization, so make sure we update what the
# function really needs.
fixtureinfo.prune_dependency_tree()
class Module(nodes.File, PyCollector):
- """ Collector for test classes and functions. """
+ """Collector for test classes and functions."""
def _getobj(self):
return self._importtestmodule()
return super().collect()
def _inject_setup_module_fixture(self) -> None:
- """Injects a hidden autouse, module scoped fixture into the collected module object
+ """Inject a hidden autouse, module scoped fixture into the collected module object
that invokes setUpModule/tearDownModule if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
self.obj.__pytest_setup_module = xunit_setup_module_fixture
def _inject_setup_function_fixture(self) -> None:
- """Injects a hidden autouse, function scoped fixture into the collected module object
+ """Inject a hidden autouse, function scoped fixture into the collected module object
that invokes setup_function/teardown_function if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
self.obj.__pytest_setup_function = xunit_setup_function_fixture
def _importtestmodule(self):
- # we assume we are only called once per module
+ # We assume we are only called once per module.
importmode = self.config.getoption("--import-mode")
try:
mod = import_path(self.fspath, mode=importmode)
"Traceback:\n"
"{traceback}".format(fspath=self.fspath, traceback=formatted_tb)
) from e
- except _pytest.runner.Skipped as e:
+ except skip.Exception as e:
if e.allow_module_level:
raise
raise self.CollectError(
session=None,
nodeid=None,
) -> None:
- # NOTE: could be just the following, but kept as-is for compat.
+ # NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent)
session = parent.session
nodes.FSCollector.__init__(
self.name = os.path.basename(str(fspath.dirname))
def setup(self) -> None:
- # not using fixtures to call setup_module here because autouse fixtures
- # from packages are not called automatically (#4085)
+ # Not using fixtures to call setup_module here because autouse fixtures
+ # from packages are not called automatically (#4085).
setup_module = _get_first_non_fixture_func(
self.obj, ("setUpModule", "setup_module")
)
self.addfinalizer(func)
def gethookproxy(self, fspath: py.path.local):
- return super()._gethookproxy(fspath)
+ warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+ return self.session.gethookproxy(fspath)
def isinitpath(self, path: py.path.local) -> bool:
- return path in self.session._initialpaths
+ warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+ return self.session.isinitpath(path)
+
+ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
+ if direntry.name == "__pycache__":
+ return False
+ path = py.path.local(direntry.path)
+ ihook = self.session.gethookproxy(path.dirpath())
+ if ihook.pytest_ignore_collect(path=path, config=self.config):
+ return False
+ norecursepatterns = self.config.getini("norecursedirs")
+ if any(path.check(fnmatch=pat) for pat in norecursepatterns):
+ return False
+ return True
+
+ def _collectfile(
+ self, path: py.path.local, handle_dupes: bool = True
+ ) -> typing.Sequence[nodes.Collector]:
+ assert (
+ path.isfile()
+ ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
+ path, path.isdir(), path.exists(), path.islink()
+ )
+ ihook = self.session.gethookproxy(path)
+ if not self.session.isinitpath(path):
+ if ihook.pytest_ignore_collect(path=path, config=self.config):
+ return ()
+
+ if handle_dupes:
+ keepduplicates = self.config.getoption("keepduplicates")
+ if not keepduplicates:
+ duplicate_paths = self.config.pluginmanager._duplicatepaths
+ if path in duplicate_paths:
+ return ()
+ else:
+ duplicate_paths.add(path)
+
+ return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return]
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.fspath.dirpath()
):
yield Module.from_parent(self, fspath=init_module)
pkg_prefixes = set() # type: Set[py.path.local]
- for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
+ for direntry in visit(str(this_path), recurse=self._recurse):
+ path = py.path.local(direntry.path)
+
# We will visit our own __init__.py file, in which case we skip it.
- is_file = path.isfile()
- if is_file:
- if path.basename == "__init__.py" and path.dirpath() == this_path:
+ if direntry.is_file():
+ if direntry.name == "__init__.py" and path.dirpath() == this_path:
continue
- parts_ = parts(path.strpath)
+ parts_ = parts(direntry.path)
if any(
str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path
for pkg_prefix in pkg_prefixes
):
continue
- if is_file:
+ if direntry.is_file():
yield from self._collectfile(path)
- elif not path.isdir():
+ elif not direntry.is_dir():
# Broken symlink or invalid/missing file.
continue
elif path.join("__init__.py").check(file=1):
def _call_with_optional_argument(func, arg) -> None:
"""Call the given function with the given argument if func accepts one argument, otherwise
- calls func without arguments"""
+ calls func without arguments."""
arg_count = func.__code__.co_argcount
if inspect.ismethod(func):
arg_count -= 1
def _get_first_non_fixture_func(obj: object, names: Iterable[str]):
"""Return the attribute from the given object to be used as a setup/teardown
- xunit-style function, but only if not marked as a fixture to
- avoid calling it twice.
- """
+ xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
for name in names:
meth = getattr(obj, name, None)
if meth is not None and fixtures.getfixturemarker(meth) is None:
class Class(PyCollector):
- """ Collector for test methods. """
+ """Collector for test methods."""
@classmethod
def from_parent(cls, parent, *, name, obj=None):
- """
- The public constructor
- """
+ """The public constructor."""
return super().from_parent(name=name, parent=parent)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
return [Instance.from_parent(self, name="()")]
def _inject_setup_class_fixture(self) -> None:
- """Injects a hidden autouse, class scoped fixture into the collected class object
+ """Inject a hidden autouse, class scoped fixture into the collected class object
that invokes setup_class/teardown_class if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
self.obj.__pytest_setup_class = xunit_setup_class_fixture
def _inject_setup_method_fixture(self) -> None:
- """Injects a hidden autouse, function scoped fixture into the collected class object
+ """Inject a hidden autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
class Instance(PyCollector):
_ALLOW_MARKERS = False # hack, destroy later
- # instances share the object with their parents in a way
+ # Instances share the object with their parents in a way
# that duplicates markers instances if not taken out
- # can be removed at node structure reorganization time
+ # can be removed at node structure reorganization time.
def _getobj(self):
# TODO: Improve the type of `parent` such that assert/ignore aren't needed.
return False
+@final
class CallSpec2:
def __init__(self, metafunc: "Metafunc") -> None:
self.metafunc = metafunc
self.marks.extend(normalize_mark_list(marks))
+@final
class Metafunc:
- """
- Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
+ """Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
+
They help to inspect a test function and to generate tests according to
test configuration or values specified in the class or module where a
test function is defined.
) -> None:
self.definition = definition
- #: access to the :class:`_pytest.config.Config` object for the test session
+ #: Access to the :class:`_pytest.config.Config` object for the test session.
self.config = config
- #: the module object where the test function is defined in.
+ #: The module object where the test function is defined in.
self.module = module
- #: underlying python test function
+ #: Underlying Python test function.
self.function = definition.obj
- #: set of fixture names required by the test function
+ #: Set of fixture names required by the test function.
self.fixturenames = fixtureinfo.names_closure
- #: class object where the test function is defined in or ``None``.
+ #: Class object where the test function is defined in or ``None``.
self.cls = cls
self._calls = [] # type: List[CallSpec2]
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
- @property
- def funcargnames(self) -> List[str]:
- """ alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
- warnings.warn(FUNCARGNAMES, stacklevel=2)
- return self.fixturenames
-
def parametrize(
self,
argnames: Union[str, List[str], Tuple[str, ...]],
*,
_param_mark: Optional[Mark] = None
) -> None:
- """ Add new invocations to the underlying test function using the list
+ """Add new invocations to the underlying test function using the list
of argvalues for the given argnames. Parametrization is performed
during the collection phase. If you need to setup expensive resources
see about setting indirect to do it rather at test setup time.
- :arg argnames: a comma-separated string denoting one or more argument
- names, or a list/tuple of argument strings.
+ :param argnames:
+ A comma-separated string denoting one or more argument names, or
+ a list/tuple of argument strings.
- :arg argvalues: The list of argvalues determines how often a
- test is invoked with different argument values. If only one
- argname was specified argvalues is a list of values. If N
- argnames were specified, argvalues must be a list of N-tuples,
- where each tuple-element specifies a value for its respective
- argname.
+ :param argvalues:
+ The list of argvalues determines how often a test is invoked with
+ different argument values.
- :arg indirect: The list of argnames or boolean. A list of arguments'
- names (subset of argnames). If True the list contains all names from
- the argnames. Each argvalue corresponding to an argname in this list will
+ If only one argname was specified argvalues is a list of values.
+ If N argnames were specified, argvalues must be a list of
+ N-tuples, where each tuple-element specifies a value for its
+ respective argname.
+
+ :param indirect:
+ A list of arguments' names (subset of argnames) or a boolean.
+ If True the list contains all names from the argnames. Each
+ argvalue corresponding to an argname in this list will
be passed as request.param to its respective argname fixture
function so that it can perform more expensive setups during the
setup phase of a test rather than at collection time.
- :arg ids: sequence of (or generator for) ids for ``argvalues``,
- or a callable to return part of the id for each argvalue.
+ :param ids:
+ Sequence of (or generator for) ids for ``argvalues``,
+ or a callable to return part of the id for each argvalue.
With sequences (and generators like ``itertools.count()``) the
returned ids should be of type ``string``, ``int``, ``float``,
If no ids are provided they will be generated automatically from
the argvalues.
- :arg scope: if specified it denotes the scope of the parameters.
+ :param scope:
+ If specified it denotes the scope of the parameters.
The scope is used for grouping tests by parameter instances.
It will also override any fixture-function defined scope, allowing
to set a dynamic scope using test context or configuration.
scope, descr="parametrize() call in {}".format(self.function.__name__)
)
- # create the new calls: if we are parametrize() multiple times (by applying the decorator
+ # Create the new calls: if we are parametrize() multiple times (by applying the decorator
# more than once) then we accumulate those calls generating the cartesian product
- # of all calls
+ # of all calls.
newcalls = []
for callspec in self._calls or [CallSpec2(self)]:
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
parameters: typing.Sequence[ParameterSet],
nodeid: str,
) -> List[str]:
- """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
+ """Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
to ``parametrize``.
- :param List[str] argnames: list of argument names passed to ``parametrize()``.
- :param ids: the ids parameter of the parametrized call (see docs).
- :param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``.
- :param str str: the nodeid of the item that generated this parametrized call.
+ :param List[str] argnames: List of argument names passed to ``parametrize()``.
+ :param ids: The ids parameter of the parametrized call (see docs).
+ :param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``.
+ :param str str: The nodeid of the item that generated this parametrized call.
:rtype: List[str]
- :return: the list of ids for each argname given
+ :returns: The list of ids for each argname given.
"""
if ids is None:
idfn = None
elif isinstance(id_value, (float, int, bool)):
new_ids.append(str(id_value))
else:
- msg = "In {}: ids must be list of string/float/int/bool, found: {} (type: {!r}) at index {}"
+ msg = ( # type: ignore[unreachable]
+ "In {}: ids must be list of string/float/int/bool, "
+ "found: {} (type: {!r}) at index {}"
+ )
fail(
msg.format(func_name, saferepr(id_value), type(id_value), idx),
pytrace=False,
argnames: typing.Sequence[str],
indirect: Union[bool, typing.Sequence[str]],
) -> Dict[str, "Literal['params', 'funcargs']"]:
- """Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg"
- to the function, based on the ``indirect`` parameter of the parametrized() call.
+ """Resolve if each parametrized argument must be considered a
+ parameter to a fixture or a "funcarg" to the function, based on the
+ ``indirect`` parameter of the parametrized() call.
- :param List[str] argnames: list of argument names passed to ``parametrize()``.
- :param indirect: same ``indirect`` parameter of ``parametrize()``.
+ :param List[str] argnames: List of argument names passed to ``parametrize()``.
+ :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
:rtype: Dict[str, str]
A dict mapping each arg name to either:
* "params" if the argname should be the parameter of a fixture of the same name.
argnames: typing.Sequence[str],
indirect: Union[bool, typing.Sequence[str]],
) -> None:
- """
- Check if all argnames are being used, by default values, or directly/indirectly.
+ """Check if all argnames are being used, by default values, or directly/indirectly.
- :param List[str] argnames: list of argument names passed to ``parametrize()``.
- :param indirect: same ``indirect`` parameter of ``parametrize()``.
- :raise ValueError: if validation fails.
+ :param List[str] argnames: List of argument names passed to ``parametrize()``.
+ :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
+ :raises ValueError: If validation fails.
"""
default_arg_names = set(get_default_arg_names(self.function))
func_name = self.function.__name__
def _find_parametrized_scope(
argnames: typing.Sequence[str],
- arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef]],
+ arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef[object]]],
indirect: Union[bool, typing.Sequence[str]],
) -> "fixtures._Scope":
"""Find the most appropriate scope for a parametrized call based on its arguments.
if name in argnames
]
if used_scopes:
- # Takes the most narrow scope from used fixtures
+ # Takes the most narrow scope from used fixtures.
for scope in reversed(fixtures.scopes):
if scope in used_scopes:
return scope
elif isinstance(val, enum.Enum):
return str(val)
elif isinstance(getattr(val, "__name__", None), str):
- # name of a class, function, module, etc.
+ # Name of a class, function, module, etc.
name = getattr(val, "__name__") # type: str
return name
return str(argname) + str(idx)
unique_ids = set(resolved_ids)
if len(unique_ids) != len(resolved_ids):
- # Record the number of occurrences of each test ID
+ # Record the number of occurrences of each test ID.
test_id_counts = Counter(resolved_ids)
- # Map the test ID to its next suffix
+ # Map the test ID to its next suffix.
test_id_suffixes = defaultdict(int) # type: Dict[str, int]
- # Suffix non-unique IDs to make them unique
+ # Suffix non-unique IDs to make them unique.
for index, test_id in enumerate(resolved_ids):
if test_id_counts[test_id] > 1:
resolved_ids[index] = "{}{}".format(test_id, test_id_suffixes[test_id])
verbose = config.getvalue("verbose")
def get_best_relpath(func):
- loc = getlocation(func, curdir)
+ loc = getlocation(func, str(curdir))
return curdir.bestrelpath(py.path.local(loc))
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
tw.sep("-", "fixtures used by {}".format(item.name))
# TODO: Fix this type ignore.
tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined]
- # dict key not used in loop but needed for sorting
+ # dict key not used in loop but needed for sorting.
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
assert fixturedefs is not None
if not fixturedefs:
continue
- # last item is expected to be the one used by the test item
+ # Last item is expected to be the one used by the test item.
write_fixture(fixturedefs[-1])
for session_item in session.items:
if not fixturedefs:
continue
for fixturedef in fixturedefs:
- loc = getlocation(fixturedef.func, curdir)
+ loc = getlocation(fixturedef.func, str(curdir))
if (fixturedef.argname, loc) in seen:
continue
seen.add((fixturedef.argname, loc))
if verbose > 0:
tw.write(" -- %s" % bestrel, yellow=True)
tw.write("\n")
- loc = getlocation(fixturedef.func, curdir)
+ loc = getlocation(fixturedef.func, str(curdir))
doc = inspect.getdoc(fixturedef.func)
if doc:
write_docstring(tw, doc)
class Function(PyobjMixin, nodes.Item):
- """ a Function Item is responsible for setting up and executing a
- Python test function.
+ """An Item responsible for setting up and executing a Python test function.
+
+ param name:
+ The full function name, including any decorations like those
+ added by parametrization (``my_func[my_param]``).
+ param parent:
+ The parent Node.
+ param config:
+ The pytest Config object.
+ param callspec:
+ If given, this is function has been parametrized and the callspec contains
+ meta information about the parametrization.
+ param callobj:
+ If given, the object which will be called when the Function is invoked,
+ otherwise the callobj will be obtained from ``parent`` using ``originalname``.
+ param keywords:
+ Keywords bound to the function object for "-k" matching.
+ param session:
+ The pytest Session object.
+ param fixtureinfo:
+ Fixture information already resolved at this fixture node..
+ param originalname:
+ The attribute name to use for accessing the underlying function object.
+ Defaults to ``name``. Set this if name is different from the original name,
+ for example when it contains decorations like those added by parametrization
+ (``my_func[my_param]``).
"""
- # disable since functions handle it themselves
+ # Disable since functions handle it themselves.
_ALLOW_MARKERS = False
def __init__(
fixtureinfo: Optional[FuncFixtureInfo] = None,
originalname: Optional[str] = None,
) -> None:
- """
- param name: the full function name, including any decorations like those
- added by parametrization (``my_func[my_param]``).
- param parent: the parent Node.
- param config: the pytest Config object
- param callspec: if given, this is function has been parametrized and the callspec contains
- meta information about the parametrization.
- param callobj: if given, the object which will be called when the Function is invoked,
- otherwise the callobj will be obtained from ``parent`` using ``originalname``
- param keywords: keywords bound to the function object for "-k" matching.
- param session: the pytest Session object
- param fixtureinfo: fixture information already resolved at this fixture node.
- param originalname:
- The attribute name to use for accessing the underlying function object.
- Defaults to ``name``. Set this if name is different from the original name,
- for example when it contains decorations like those added by parametrization
- (``my_func[my_param]``).
- """
super().__init__(name, parent, config=config, session=session)
if callobj is not NOTSET:
#: .. versionadded:: 3.0
self.originalname = originalname or name
- # note: when FunctionDefinition is introduced, we should change ``originalname``
- # to a readonly property that returns FunctionDefinition.name
+ # Note: when FunctionDefinition is introduced, we should change ``originalname``
+ # to a readonly property that returns FunctionDefinition.name.
self.keywords.update(self.obj.__dict__)
self.own_markers.extend(get_unpacked_marks(self.obj))
@classmethod
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
- """
- The public constructor
- """
+ """The public constructor."""
return super().from_parent(parent=parent, **kw)
def _initrequest(self) -> None:
@property
def function(self):
- "underlying python 'function' object"
+ """Underlying python 'function' object."""
return getimfunc(self.obj)
def _getobj(self):
@property
def _pyfuncitem(self):
- "(compatonly) for code expecting pytest-2.2 style request objects"
+ """(compatonly) for code expecting pytest-2.2 style request objects."""
return self
- @property
- def funcargnames(self) -> List[str]:
- """ alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
- warnings.warn(FUNCARGNAMES, stacklevel=2)
- return self.fixturenames
-
def runtest(self) -> None:
- """ execute the underlying test function. """
+ """Execute the underlying test function."""
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
def setup(self) -> None:
self.obj = self._getobj()
self._request._fillfixtures()
- def _prunetraceback(self, excinfo: ExceptionInfo) -> None:
+ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
code = _pytest._code.Code(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno
excinfo.traceback = ntraceback.filter()
# issue364: mark all but first and last frames to
- # only show a single-line message for each frame
+ # only show a single-line message for each frame.
if self.config.getoption("tbstyle", "auto") == "auto":
if len(excinfo.traceback) > 2:
for entry in excinfo.traceback[1:-1]:
class FunctionDefinition(Function):
- """
- internal hack until we get actual definition nodes instead of the
- crappy metafunc hack
- """
+ """Internal hack until we get actual definition nodes instead of the
+ crappy metafunc hack."""
def runtest(self) -> None:
raise RuntimeError("function definitions are not supposed to be used")
-import inspect
import math
import pprint
from collections.abc import Iterable
from collections.abc import Mapping
from collections.abc import Sized
from decimal import Decimal
-from itertools import filterfalse
from numbers import Number
from types import TracebackType
from typing import Any
from typing import TypeVar
from typing import Union
-from more_itertools.more import always_iterable
-
import _pytest._code
+from _pytest.compat import final
from _pytest.compat import overload
from _pytest.compat import STRING_TYPES
from _pytest.compat import TYPE_CHECKING
from typing import Type
-BASE_TYPE = (type, STRING_TYPES)
-
-
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
at_str = " at {}".format(at) if at else ""
return TypeError(
class ApproxBase:
- """
- Provide shared utilities for making approximate comparisons between numbers
- or sequences of numbers.
- """
+ """Provide shared utilities for making approximate comparisons between
+ numbers or sequences of numbers."""
# Tell numpy to use our `__eq__` operator instead of its.
__array_ufunc__ = None
return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
def _yield_comparisons(self, actual):
- """
- Yield all the pairs of numbers to be compared. This is used to
- implement the `__eq__` method.
+ """Yield all the pairs of numbers to be compared.
+
+ This is used to implement the `__eq__` method.
"""
raise NotImplementedError
def _check_type(self) -> None:
- """
- Raise a TypeError if the expected value is not a valid type.
- """
+ """Raise a TypeError if the expected value is not a valid type."""
# This is only a concern if the expected value is a sequence. In every
# other case, the approx() function ensures that the expected value has
# a numeric type. For this reason, the default is to do nothing. The
class ApproxNumpy(ApproxBase):
- """
- Perform approximate comparisons where the expected value is numpy array.
- """
+ """Perform approximate comparisons where the expected value is numpy array."""
def __repr__(self) -> str:
list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist())
def __eq__(self, actual) -> bool:
import numpy as np
- # self.expected is supposed to always be an array here
+ # self.expected is supposed to always be an array here.
if not np.isscalar(actual):
try:
class ApproxMapping(ApproxBase):
- """
- Perform approximate comparisons where the expected value is a mapping with
- numeric values (the keys can be anything).
- """
+ """Perform approximate comparisons where the expected value is a mapping
+ with numeric values (the keys can be anything)."""
def __repr__(self) -> str:
return "approx({!r})".format(
class ApproxSequencelike(ApproxBase):
- """
- Perform approximate comparisons where the expected value is a sequence of
- numbers.
- """
+ """Perform approximate comparisons where the expected value is a sequence of numbers."""
def __repr__(self) -> str:
seq_type = type(self.expected)
class ApproxScalar(ApproxBase):
- """
- Perform approximate comparisons where the expected value is a single number.
- """
+ """Perform approximate comparisons where the expected value is a single number."""
# Using Real should be better than this Union, but not possible yet:
# https://github.com/python/typeshed/pull/3108
DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal]
def __repr__(self) -> str:
- """
- Return a string communicating both the expected value and the tolerance
- for the comparison being made, e.g. '1.0 ± 1e-6', '(3+4j) ± 5e-6 ∠ ±180°'.
+ """Return a string communicating both the expected value and the
+ tolerance for the comparison being made.
+
+ For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
"""
# Infinities aren't compared using tolerances, so don't show a
- # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j)
+ # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j).
if math.isinf(abs(self.expected)):
return str(self.expected)
return "{} ± {}".format(self.expected, vetted_tolerance)
def __eq__(self, actual) -> bool:
- """
- Return true if the given value is equal to the expected value within
- the pre-specified tolerance.
- """
+ """Return whether the given value is equal to the expected value
+ within the pre-specified tolerance."""
if _is_numpy_array(actual):
# Call ``__eq__()`` manually to prevent infinite-recursion with
# numpy<1.13. See #3748.
@property
def tolerance(self):
- """
- Return the tolerance for the comparison. This could be either an
- absolute tolerance or a relative tolerance, depending on what the user
- specified or which would be larger.
+ """Return the tolerance for the comparison.
+
+ This could be either an absolute tolerance or a relative tolerance,
+ depending on what the user specified or which would be larger.
"""
def set_default(x, default):
class ApproxDecimal(ApproxScalar):
- """
- Perform approximate comparisons where the expected value is a decimal.
- """
+ """Perform approximate comparisons where the expected value is a Decimal."""
DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
- """
- Assert that two numbers (or two sets of numbers) are equal to each other
+ """Assert that two numbers (or two sets of numbers) are equal to each other
within some tolerance.
Due to the `intricacies of floating-point arithmetic`__, numbers that we
elif (
isinstance(expected, Iterable)
and isinstance(expected, Sized)
- and not isinstance(expected, STRING_TYPES)
+ # Type ignored because the error is wrong -- not unreachable.
+ and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
):
cls = ApproxSequencelike
else:
def _is_numpy_array(obj: object) -> bool:
- """
- Return true if the given object is a numpy array. Make a special effort to
- avoid importing numpy unless it's really necessary.
+ """Return true if the given object is a numpy array.
+
+ A special effort is made to avoid importing numpy unless it's really necessary.
"""
import sys
def raises(
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
*,
- match: "Optional[Union[str, Pattern]]" = ...
+ match: "Optional[Union[str, Pattern[str]]]" = ...
) -> "RaisesContext[_E]":
- ... # pragma: no cover
+ ...
@overload # noqa: F811
def raises( # noqa: F811
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
- func: Callable,
+ func: Callable[..., Any],
*args: Any,
**kwargs: Any
) -> _pytest._code.ExceptionInfo[_E]:
- ... # pragma: no cover
+ ...
def raises( # noqa: F811
*args: Any,
**kwargs: Any
) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]:
- r"""
- Assert that a code block/function call raises ``expected_exception``
+ r"""Assert that a code block/function call raises ``expected_exception``
or raise a failure exception otherwise.
- :kwparam match: if specified, a string containing a regular expression,
+ :kwparam match:
+ If specified, a string containing a regular expression,
or a regular expression object, that is tested against the string
representation of the exception using ``re.search``. To match a literal
string that may contain `special characters`__, the pattern can
documentation for :ref:`the try statement <python:try>`.
"""
__tracebackhide__ = True
- for exc in filterfalse(
- inspect.isclass, always_iterable(expected_exception, BASE_TYPE)
- ):
- msg = "exceptions must be derived from BaseException, not %s"
- raise TypeError(msg % type(exc))
+
+ if isinstance(expected_exception, type):
+ excepted_exceptions = (expected_exception,) # type: Tuple[Type[_E], ...]
+ else:
+ excepted_exceptions = expected_exception
+ for exc in excepted_exceptions:
+ if not isinstance(exc, type) or not issubclass(exc, BaseException): # type: ignore[unreachable]
+ msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
+ not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
+ raise TypeError(msg.format(not_a))
message = "DID NOT RAISE {}".format(expected_exception)
if not args:
- match = kwargs.pop("match", None)
+ match = kwargs.pop("match", None) # type: Optional[Union[str, Pattern[str]]]
if kwargs:
msg = "Unexpected keyword arguments passed to pytest.raises: "
msg += ", ".join(sorted(kwargs))
raises.Exception = fail.Exception # type: ignore
+@final
class RaisesContext(Generic[_E]):
def __init__(
self,
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
message: str,
- match_expr: Optional[Union[str, "Pattern"]] = None,
+ match_expr: Optional[Union[str, "Pattern[str]"]] = None,
) -> None:
self.expected_exception = expected_exception
self.message = message
-""" recording warnings during test function execution. """
+"""Record warnings during test function execution."""
import re
import warnings
from types import TracebackType
from typing import TypeVar
from typing import Union
+from _pytest.compat import final
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.fixtures import fixture
@overload
def deprecated_call(
- *, match: Optional[Union[str, "Pattern"]] = ...
+ *, match: Optional[Union[str, "Pattern[str]"]] = ...
) -> "WarningsRecorder":
- raise NotImplementedError()
+ ...
@overload # noqa: F811
def deprecated_call( # noqa: F811
func: Callable[..., T], *args: Any, **kwargs: Any
) -> T:
- raise NotImplementedError()
+ ...
def deprecated_call( # noqa: F811
- func: Optional[Callable] = None, *args: Any, **kwargs: Any
+ func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any
) -> Union["WarningsRecorder", Any]:
"""Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``.
def warns(
expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]],
*,
- match: "Optional[Union[str, Pattern]]" = ...
+ match: "Optional[Union[str, Pattern[str]]]" = ...
) -> "WarningsChecker":
- raise NotImplementedError()
+ ...
@overload # noqa: F811
*args: Any,
**kwargs: Any
) -> T:
- raise NotImplementedError()
+ ...
def warns( # noqa: F811
expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]],
*args: Any,
- match: Optional[Union[str, "Pattern"]] = None,
+ match: Optional[Union[str, "Pattern[str]"]] = None,
**kwargs: Any
) -> Union["WarningsChecker", Any]:
r"""Assert that code raises a particular class of warning.
self._entered = False
+@final
class WarningsChecker(WarningsRecorder):
def __init__(
self,
expected_warning: Optional[
Union["Type[Warning]", Tuple["Type[Warning]", ...]]
] = None,
- match_expr: Optional[Union[str, "Pattern"]] = None,
+ match_expr: Optional[Union[str, "Pattern[str]"]] = None,
) -> None:
super().__init__()
from io import StringIO
from pprint import pprint
from typing import Any
+from typing import cast
from typing import Dict
from typing import Iterable
from typing import Iterator
from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import ExceptionRepr
from _pytest._code.code import ReprEntry
from _pytest._code.code import ReprEntryNative
from _pytest._code.code import ReprExceptionInfo
from _pytest._code.code import ReprTraceback
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
+from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.nodes import Collector
class BaseReport:
when = None # type: Optional[str]
location = None # type: Optional[Tuple[str, Optional[int], str]]
- # TODO: Improve this Any.
- longrepr = None # type: Optional[Any]
+ longrepr = (
+ None
+ ) # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr]
sections = [] # type: List[Tuple[str, str]]
nodeid = None # type: str
if TYPE_CHECKING:
# Can have arbitrary fields given to __init__().
def __getattr__(self, key: str) -> Any:
- raise NotImplementedError()
+ ...
def toterminal(self, out: TerminalWriter) -> None:
if hasattr(self, "node"):
return
if hasattr(longrepr, "toterminal"):
- longrepr.toterminal(out)
+ longrepr_terminal = cast(TerminalRepr, longrepr)
+ longrepr_terminal.toterminal(out)
else:
try:
s = str(longrepr)
@property
def longreprtext(self) -> str:
- """
- Read-only property that returns the full string representation
- of ``longrepr``.
+ """Read-only property that returns the full string representation of
+ ``longrepr``.
.. versionadded:: 3.0
"""
@property
def caplog(self) -> str:
- """Return captured log lines, if log capturing is enabled
+ """Return captured log lines, if log capturing is enabled.
.. versionadded:: 3.5
"""
@property
def capstdout(self) -> str:
- """Return captured text from stdout, if capturing is enabled
+ """Return captured text from stdout, if capturing is enabled.
.. versionadded:: 3.0
"""
@property
def capstderr(self) -> str:
- """Return captured text from stderr, if capturing is enabled
+ """Return captured text from stderr, if capturing is enabled.
.. versionadded:: 3.0
"""
@property
def count_towards_summary(self) -> bool:
- """
- **Experimental**
-
- ``True`` if this report should be counted towards the totals shown at the end of the
- test session: "1 passed, 1 failure, etc".
+ """**Experimental** Whether this report should be counted towards the
+ totals shown at the end of the test session: "1 passed, 1 failure, etc".
.. note::
@property
def head_line(self) -> Optional[str]:
- """
- **Experimental**
-
- Returns the head line shown with longrepr output for this report, more commonly during
- traceback representation during failures::
+ """**Experimental** The head line shown with longrepr output for this
+ report, more commonly during traceback representation during
+ failures::
________ Test.foo ________
return verbose
def _to_json(self) -> Dict[str, Any]:
- """
- This was originally the serialize_report() function from xdist (ca03269).
+ """Return the contents of this report as a dict of builtin entries,
+ suitable for serialization.
- Returns the contents of this report as a dict of builtin entries, suitable for
- serialization.
+ This was originally the serialize_report() function from xdist (ca03269).
Experimental method.
"""
@classmethod
def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R:
- """
- This was originally the serialize_report() function from xdist (ca03269).
+ """Create either a TestReport or CollectReport, depending on the calling class.
- Factory method that returns either a TestReport or CollectReport, depending on the calling
- class. It's the callers responsibility to know which class to pass here.
+ It is the callers responsibility to know which class to pass here.
+
+ This was originally the serialize_report() function from xdist (ca03269).
Experimental method.
"""
raise RuntimeError(stream.getvalue())
+@final
class TestReport(BaseReport):
- """ Basic test report object (also used for setup and teardown calls if
- they fail).
- """
+ """Basic test report object (also used for setup and teardown calls if
+ they fail)."""
__test__ = False
location: Tuple[str, Optional[int], str],
keywords,
outcome: "Literal['passed', 'failed', 'skipped']",
- longrepr,
+ longrepr: Union[
+ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
+ ],
when: "Literal['setup', 'call', 'teardown']",
sections: Iterable[Tuple[str, str]] = (),
duration: float = 0,
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
**extra
) -> None:
- #: normalized collection node id
+ #: Normalized collection nodeid.
self.nodeid = nodeid
- #: a (filesystempath, lineno, domaininfo) tuple indicating the
+ #: A (filesystempath, lineno, domaininfo) tuple indicating the
#: actual location of a test item - it might be different from the
#: collected one e.g. if a method is inherited from a different module.
self.location = location # type: Tuple[str, Optional[int], str]
- #: a name -> value dictionary containing all keywords and
+ #: A name -> value dictionary containing all keywords and
#: markers associated with a test invocation.
self.keywords = keywords
- #: test outcome, always one of "passed", "failed", "skipped".
+ #: Test outcome, always one of "passed", "failed", "skipped".
self.outcome = outcome
#: None or a failure representation.
self.longrepr = longrepr
- #: one of 'setup', 'call', 'teardown' to indicate runtest phase.
+ #: One of 'setup', 'call', 'teardown' to indicate runtest phase.
self.when = when
- #: user properties is a list of tuples (name, value) that holds user
- #: defined properties of the test
+ #: User properties is a list of tuples (name, value) that holds user
+ #: defined properties of the test.
self.user_properties = list(user_properties or [])
- #: list of pairs ``(str, str)`` of extra information which needs to
+ #: List of pairs ``(str, str)`` of extra information which needs to
#: marshallable. Used by pytest to add captured text
#: from ``stdout`` and ``stderr``, but may be used by other plugins
#: to add arbitrary information to reports.
self.sections = list(sections)
- #: time it took to run just the test
+ #: Time it took to run just the test.
self.duration = duration
self.__dict__.update(extra)
@classmethod
def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
- """
- Factory method to create and fill a TestReport with standard item and call info.
- """
+ """Create and fill a TestReport with standard item and call info."""
when = call.when
# Remove "collect" from the Literal type -- only for collection calls.
assert when != "collect"
sections = []
if not call.excinfo:
outcome = "passed" # type: Literal["passed", "failed", "skipped"]
- # TODO: Improve this Any.
- longrepr = None # type: Optional[Any]
+ longrepr = (
+ None
+ ) # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr]
else:
if not isinstance(excinfo, ExceptionInfo):
outcome = "failed"
)
+@final
class CollectReport(BaseReport):
"""Collection report object."""
sections: Iterable[Tuple[str, str]] = (),
**extra
) -> None:
- #: normalized collection node id
+ #: Normalized collection nodeid.
self.nodeid = nodeid
- #: test outcome, always one of "passed", "failed", "skipped".
+ #: Test outcome, always one of "passed", "failed", "skipped".
self.outcome = outcome
#: None or a failure representation.
#: The collected items and collection nodes.
self.result = result or []
- #: list of pairs ``(str, str)`` of extra information which needs to
- #: marshallable. Used by pytest to add captured text
- #: from ``stdout`` and ``stderr``, but may be used by other plugins
- #: to add arbitrary information to reports.
+ #: List of pairs ``(str, str)`` of extra information which needs to
+ #: marshallable.
+ # Used by pytest to add captured text : from ``stdout`` and ``stderr``,
+ # but may be used by other plugins : to add arbitrary information to
+ # reports.
self.sections = list(sections)
self.__dict__.update(extra)
class CollectErrorRepr(TerminalRepr):
- def __init__(self, msg) -> None:
+ def __init__(self, msg: str) -> None:
self.longrepr = msg
def toterminal(self, out: TerminalWriter) -> None:
data = report._to_json()
data["$report_type"] = report.__class__.__name__
return data
- return None
+ # TODO: Check if this is actually reachable.
+ return None # type: ignore[unreachable]
def pytest_report_from_serializable(
def _report_to_json(report: BaseReport) -> Dict[str, Any]:
- """
- This was originally the serialize_report() function from xdist (ca03269).
+ """Return the contents of this report as a dict of builtin entries,
+ suitable for serialization.
- Returns the contents of this report as a dict of builtin entries, suitable for
- serialization.
+ This was originally the serialize_report() function from xdist (ca03269).
"""
def serialize_repr_entry(
else:
return None
- def serialize_longrepr(rep: BaseReport) -> Dict[str, Any]:
+ def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
assert rep.longrepr is not None
+ # TODO: Investigate whether the duck typing is really necessary here.
+ longrepr = cast(ExceptionRepr, rep.longrepr)
result = {
- "reprcrash": serialize_repr_crash(rep.longrepr.reprcrash),
- "reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback),
- "sections": rep.longrepr.sections,
+ "reprcrash": serialize_repr_crash(longrepr.reprcrash),
+ "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
+ "sections": longrepr.sections,
} # type: Dict[str, Any]
- if isinstance(rep.longrepr, ExceptionChainRepr):
+ if isinstance(longrepr, ExceptionChainRepr):
result["chain"] = []
- for repr_traceback, repr_crash, description in rep.longrepr.chain:
+ for repr_traceback, repr_crash, description in longrepr.chain:
result["chain"].append(
(
serialize_repr_traceback(repr_traceback),
if hasattr(report.longrepr, "reprtraceback") and hasattr(
report.longrepr, "reprcrash"
):
- d["longrepr"] = serialize_longrepr(report)
+ d["longrepr"] = serialize_exception_longrepr(report)
else:
d["longrepr"] = str(report.longrepr)
else:
def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
- """
- This was originally the serialize_report() function from xdist (ca03269).
+ """Return **kwargs that can be used to construct a TestReport or
+ CollectReport instance.
- Returns **kwargs that can be used to construct a TestReport or CollectReport instance.
+ This was originally the serialize_report() function from xdist (ca03269).
"""
def deserialize_repr_entry(entry_data):
]
return ReprTraceback(**repr_traceback_dict)
- def deserialize_repr_crash(repr_crash_dict: Optional[dict]):
+ def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]):
if repr_crash_dict is not None:
return ReprFileLocation(**repr_crash_dict)
else:
+++ /dev/null
-""" log machine-parseable test session result information in a plain
-text file.
-"""
-import os
-
-import py
-
-from _pytest._code.code import ExceptionRepr
-from _pytest.config import Config
-from _pytest.config.argparsing import Parser
-from _pytest.reports import CollectReport
-from _pytest.reports import TestReport
-from _pytest.store import StoreKey
-
-
-resultlog_key = StoreKey["ResultLog"]()
-
-
-def pytest_addoption(parser: Parser) -> None:
- group = parser.getgroup("terminal reporting", "resultlog plugin options")
- group.addoption(
- "--resultlog",
- "--result-log",
- action="store",
- metavar="path",
- default=None,
- help="DEPRECATED path for machine-readable result log.",
- )
-
-
-def pytest_configure(config: Config) -> None:
- resultlog = config.option.resultlog
- # prevent opening resultlog on worker nodes (xdist)
- if resultlog and not hasattr(config, "workerinput"):
- dirname = os.path.dirname(os.path.abspath(resultlog))
- if not os.path.isdir(dirname):
- os.makedirs(dirname)
- logfile = open(resultlog, "w", 1) # line buffered
- config._store[resultlog_key] = ResultLog(config, logfile)
- config.pluginmanager.register(config._store[resultlog_key])
-
- from _pytest.deprecated import RESULT_LOG
- from _pytest.warnings import _issue_warning_captured
-
- _issue_warning_captured(RESULT_LOG, config.hook, stacklevel=2)
-
-
-def pytest_unconfigure(config: Config) -> None:
- resultlog = config._store.get(resultlog_key, None)
- if resultlog:
- resultlog.logfile.close()
- del config._store[resultlog_key]
- config.pluginmanager.unregister(resultlog)
-
-
-class ResultLog:
- def __init__(self, config, logfile):
- self.config = config
- self.logfile = logfile # preferably line buffered
-
- def write_log_entry(self, testpath, lettercode, longrepr):
- print("{} {}".format(lettercode, testpath), file=self.logfile)
- for line in longrepr.splitlines():
- print(" %s" % line, file=self.logfile)
-
- def log_outcome(self, report, lettercode, longrepr):
- testpath = getattr(report, "nodeid", None)
- if testpath is None:
- testpath = report.fspath
- self.write_log_entry(testpath, lettercode, longrepr)
-
- def pytest_runtest_logreport(self, report: TestReport) -> None:
- if report.when != "call" and report.passed:
- return
- res = self.config.hook.pytest_report_teststatus(
- report=report, config=self.config
- )
- code = res[1]
- if code == "x":
- longrepr = str(report.longrepr)
- elif code == "X":
- longrepr = ""
- elif report.passed:
- longrepr = ""
- elif report.skipped:
- assert report.longrepr is not None
- longrepr = str(report.longrepr[2])
- else:
- longrepr = str(report.longrepr)
- self.log_outcome(report, code, longrepr)
-
- def pytest_collectreport(self, report: CollectReport) -> None:
- if not report.passed:
- if report.failed:
- code = "F"
- longrepr = str(report.longrepr)
- else:
- assert report.skipped
- code = "S"
- longrepr = "%s:%d: %s" % report.longrepr # type: ignore
- self.log_outcome(report, code, longrepr)
-
- def pytest_internalerror(self, excrepr: ExceptionRepr) -> None:
- if excrepr.reprcrash is not None:
- path = excrepr.reprcrash.path
- else:
- path = "cwd:%s" % py.path.local()
- self.write_log_entry(path, "!", str(excrepr))
-""" basic collect and runtest protocol implementations """
+"""Basic collect and runtest protocol implementations."""
import bdb
import os
import sys
-from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from _pytest import timing
from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
+from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config.argparsing import Parser
from _pytest.nodes import Collector
from _pytest.terminal import TerminalReporter
#
-# pytest plugin hooks
+# pytest plugin hooks.
def pytest_addoption(parser: Parser) -> None:
metavar="N",
help="show N slowest setup/test durations (N=0 for all).",
)
+ group.addoption(
+ "--durations-min",
+ action="store",
+ type=float,
+ default=0.005,
+ metavar="N",
+ help="Minimal duration in seconds for inclusion in slowest list. Default 0.005",
+ )
def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
durations = terminalreporter.config.option.durations
+ durations_min = terminalreporter.config.option.durations_min
verbose = terminalreporter.config.getvalue("verbose")
if durations is None:
return
dlist = dlist[:durations]
for i, rep in enumerate(dlist):
- if verbose < 2 and rep.duration < 0.005:
+ if verbose < 2 and rep.duration < durations_min:
tr.write_line("")
tr.write_line(
- "(%s durations < 0.005s hidden. Use -vv to show these durations.)"
- % (len(dlist) - i)
+ "(%s durations < %gs hidden. Use -vv to show these durations.)"
+ % (len(dlist) - i, durations_min)
)
break
tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid))
if not item.config.getoption("setuponly", False):
reports.append(call_and_report(item, "call", log))
reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
- # after all teardown hooks have been called
- # want funcargs and request info to go away
+ # After all teardown hooks have been called
+ # want funcargs and request info to go away.
if hasrequest:
item._request = False # type: ignore[attr-defined]
item.funcargs = None # type: ignore[attr-defined]
def _update_current_test_var(
item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
) -> None:
- """
- Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
+ """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
"""
return report
-def check_interactive_exception(call: "CallInfo", report: BaseReport) -> bool:
+def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool:
"""Check whether the call raised an exception that should be reported as
interactive."""
if call.excinfo is None:
)
-_T = TypeVar("_T")
+TResult = TypeVar("TResult", covariant=True)
+@final
@attr.s(repr=False)
-class CallInfo(Generic[_T]):
- """ Result/Exception info a function invocation.
-
- :param T result: The return value of the call, if it didn't raise. Can only be accessed
- if excinfo is None.
- :param Optional[ExceptionInfo] excinfo: The captured exception of the call, if it raised.
- :param float start: The system time when the call started, in seconds since the epoch.
- :param float stop: The system time when the call ended, in seconds since the epoch.
- :param float duration: The call duration, in seconds.
- :param str when: The context of invocation: "setup", "call", "teardown", ...
+class CallInfo(Generic[TResult]):
+ """Result/Exception info a function invocation.
+
+ :param T result:
+ The return value of the call, if it didn't raise. Can only be
+ accessed if excinfo is None.
+ :param Optional[ExceptionInfo] excinfo:
+ The captured exception of the call, if it raised.
+ :param float start:
+ The system time when the call started, in seconds since the epoch.
+ :param float stop:
+ The system time when the call ended, in seconds since the epoch.
+ :param float duration:
+ The call duration, in seconds.
+ :param str when:
+ The context of invocation: "setup", "call", "teardown", ...
"""
- _result = attr.ib(type="Optional[_T]")
+ _result = attr.ib(type="Optional[TResult]")
excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]])
start = attr.ib(type=float)
stop = attr.ib(type=float)
when = attr.ib(type="Literal['collect', 'setup', 'call', 'teardown']")
@property
- def result(self) -> _T:
+ def result(self) -> TResult:
if self.excinfo is not None:
raise AttributeError("{!r} has no valid result".format(self))
# The cast is safe because an exception wasn't raised, hence
# _result has the expected function return type (which may be
# None, that's why a cast and not an assert).
- return cast(_T, self._result)
+ return cast(TResult, self._result)
@classmethod
def from_call(
cls,
- func: "Callable[[], _T]",
+ func: "Callable[[], TResult]",
when: "Literal['collect', 'setup', 'call', 'teardown']",
reraise: "Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]]" = None,
- ) -> "CallInfo[_T]":
+ ) -> "CallInfo[TResult]":
excinfo = None
start = timing.time()
precise_start = timing.perf_counter()
try:
- result = func() # type: Optional[_T]
+ result = func() # type: Optional[TResult]
except BaseException:
excinfo = ExceptionInfo.from_current()
if reraise is not None and isinstance(excinfo.value, reraise):
def pytest_make_collect_report(collector: Collector) -> CollectReport:
call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
- # TODO: Better typing for longrepr.
- longrepr = None # type: Optional[Any]
+ longrepr = None # type: Union[None, Tuple[str, int, str], str, TerminalRepr]
if not call.excinfo:
outcome = "passed" # type: Literal["passed", "skipped", "failed"]
else:
outcome = "failed"
errorinfo = collector.repr_failure(call.excinfo)
if not hasattr(errorinfo, "toterminal"):
+ assert isinstance(errorinfo, str)
errorinfo = CollectErrorRepr(errorinfo)
longrepr = errorinfo
result = call.result if not call.excinfo else None
class SetupState:
- """ shared state for setting up/tearing down test items or collectors. """
+ """Shared state for setting up/tearing down test items or collectors."""
def __init__(self):
self.stack = [] # type: List[Node]
self._finalizers = {} # type: Dict[Node, List[Callable[[], object]]]
def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None:
- """ attach a finalizer to the given colitem. """
+ """Attach a finalizer to the given colitem."""
assert colitem and not isinstance(colitem, tuple)
assert callable(finalizer)
# assert colitem in self.stack # some unit tests don't setup stack :/
def prepare(self, colitem) -> None:
"""Setup objects along the collector chain to the test-method."""
- # check if the last collection node has raised an error
+ # Check if the last collection node has raised an error.
for col in self.stack:
if hasattr(col, "_prepare_exc"):
exc = col._prepare_exc # type: ignore[attr-defined]
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(
- fixturedef: FixtureDef, request: SubRequest
+ fixturedef: FixtureDef[object], request: SubRequest
) -> Generator[None, None, None]:
yield
if request.config.option.setupshow:
_show_fixture_action(fixturedef, "SETUP")
-def pytest_fixture_post_finalizer(fixturedef: FixtureDef) -> None:
+def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None:
if fixturedef.cached_result is not None:
config = fixturedef._fixturemanager.config
if config.option.setupshow:
del fixturedef.cached_param # type: ignore[attr-defined]
-def _show_fixture_action(fixturedef: FixtureDef, msg: str) -> None:
+def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None:
config = fixturedef._fixturemanager.config
capman = config.pluginmanager.getplugin("capturemanager")
if capman:
@pytest.hookimpl(tryfirst=True)
def pytest_fixture_setup(
- fixturedef: FixtureDef, request: SubRequest
+ fixturedef: FixtureDef[object], request: SubRequest
) -> Optional[object]:
# Will return a dummy fixture if the setuponly option is provided.
if request.config.option.setupplan:
-""" support for skip/xfail functions and markers. """
+"""Support for skip/xfail functions and markers."""
import os
import platform
import sys
and rep.skipped
and type(rep.longrepr) is tuple
):
- # skipped by mark.skipif; change the location of the failure
+ # Skipped by mark.skipif; change the location of the failure
# to point to the item definition, otherwise it will display
- # the location of where the skip exception was raised within pytest
+ # the location of where the skip exception was raised within pytest.
_, _, reason = rep.longrepr
filename, line = item.reportinfo()[:2]
assert line is not None
def __getitem__(self, key: StoreKey[T]) -> T:
"""Get the value for key.
- Raises KeyError if the key wasn't set before.
+ Raises ``KeyError`` if the key wasn't set before.
"""
return cast(T, self._store[key])
def __delitem__(self, key: StoreKey[T]) -> None:
"""Delete the value for key.
- Raises KeyError if the key wasn't set before.
+ Raises ``KeyError`` if the key wasn't set before.
"""
del self._store[key]
def __contains__(self, key: StoreKey[T]) -> bool:
- """Returns whether key was set."""
+ """Return whether key was set."""
return key in self._store
-""" terminal reporting of the full testing process.
+"""Terminal reporting of the full testing process.
This is a good source for looking at the various reporting hooks.
"""
import attr
import pluggy
import py
-from more_itertools import collapse
import pytest
from _pytest import nodes
from _pytest import timing
from _pytest._code import ExceptionInfo
from _pytest._code.code import ExceptionRepr
-from _pytest._io import TerminalWriter
from _pytest._io.wcwidth import wcswidth
+from _pytest.compat import final
from _pytest.compat import order_preserving_dict
from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config.argparsing import Parser
-from _pytest.deprecated import TERMINALWRITER_WRITER
from _pytest.nodes import Item
from _pytest.nodes import Node
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import Path
from _pytest.reports import BaseReport
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
class MoreQuietAction(argparse.Action):
- """
- a modified copy of the argparse count action which counts down and updates
- the legacy quiet attribute at the same time
+ """A modified copy of the argparse count action which counts down and updates
+ the legacy quiet attribute at the same time.
- used to unify verbosity handling
+ Used to unify verbosity handling.
"""
def __init__(
@attr.s
class WarningReport:
- """
- Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
+ """Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
- :ivar str message: user friendly message about the warning
- :ivar str|None nodeid: node id that generated the warning (see ``get_location``).
+ :ivar str message:
+ User friendly message about the warning.
+ :ivar str|None nodeid:
+ nodeid that generated the warning (see ``get_location``).
:ivar tuple|py.path.local fslocation:
- file system location of the source of the warning (see ``get_location``).
+ File system location of the source of the warning (see ``get_location``).
"""
message = attr.ib(type=str)
count_towards_summary = True
def get_location(self, config: Config) -> Optional[str]:
- """
- Returns the more user-friendly information about the location
- of a warning, or None.
- """
+ """Return the more user-friendly information about the location of a warning, or None."""
if self.nodeid:
return self.nodeid
if self.fslocation:
if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
filename, linenum = self.fslocation[:2]
- relpath = py.path.local(filename).relto(config.invocation_dir)
- if not relpath:
- relpath = str(filename)
+ relpath = bestrelpath(
+ config.invocation_params.dir, absolutepath(filename)
+ )
return "{}:{}".format(relpath, linenum)
else:
return str(self.fslocation)
return None
+@final
class TerminalReporter:
def __init__(self, config: Config, file: Optional[TextIO] = None) -> None:
import _pytest.config
self.stats = {} # type: Dict[str, List[Any]]
self._main_color = None # type: Optional[str]
- self._known_types = None # type: Optional[List]
+ self._known_types = None # type: Optional[List[str]]
self.startdir = config.invocation_dir
+ self.startpath = config.invocation_params.dir
if file is None:
file = sys.stdout
self._tw = _pytest.config.create_terminal_writer(config, file)
self._screen_width = self._tw.fullwidth
- self.currentfspath = None # type: Any
+ self.currentfspath = None # type: Union[None, Path, str, int]
self.reportchars = getreportopt(config)
self.hasmarkup = self._tw.hasmarkup
self.isatty = file.isatty()
self._already_displayed_warnings = None # type: Optional[int]
self._keyboardinterrupt_memo = None # type: Optional[ExceptionRepr]
- @property
- def writer(self) -> TerminalWriter:
- warnings.warn(TERMINALWRITER_WRITER, stacklevel=2)
- return self._tw
-
- @writer.setter
- def writer(self, value: TerminalWriter) -> None:
- warnings.warn(TERMINALWRITER_WRITER, stacklevel=2)
- self._tw = value
-
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
- """Return True if we should display progress information based on the current config"""
+ """Return whether we should display progress information based on the current config."""
# do not show progress if we are not capturing output (#3038)
if self.config.getoption("capture", "no") == "no":
return False
return char in self.reportchars
def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None:
- fspath = self.config.rootdir.join(nodeid.split("::")[0])
- # NOTE: explicitly check for None to work around py bug, and for less
- # overhead in general (https://github.com/pytest-dev/py/pull/207).
+ fspath = self.config.rootpath / nodeid.split("::")[0]
if self.currentfspath is None or fspath != self.currentfspath:
if self.currentfspath is not None and self._show_progress_info:
self._write_progress_information_filling_space()
self.currentfspath = fspath
- relfspath = self.startdir.bestrelpath(fspath)
+ relfspath = bestrelpath(self.startpath, fspath)
self._tw.line()
self._tw.write(relfspath + " ")
self._tw.write(res, flush=True, **markup)
- def write_ensure_prefix(self, prefix, extra: str = "", **kwargs) -> None:
+ def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None:
if self.currentfspath != prefix:
self._tw.line()
self.currentfspath = prefix
self._tw.line(line, **markup)
def rewrite(self, line: str, **markup: bool) -> None:
- """
- Rewinds the terminal cursor to the beginning and writes the given line.
+ """Rewinds the terminal cursor to the beginning and writes the given line.
- :kwarg erase: if True, will also add spaces until the full terminal width to ensure
+ :param erase:
+ If True, will also add spaces until the full terminal width to ensure
previous lines are properly erased.
The rest of the keyword arguments are markup instructions.
def line(self, msg: str, **kw: bool) -> None:
self._tw.line(msg, **kw)
- def _add_stats(self, category: str, items: Sequence) -> None:
+ def _add_stats(self, category: str, items: Sequence[Any]) -> None:
set_main_color = category not in self.stats
self.stats.setdefault(category, []).extend(items)
if set_main_color:
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
if self.config.option.traceconfig:
msg = "PLUGIN registered: {}".format(plugin)
- # XXX this event may happen during setup/teardown time
+ # XXX This event may happen during setup/teardown time
# which unfortunately captures our output here
- # which garbles our output if we use self.write_line
+ # which garbles our output if we use self.write_line.
self.write_line(msg)
def pytest_deselected(self, items: Sequence[Item]) -> None:
def pytest_runtest_logstart(
self, nodeid: str, location: Tuple[str, Optional[int], str]
) -> None:
- # ensure that the path is printed before the
- # 1st test of a module starts running
+ # Ensure that the path is printed before the
+ # 1st test of a module starts running.
if self.showlongtestinfo:
line = self._locationline(nodeid, *location)
self.write_ensure_prefix(line, "")
rep = report
res = self.config.hook.pytest_report_teststatus(
report=rep, config=self.config
- ) # type: Tuple[str, str, str]
+ ) # type: Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]
category, letter, word = res
- if isinstance(word, tuple):
- word, markup = word
- else:
+ if not isinstance(word, tuple):
markup = None
+ else:
+ word, markup = word
self._add_stats(category, [rep])
if not letter and not word:
- # probably passed setup/teardown
+ # Probably passed setup/teardown.
return
running_xdist = hasattr(rep, "node")
if markup is None:
@property
def _width_of_current_line(self) -> int:
- """Return the width of current line, using the superior implementation of py-1.6 when available"""
+ """Return the width of the current line."""
return self._tw.width_of_current_line
def pytest_collection(self) -> None:
self._write_report_lines_from_hooks(lines)
def _write_report_lines_from_hooks(
- self, lines: List[Union[str, List[str]]]
+ self, lines: Sequence[Union[str, Sequence[str]]]
) -> None:
- lines.reverse()
- for line in collapse(lines):
- self.write_line(line)
+ for line_or_lines in reversed(lines):
+ if isinstance(line_or_lines, str):
+ self.write_line(line_or_lines)
+ else:
+ for line in line_or_lines:
+ self.write_line(line)
def pytest_report_header(self, config: Config) -> List[str]:
- line = "rootdir: %s" % config.rootdir
+ line = "rootdir: %s" % config.rootpath
- if config.inifile:
- line += ", configfile: " + config.rootdir.bestrelpath(config.inifile)
+ if config.inipath:
+ line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
testpaths = config.getini("testpaths")
if testpaths and config.args == testpaths:
- rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths]
+ rel_paths = [bestrelpath(config.rootpath, x) for x in testpaths]
line += ", testpaths: {}".format(", ".join(rel_paths))
result = [line]
rep.toterminal(self._tw)
def _printcollecteditems(self, items: Sequence[Item]) -> None:
- # to print out items and their parent collectors
+ # To print out items and their parent collectors
# we take care to leave out Instances aka ()
- # because later versions are going to get rid of them anyway
+ # because later versions are going to get rid of them anyway.
if self.config.option.verbose < 0:
if self.config.option.verbose < -1:
counts = {} # type: Dict[str, int]
line += "[".join(values)
return line
- # collect_fspath comes from testid which has a "/"-normalized path
+ # collect_fspath comes from testid which has a "/"-normalized path.
if fspath:
res = mkrel(nodeid)
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
"\\", nodes.SEP
):
- res += " <- " + self.startdir.bestrelpath(fspath)
+ res += " <- " + bestrelpath(self.startpath, fspath)
else:
res = "[location]"
return res + " "
return ""
#
- # summaries for sessionfinish
+ # Summaries for sessionfinish.
#
def getreports(self, name: str):
values = []
def show_skipped(lines: List[str]) -> None:
skipped = self.stats.get("skipped", []) # type: List[CollectReport]
- fskips = _folded_skips(self.startdir, skipped) if skipped else []
+ fskips = _folded_skips(self.startpath, skipped) if skipped else []
if not fskips:
return
verbose_word = skipped[0]._get_verbose_word(self.config)
def _folded_skips(
- startdir: py.path.local, skipped: Sequence[CollectReport],
+ startpath: Path, skipped: Sequence[CollectReport],
) -> List[Tuple[int, str, Optional[int], str]]:
d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]]
for event in skipped:
assert event.longrepr is not None
+ assert isinstance(event.longrepr, tuple), (event, event.longrepr)
assert len(event.longrepr) == 3, (event, event.longrepr)
fspath, lineno, reason = event.longrepr
# For consistency, report all fspaths in relative form.
- fspath = startdir.bestrelpath(py.path.local(fspath))
+ fspath = bestrelpath(startpath, Path(fspath))
keywords = getattr(event, "keywords", {})
- # folding reports with global pytestmark variable
- # this is workaround, because for now we cannot identify the scope of a skip marker
- # TODO: revisit after marks scope would be fixed
+ # Folding reports with global pytestmark variable.
+ # This is a workaround, because for now we cannot identify the scope of a skip marker
+ # TODO: Revisit after marks scope would be fixed.
if (
event.when == "setup"
and "skip" in keywords
def _plugin_nameversions(plugininfo) -> List[str]:
values = [] # type: List[str]
for plugin, dist in plugininfo:
- # gets us name and version!
+ # Gets us name and version!
name = "{dist.project_name}-{dist.version}".format(dist=dist)
- # questionable convenience, but it keeps things short
+ # Questionable convenience, but it keeps things short.
if name.startswith("pytest-"):
name = name[7:]
- # we decided to print python package names
- # they can have more than one plugin
+ # We decided to print python package names they can have more than one plugin.
if name not in values:
values.append(name)
return values
def format_session_duration(seconds: float) -> str:
- """Format the given seconds in a human readable manner to show in the final summary"""
+ """Format the given seconds in a human readable manner to show in the final summary."""
if seconds < 60:
return "{:.2f}s".format(seconds)
else:
-"""
-Indirection for time functions.
+"""Indirection for time functions.
We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
pytest runtime information (issue #185).
-""" support for providing temporary directories to test functions. """
+"""Support for providing temporary directories to test functions."""
import os
import re
import tempfile
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import Path
+from _pytest.compat import final
from _pytest.config import Config
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch
+@final
@attr.s
class TempPathFactory:
"""Factory for temporary directories under the common base temp directory.
- The base directory can be configured using the ``--basetemp`` option."""
+ The base directory can be configured using the ``--basetemp`` option.
+ """
_given_basetemp = attr.ib(
- type=Path,
- # using os.path.abspath() to get absolute path instead of resolve() as it
- # does not work the same in all platforms (see #4427)
- # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
+ type=Optional[Path],
+ # Use os.path.abspath() to get absolute path instead of resolve() as it
+ # does not work the same in all platforms (see #4427).
+ # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
# Ignore type because of https://github.com/python/mypy/issues/6172.
converter=attr.converters.optional(
lambda p: Path(os.path.abspath(str(p))) # type: ignore
_basetemp = attr.ib(type=Optional[Path], default=None)
@classmethod
- def from_config(cls, config) -> "TempPathFactory":
- """
- :param config: a pytest configuration
- """
+ def from_config(cls, config: Config) -> "TempPathFactory":
+ """Create a factory according to pytest configuration."""
return cls(
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
)
return basename
def mktemp(self, basename: str, numbered: bool = True) -> Path:
- """Creates a new temporary directory managed by the factory.
+ """Create a new temporary directory managed by the factory.
:param basename:
Directory base name, must be a relative path.
means that this function will create directories named ``"foo-0"``,
``"foo-1"``, ``"foo-2"`` and so on.
- :return:
+ :returns:
The path to the new directory.
"""
basename = self._ensure_relative_to_basetemp(basename)
return p
def getbasetemp(self) -> Path:
- """ return base temporary directory. """
+ """Return base temporary directory."""
if self._basetemp is not None:
return self._basetemp
return t
+@final
@attr.s
class TempdirFactory:
- """
- backward comptibility wrapper that implements
- :class:``py.path.local`` for :class:``TempPathFactory``
- """
+ """Backward comptibility wrapper that implements :class:``py.path.local``
+ for :class:``TempPathFactory``."""
_tmppath_factory = attr.ib(type=TempPathFactory)
def mktemp(self, basename: str, numbered: bool = True) -> py.path.local:
- """
- Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object.
- """
+ """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object."""
return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
def getbasetemp(self) -> py.path.local:
- """backward compat wrapper for ``_tmppath_factory.getbasetemp``"""
+ """Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
return py.path.local(self._tmppath_factory.getbasetemp().resolve())
def get_user() -> Optional[str]:
"""Return the current user name, or None if getuser() does not work
- in the current environment (see #1010).
- """
+ in the current environment (see #1010)."""
import getpass
try:
@pytest.fixture(scope="session")
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
- """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
- """
+ """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session."""
# Set dynamically by pytest_configure() above.
return request.config._tmpdirhandler # type: ignore
@pytest.fixture(scope="session")
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
- """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
- """
+ """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session."""
# Set dynamically by pytest_configure() above.
return request.config._tmp_path_factory # type: ignore
@pytest.fixture
def tmpdir(tmp_path: Path) -> py.path.local:
- """Return a temporary directory path object
- which is unique to each test function invocation,
- created as a sub directory of the base temporary
- directory. The returned object is a `py.path.local`_
- path object.
+ """Return a temporary directory path object which is unique to each test
+ function invocation, created as a sub directory of the base temporary
+ directory.
+
+ The returned object is a `py.path.local`_ path object.
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
"""
@pytest.fixture
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
- """Return a temporary directory path object
- which is unique to each test function invocation,
- created as a sub directory of the base temporary
- directory. The returned object is a :class:`pathlib.Path`
- object.
+ """Return a temporary directory path object which is unique to each test
+ function invocation, created as a sub directory of the base temporary
+ directory.
+
+ The returned object is a :class:`pathlib.Path` object.
.. note::
- in python < 3.6 this is a pathlib2.Path
+ In python < 3.6 this is a pathlib2.Path.
"""
return _mk_tmp(request, tmp_path_factory)
-""" discovery and running of std-library "unittest" style tests. """
+"""Discover and run std-library "unittest" style tests."""
import sys
import traceback
import types
def pytest_pycollect_makeitem(
collector: PyCollector, name: str, obj: object
) -> Optional["UnitTestCase"]:
- # has unittest been imported and is obj a subclass of its TestCase?
+ # Has unittest been imported and is obj a subclass of its TestCase?
try:
ut = sys.modules["unittest"]
# Type ignored because `ut` is an opaque module.
return None
except Exception:
return None
- # yes, so let's collect it
+ # Yes, so let's collect it.
item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase
return item
class UnitTestCase(Class):
- # marker for fixturemanger.getfixtureinfo()
- # to declare that our children do not support funcargs
+ # Marker for fixturemanger.getfixtureinfo()
+ # to declare that our children do not support funcargs.
nofuncargs = True
def collect(self) -> Iterable[Union[Item, Collector]]:
def _inject_setup_teardown_fixtures(self, cls: type) -> None:
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
- teardown functions (#517)"""
+ teardown functions (#517)."""
class_fixture = _make_xunit_fixture(
cls, "setUpClass", "tearDownClass", scope="class", pass_self=False
)
class TestCaseFunction(Function):
nofuncargs = True
- _excinfo = None # type: Optional[List[_pytest._code.ExceptionInfo]]
+ _excinfo = None # type: Optional[List[_pytest._code.ExceptionInfo[BaseException]]]
_testcase = None # type: Optional[unittest.TestCase]
def setup(self) -> None:
- # a bound method to be called during teardown() if set (see 'runtest()')
+ # A bound method to be called during teardown() if set (see 'runtest()').
self._explicit_tearDown = None # type: Optional[Callable[[], None]]
assert self.parent is not None
self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
pass
def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
- # unwrap potential exception info (see twisted trial support below)
+ # Unwrap potential exception info (see twisted trial support below).
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
try:
excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type]
- # invoke the attributes to trigger storing the traceback
- # trial causes some issue there
+ # Invoke the attributes to trigger storing the traceback
+ # trial causes some issue there.
excinfo.value
excinfo.traceback
except TypeError:
def _expecting_failure(self, test_method) -> bool:
"""Return True if the given unittest method (or the entire class) is marked
- with @expectedFailure"""
+ with @expectedFailure."""
expecting_failure_method = getattr(
test_method, "__unittest_expecting_failure__", False
)
maybe_wrap_pytest_function_for_tracing(self)
- # let the unittest framework handle async functions
+ # Let the unittest framework handle async functions.
if is_async_function(self.obj):
# Type ignored because self acts as the TestResult, but is not actually one.
self._testcase(result=self) # type: ignore[arg-type]
else:
- # when --pdb is given, we want to postpone calling tearDown() otherwise
+ # When --pdb is given, we want to postpone calling tearDown() otherwise
# when entering the pdb prompt, tearDown() would have probably cleaned up
- # instance variables, which makes it difficult to debug
- # arguably we could always postpone tearDown(), but this changes the moment where the
+ # instance variables, which makes it difficult to debug.
+ # Arguably we could always postpone tearDown(), but this changes the moment where the
# TestCase instance interacts with the results object, so better to only do it
- # when absolutely needed
+ # when absolutely needed.
if self.config.getoption("usepdb") and not _is_skipped(self.obj):
self._explicit_tearDown = self._testcase.tearDown
setattr(self._testcase, "tearDown", lambda *args: None)
- # we need to update the actual bound method with self.obj, because
- # wrap_pytest_function_for_tracing replaces self.obj by a wrapper
+ # We need to update the actual bound method with self.obj, because
+ # wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
setattr(self._testcase, self.name, self.obj)
try:
self._testcase(result=self) # type: ignore[arg-type]
finally:
delattr(self._testcase, self.name)
- def _prunetraceback(self, excinfo: _pytest._code.ExceptionInfo) -> None:
+ def _prunetraceback(
+ self, excinfo: _pytest._code.ExceptionInfo[BaseException]
+ ) -> None:
Function._prunetraceback(self, excinfo)
traceback = excinfo.traceback.filter(
lambda x: not x.frame.f_globals.get("__unittest")
and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
):
excinfo = call.excinfo
- # let's substitute the excinfo with a pytest.skip one
+ # Let's substitute the excinfo with a pytest.skip one.
call2 = CallInfo[None].from_call(
lambda: pytest.skip(str(excinfo.value)), call.when
)
call.excinfo = call2.excinfo
-# twisted trial support
+# Twisted trial support.
@hookimpl(hookwrapper=True)
def _is_skipped(obj) -> bool:
- """Return True if the given object has been marked with @unittest.skip"""
+ """Return True if the given object has been marked with @unittest.skip."""
return bool(getattr(obj, "__unittest_skip__", False))
import attr
+from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
if TYPE_CHECKING:
__module__ = "pytest"
+@final
class PytestAssertRewriteWarning(PytestWarning):
"""Warning emitted by the pytest assert rewrite module."""
__module__ = "pytest"
+@final
class PytestCacheWarning(PytestWarning):
"""Warning emitted by the cache plugin in various situations."""
__module__ = "pytest"
+@final
class PytestConfigWarning(PytestWarning):
"""Warning emitted for configuration issues."""
__module__ = "pytest"
+@final
class PytestCollectionWarning(PytestWarning):
"""Warning emitted when pytest is not able to collect a file or symbol in a module."""
__module__ = "pytest"
+@final
class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
"""Warning class for features that will be removed in a future version."""
__module__ = "pytest"
+@final
class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
"""Warning category used to denote experiments in pytest.
)
+@final
class PytestUnhandledCoroutineWarning(PytestWarning):
"""Warning emitted for an unhandled coroutine.
__module__ = "pytest"
+@final
class PytestUnknownMarkWarning(PytestWarning):
"""Warning emitted on use of unknown markers.
_W = TypeVar("_W", bound=PytestWarning)
+@final
@attr.s
class UnformattedWarning(Generic[_W]):
"""A warning meant to be formatted during runtime.
template = attr.ib(type=str)
def format(self, **kwargs: Any) -> _W:
- """Returns an instance of the warning category, formatted with given kwargs"""
+ """Return an instance of the warning category, formatted with given kwargs."""
return self.category(self.template.format(**kwargs))
-import re
import sys
import warnings
from contextlib import contextmanager
-from functools import lru_cache
from typing import Generator
from typing import Optional
-from typing import Tuple
import pytest
from _pytest.compat import TYPE_CHECKING
+from _pytest.config import apply_warning_filters
from _pytest.config import Config
-from _pytest.config.argparsing import Parser
+from _pytest.config import parse_warning_filter
from _pytest.main import Session
from _pytest.nodes import Item
from _pytest.terminal import TerminalReporter
if TYPE_CHECKING:
- from typing import Type
from typing_extensions import Literal
-@lru_cache(maxsize=50)
-def _parse_filter(
- arg: str, *, escape: bool
-) -> "Tuple[str, str, Type[Warning], str, int]":
- """Parse a warnings filter string.
-
- This is copied from warnings._setoption, but does not apply the filter,
- only parses it, and makes the escaping optional.
- """
- parts = arg.split(":")
- if len(parts) > 5:
- raise warnings._OptionError("too many fields (max 5): {!r}".format(arg))
- while len(parts) < 5:
- parts.append("")
- action_, message, category_, module, lineno_ = [s.strip() for s in parts]
- action = warnings._getaction(action_) # type: str # type: ignore[attr-defined]
- category = warnings._getcategory(
- category_
- ) # type: Type[Warning] # type: ignore[attr-defined]
- if message and escape:
- message = re.escape(message)
- if module and escape:
- module = re.escape(module) + r"\Z"
- if lineno_:
- try:
- lineno = int(lineno_)
- if lineno < 0:
- raise ValueError
- except (ValueError, OverflowError) as e:
- raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e
- else:
- lineno = 0
- return (action, message, category, module, lineno)
-
-
-def pytest_addoption(parser: Parser) -> None:
- group = parser.getgroup("pytest-warnings")
- group.addoption(
- "-W",
- "--pythonwarnings",
- action="append",
- help="set which warnings to report, see -W option of python itself.",
- )
- parser.addini(
- "filterwarnings",
- type="linelist",
- help="Each line specifies a pattern for "
- "warnings.filterwarnings. "
- "Processed after -W/--pythonwarnings.",
- )
-
-
def pytest_configure(config: Config) -> None:
config.addinivalue_line(
"markers",
when: "Literal['config', 'collect', 'runtest']",
item: Optional[Item],
) -> Generator[None, None, None]:
- """
- Context manager that catches warnings generated in the contained execution block.
+ """Context manager that catches warnings generated in the contained execution block.
``item`` can be None if we are not in the context of an item execution.
Each warning captured triggers the ``pytest_warning_recorded`` hook.
"""
- cmdline_filters = config.getoption("pythonwarnings") or []
- inifilters = config.getini("filterwarnings")
+ config_filters = config.getini("filterwarnings")
+ cmdline_filters = config.known_args_namespace.pythonwarnings or []
with warnings.catch_warnings(record=True) as log:
# mypy can't infer that record=True means log is not None; help it.
assert log is not None
if not sys.warnoptions:
- # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
+ # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).
warnings.filterwarnings("always", category=DeprecationWarning)
warnings.filterwarnings("always", category=PendingDeprecationWarning)
- warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning)
-
- # filters should have this precedence: mark, cmdline options, ini
- # filters should be applied in the inverse order of precedence
- for arg in inifilters:
- warnings.filterwarnings(*_parse_filter(arg, escape=False))
-
- for arg in cmdline_filters:
- warnings.filterwarnings(*_parse_filter(arg, escape=True))
+ apply_warning_filters(config_filters, cmdline_filters)
+ # apply filters from "filterwarnings" marks
nodeid = "" if item is None else item.nodeid
if item is not None:
for mark in item.iter_markers(name="filterwarnings"):
for arg in mark.args:
- warnings.filterwarnings(*_parse_filter(arg, escape=False))
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
yield
yield
-def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None:
- """
- This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
- at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded
- hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891.
-
- :param warning: the warning instance.
- :param hook: the hook caller
- :param stacklevel: stacklevel forwarded to warnings.warn
- """
- with warnings.catch_warnings(record=True) as records:
- warnings.simplefilter("always", type(warning))
- warnings.warn(warning, stacklevel=stacklevel)
- frame = sys._getframe(stacklevel - 1)
- location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
- hook.pytest_warning_captured.call_historic(
- kwargs=dict(
- warning_message=records[0], when="config", item=None, location=location
- )
- )
- hook.pytest_warning_recorded.call_historic(
- kwargs=dict(
- warning_message=records[0], when="config", nodeid="", location=location
- )
- )
+@pytest.hookimpl(hookwrapper=True)
+def pytest_load_initial_conftests(
+ early_config: "Config",
+) -> Generator[None, None, None]:
+ with catch_warnings_for_item(
+ config=early_config, ihook=early_config.hook, when="config", item=None
+ ):
+ yield
# PYTHON_ARGCOMPLETE_OK
-"""
-pytest: unit and functional testing with Python.
-"""
+"""pytest: unit and functional testing with Python."""
from . import collect
from _pytest import __version__
from _pytest.assertion import register_assert_rewrite
-"""
-pytest entry point
-"""
+"""The pytest entry point."""
import pytest
if __name__ == "__main__":
import sys
+import warnings
from types import ModuleType
from typing import Any
from typing import List
import pytest
-
+from _pytest.deprecated import PYTEST_COLLECT_MODULE
COLLECT_FAKEMODULE_ATTRIBUTES = [
"Collector",
def __getattr__(self, name: str) -> Any:
if name not in self.__all__:
raise AttributeError(name)
- # Uncomment this after 6.0 release (#7361)
- # warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2)
+ warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2)
return getattr(pytest, name)
def test_file_not_found(self, testdir):
result = testdir.runpytest("asd")
assert result.ret != 0
- result.stderr.fnmatch_lines(["ERROR: file not found*asd"])
+ result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"])
def test_file_not_found_unconfigure_issue143(self, testdir):
testdir.makeconftest(
)
result = testdir.runpytest("-s", "asd")
assert result.ret == ExitCode.USAGE_ERROR
- result.stderr.fnmatch_lines(["ERROR: file not found*asd"])
+ result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"])
result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"])
def test_config_preparse_plugin_option(self, testdir):
"E {}: No module named 'qwerty'".format(exc_name),
]
- @pytest.mark.filterwarnings("ignore::pytest.PytestDeprecationWarning")
def test_early_skip(self, testdir):
testdir.mkdir("xyz")
testdir.makeconftest(
"""
import pytest
- def pytest_collect_directory():
+ def pytest_collect_file():
pytest.skip("early")
"""
)
result.stdout.fnmatch_lines(["pytest_sessionfinish_called"])
assert result.ret == ExitCode.USAGE_ERROR
- @pytest.mark.usefixtures("recwarn")
def test_namespace_import_doesnt_confuse_import_hook(self, testdir):
- """
- Ref #383. Python 3.3's namespace package messed with our import hooks
+ """Ref #383.
+
+ Python 3.3's namespace package messed with our import hooks.
Importing a module that didn't exist, even if the ImportError was
gracefully handled, would make our test crash.
-
- Use recwarn here to silence this warning in Python 2.7:
- ImportWarning: Not importing directory '...\not_a_package': missing __init__.py
"""
testdir.mkdir("not_a_package")
p = testdir.makepyfile(
)
def test_plugins_given_as_strings(self, tmpdir, monkeypatch, _sys_snapshot):
- """test that str values passed to main() as `plugins` arg
- are interpreted as module names to be imported and registered.
- #855.
- """
+ """Test that str values passed to main() as `plugins` arg are
+ interpreted as module names to be imported and registered (#855)."""
with pytest.raises(ImportError) as excinfo:
pytest.main([str(tmpdir)], plugins=["invalid.module"])
assert "invalid" in str(excinfo.value)
):
pytest.main("-h") # type: ignore[arg-type]
- def test_invoke_with_path(self, tmpdir, capsys):
+ def test_invoke_with_path(self, tmpdir: py.path.local, capsys) -> None:
retcode = pytest.main(tmpdir)
assert retcode == ExitCode.NO_TESTS_COLLECTED
out, err = capsys.readouterr()
result.stderr.fnmatch_lines(["*not*found*test_missing*"])
def test_cmdline_python_namespace_package(self, testdir, monkeypatch):
- """
- test --pyargs option with namespace packages (#1567)
+ """Test --pyargs option with namespace packages (#1567).
Ref: https://packaging.python.org/guides/packaging-namespace-packages/
"""
def test_cmdline_python_package_not_exists(self, testdir):
result = testdir.runpytest("--pyargs", "tpkgwhatv")
assert result.ret
- result.stderr.fnmatch_lines(["ERROR*file*or*package*not*found*"])
+ result.stderr.fnmatch_lines(["ERROR*module*or*package*not*found*"])
@pytest.mark.xfail(reason="decide: feature or bug")
def test_noclass_discovery_if_not_testcase(self, testdir):
def test_deferred_hook_checking(testdir):
- """
- Check hooks as late as possible (#1821).
- """
+ """Check hooks as late as possible (#1821)."""
testdir.syspathinsert()
testdir.makepyfile(
**{
def test_fixture_order_respects_scope(testdir):
- """Ensure that fixtures are created according to scope order, regression test for #2405
- """
+ """Ensure that fixtures are created according to scope order (#2405)."""
testdir.makepyfile(
"""
import pytest
def test_frame_leak_on_failing_test(testdir):
- """pytest would leak garbage referencing the frames of tests that failed that could never be reclaimed (#2798)
+ """Pytest would leak garbage referencing the frames of tests that failed
+ that could never be reclaimed (#2798).
Unfortunately it was not possible to remove the actual circles because most of them
are made of traceback objects which cannot be weakly referenced. Those objects at least
assert lines[1] == " pass"
def test_repr_source_excinfo(self) -> None:
- """ check if indentation is right """
+ """Check if indentation is right."""
try:
def f():
)
assert out == expected_out
+ def test_exec_type_error_filter(self, importasmod):
+ """See #7742"""
+ mod = importasmod(
+ """\
+ def f():
+ exec("a = 1", {}, [])
+ """
+ )
+ with pytest.raises(TypeError) as excinfo:
+ mod.f()
+ # previously crashed with `AttributeError: list has no attribute get`
+ excinfo.traceback.filter()
+
@pytest.mark.parametrize("style", ["short", "long"])
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
c(1) # type: ignore
finally:
if teardown:
- teardown()
+ teardown() # type: ignore[unreachable]
source = excinfo.traceback[-1].statement
assert str(source).strip() == "c(1) # type: ignore"
-import copy
-import inspect
+import warnings
from unittest import mock
import pytest
from _pytest import deprecated
-from _pytest import nodes
-from _pytest.config import Config
+from _pytest.pytester import Testdir
-@pytest.mark.filterwarnings("default")
-def test_resultlog_is_deprecated(testdir):
- result = testdir.runpytest("--help")
- result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"])
-
- testdir.makepyfile(
- """
- def test():
- pass
- """
- )
- result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log"))
- result.stdout.fnmatch_lines(
- [
- "*--result-log is deprecated, please try the new pytest-reportlog plugin.",
- "*See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information*",
- ]
- )
-
-
-@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361")
@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore
# false positive due to dynamic attribute
def test_pytest_collect_module_deprecated(attribute):
getattr(pytest.collect, attribute)
-def test_terminal_reporter_writer_attr(pytestconfig: Config) -> None:
- """Check that TerminalReporter._tw is also available as 'writer' (#2984)
- This attribute has been deprecated in 5.4.
- """
- try:
- import xdist # noqa
-
- pytest.skip("xdist workers disable the terminal reporter plugin")
- except ImportError:
- pass
- terminal_reporter = pytestconfig.pluginmanager.get_plugin("terminalreporter")
- original_tw = terminal_reporter._tw
-
- with pytest.warns(pytest.PytestDeprecationWarning) as cw:
- assert terminal_reporter.writer is original_tw
- assert len(cw) == 1
- assert cw[0].filename == __file__
-
- new_tw = copy.copy(original_tw)
- with pytest.warns(pytest.PytestDeprecationWarning) as cw:
- terminal_reporter.writer = new_tw
- try:
- assert terminal_reporter._tw is new_tw
- finally:
- terminal_reporter.writer = original_tw
- assert len(cw) == 2
- assert cw[0].filename == cw[1].filename == __file__
-
-
@pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS))
@pytest.mark.filterwarnings("default")
def test_external_plugins_integrated(testdir, plugin):
testdir.parseconfig("-p", plugin)
-@pytest.mark.parametrize("junit_family", [None, "legacy", "xunit2"])
-def test_warn_about_imminent_junit_family_default_change(testdir, junit_family):
- """Show a warning if junit_family is not defined and --junitxml is used (#6179)"""
- testdir.makepyfile(
- """
- def test_foo():
- pass
- """
- )
- if junit_family:
- testdir.makeini(
- """
- [pytest]
- junit_family={junit_family}
- """.format(
- junit_family=junit_family
- )
- )
-
- result = testdir.runpytest("--junit-xml=foo.xml")
- warning_msg = (
- "*PytestDeprecationWarning: The 'junit_family' default value will change*"
- )
- if junit_family:
- result.stdout.no_fnmatch_line(warning_msg)
- else:
- result.stdout.fnmatch_lines([warning_msg])
-
-
-def test_node_direct_ctor_warning() -> None:
- class MockConfig:
- pass
-
- ms = MockConfig()
- with pytest.warns(
- DeprecationWarning,
- match="Direct construction of .* has been deprecated, please use .*.from_parent.*",
- ) as w:
- nodes.Node(name="test", config=ms, session=ms, nodeid="None") # type: ignore
- assert w[0].lineno == inspect.currentframe().f_lineno - 1 # type: ignore
- assert w[0].filename == __file__
-
-
-@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361")
def test_fillfuncargs_is_deprecated() -> None:
with pytest.warns(
pytest.PytestDeprecationWarning,
pytest._fillfuncargs(mock.Mock())
-@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361")
def test_minus_k_dash_is_deprecated(testdir) -> None:
threepass = testdir.makepyfile(
test_threepass="""
result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"])
-@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361")
def test_minus_k_colon_is_deprecated(testdir) -> None:
threepass = testdir.makepyfile(
test_threepass="""
)
result = testdir.runpytest("-k", "test_two:", threepass)
result.stdout.fnmatch_lines(["*The `-k 'expr:'` syntax*deprecated*"])
+
+
+def test_fscollector_gethookproxy_isinitpath(testdir: Testdir) -> None:
+ module = testdir.getmodulecol(
+ """
+ def test_foo(): pass
+ """,
+ withinit=True,
+ )
+ assert isinstance(module, pytest.Module)
+ package = module.parent
+ assert isinstance(package, pytest.Package)
+
+ with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"):
+ package.gethookproxy(testdir.tmpdir)
+
+ with pytest.warns(pytest.PytestDeprecationWarning, match="isinitpath"):
+ package.isinitpath(testdir.tmpdir)
+
+ # The methods on Session are *not* deprecated.
+ session = module.session
+ with warnings.catch_warnings(record=True) as rec:
+ session.gethookproxy(testdir.tmpdir)
+ session.isinitpath(testdir.tmpdir)
+ assert len(rec) == 0
--- /dev/null
+import unittest
+
+
+class Test(unittest.TestCase):
+ async def test_foo(self):
+ assert False
-"""
-Generates an executable with pytest runner embedded using PyInstaller.
-"""
+"""Generate an executable with pytest runner embedded using PyInstaller."""
if __name__ == "__main__":
import pytest
import subprocess
def test_log_report_captures_according_to_config_option_upon_failure(testdir):
- """ Test that upon failure:
- (1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised
- (2) The `DEBUG` message does NOT appear in the `Captured log call` report
- (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO`
+ """Test that upon failure:
+ (1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised.
+ (2) The `DEBUG` message does NOT appear in the `Captured log call` report.
+ (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO`.
"""
testdir.makepyfile(
"""
expected_lines.extend(
[
"*test_collection_collect_only_live_logging.py::test_simple*",
- "no tests ran in [0-1].[0-9][0-9]s",
+ "no tests ran in [0-9].[0-9][0-9]s",
]
)
elif verbose == "-qq":
def test_colored_captured_log(testdir):
- """
- Test that the level names of captured log messages of a failing test are
- colored.
- """
+ """Test that the level names of captured log messages of a failing test
+ are colored."""
testdir.makepyfile(
"""
import logging
def test_colored_ansi_esc_caplogtext(testdir):
- """
- Make sure that caplog.text does not contain ANSI escape sequences.
- """
+ """Make sure that caplog.text does not contain ANSI escape sequences."""
testdir.makepyfile(
"""
import logging
def test_logging_emit_error(testdir: Testdir) -> None:
- """
- An exception raised during emit() should fail the test.
+ """An exception raised during emit() should fail the test.
The default behavior of logging is to print "Logging error"
to stderr with the call stack and some extra details.
def test_logging_emit_error_supressed(testdir: Testdir) -> None:
- """
- If logging is configured to silently ignore errors, pytest
- doesn't propagate errors either.
- """
+ """If logging is configured to silently ignore errors, pytest
+ doesn't propagate errors either."""
testdir.makepyfile(
"""
import logging
--- /dev/null
+*.html
+assets/
--- /dev/null
+This folder contains tests and support files for smoke testing popular plugins against the current pytest version.
+
+The objective is to gauge if any intentional or unintentional changes in pytest break plugins.
+
+As a rule of thumb, we should add plugins here:
+
+1. That are used at large. This might be subjective in some cases, but if answer is yes to
+ the question: *if a new release of pytest causes pytest-X to break, will this break a ton of test suites out there?*.
+2. That don't have large external dependencies: such as external services.
+
+Besides adding the plugin as dependency, we should also add a quick test which uses some
+minimal part of the plugin, a smoke test. Also consider reusing one of the existing tests if that's
+possible.
--- /dev/null
+Feature: Buy things with apple
+
+ Scenario: Buy fruits
+ Given A wallet with 50
+
+ When I buy some apples for 1
+ And I buy some bananas for 2
+
+ Then I have 47 left
--- /dev/null
+from pytest_bdd import given
+from pytest_bdd import scenario
+from pytest_bdd import then
+from pytest_bdd import when
+
+import pytest
+
+
+@scenario("bdd_wallet.feature", "Buy fruits")
+def test_publish():
+ pass
+
+
+@pytest.fixture
+def wallet():
+ class Wallet:
+ amount = 0
+
+ return Wallet()
+
+
+@given("A wallet with 50")
+def fill_wallet(wallet):
+ wallet.amount = 50
+
+
+@when("I buy some apples for 1")
+def buy_apples(wallet):
+ wallet.amount -= 1
+
+
+@when("I buy some bananas for 2")
+def buy_bananas(wallet):
+ wallet.amount -= 2
+
+
+@then("I have 47 left")
+def check(wallet):
+ assert wallet.amount == 47
--- /dev/null
+SECRET_KEY = "mysecret"
--- /dev/null
+[pytest]
+addopts = --strict-markers
+filterwarnings =
+ error::pytest.PytestWarning
--- /dev/null
+import anyio
+
+import pytest
+
+
+@pytest.mark.anyio
+async def test_sleep():
+ await anyio.sleep(0)
--- /dev/null
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_sleep():
+ await asyncio.sleep(0)
--- /dev/null
+def test_mocker(mocker):
+ mocker.MagicMock()
--- /dev/null
+import trio
+
+import pytest
+
+
+@pytest.mark.trio
+async def test_sleep():
+ await trio.sleep(0)
--- /dev/null
+import pytest_twisted
+from twisted.internet.task import deferLater
+
+
+def sleep():
+ import twisted.internet.reactor
+
+ return deferLater(clock=twisted.internet.reactor, delay=0)
+
+
+@pytest_twisted.inlineCallbacks
+def test_inlineCallbacks():
+ yield sleep()
+
+
+@pytest_twisted.ensureDeferred
+async def test_inlineCallbacks_async():
+ await sleep()
--- /dev/null
+import pytest
+
+
+def test_foo():
+ assert True
+
+
+@pytest.mark.parametrize("i", range(3))
+def test_bar(i):
+ assert True
],
)
def test_comparison_operator_type_error(self, op):
- """
- pytest.approx should raise TypeError for operators other than == and != (#2003).
- """
+ """pytest.approx should raise TypeError for operators other than == and != (#2003)."""
with pytest.raises(TypeError):
op(1, approx(1, rel=1e-6, abs=1e-12))
result = testdir.runpytest("--collect-only")
result.stdout.fnmatch_lines(["*MyFunction*some*"])
- def test_makeitem_non_underscore(self, testdir, monkeypatch):
- modcol = testdir.getmodulecol("def _hello(): pass")
- values = []
- monkeypatch.setattr(
- pytest.Module, "_makeitem", lambda self, name, obj: values.append(name)
- )
- values = modcol.collect()
- assert "_hello" not in values
-
def test_issue2369_collect_module_fileext(self, testdir):
"""Ensure we can collect files with weird file extensions as Python
modules (#2369)"""
result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 passed*"])
+ def test_early_ignored_attributes(self, testdir: Testdir) -> None:
+ """Builtin attributes should be ignored early on, even if
+ configuration would otherwise allow them.
+
+ This tests a performance optimization, not correctness, really,
+ although it tests PytestCollectionWarning is not raised, while
+ it would have been raised otherwise.
+ """
+ testdir.makeini(
+ """
+ [pytest]
+ python_classes=*
+ python_functions=*
+ """
+ )
+ testdir.makepyfile(
+ """
+ class TestEmpty:
+ pass
+ test_empty = TestEmpty()
+ def test_real():
+ pass
+ """
+ )
+ items, rec = testdir.inline_genitems()
+ assert rec.ret == 0
+ assert len(items) == 1
+
def test_setup_only_available_in_subdir(testdir):
sub1 = testdir.mkpydir("sub1")
result.stdout.fnmatch_lines([">*asd*", "E*NameError*"])
def test_traceback_filter_error_during_fixture_collection(self, testdir):
- """integration test for issue #995.
- """
+ """Integration test for issue #995."""
testdir.makepyfile(
"""
import pytest
result.stdout.fnmatch_lines(["*ValueError: fail me*", "* 1 error in *"])
def test_filter_traceback_generated_code(self) -> None:
- """test that filter_traceback() works with the fact that
+ """Test that filter_traceback() works with the fact that
_pytest._code.code.Code.path attribute might return an str object.
+
In this case, one of the entries on the traceback was produced by
dynamically generated code.
See: https://bitbucket.org/pytest-dev/py/issues/71
This fixes #995.
"""
- from _pytest.python import filter_traceback
+ from _pytest._code import filter_traceback
try:
ns = {} # type: Dict[str, Any]
assert not filter_traceback(traceback[-1])
def test_filter_traceback_path_no_longer_valid(self, testdir) -> None:
- """test that filter_traceback() works with the fact that
+ """Test that filter_traceback() works with the fact that
_pytest._code.code.Code.path attribute might return an str object.
+
In this case, one of the files in the traceback no longer exists.
This fixes #1133.
"""
- from _pytest.python import filter_traceback
+ from _pytest._code import filter_traceback
testdir.syspathinsert()
testdir.makepyfile(
def test_syntax_error_with_non_ascii_chars(testdir):
- """Fix decoding issue while formatting SyntaxErrors during collection (#578)
- """
+ """Fix decoding issue while formatting SyntaxErrors during collection (#578)."""
testdir.makepyfile("☃")
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"])
import pytest
from _pytest import fixtures
+from _pytest.compat import getfuncargnames
from _pytest.config import ExitCode
from _pytest.fixtures import FixtureRequest
from _pytest.pathlib import Path
def f():
raise NotImplementedError()
- assert not fixtures.getfuncargnames(f)
+ assert not getfuncargnames(f)
def g(arg):
raise NotImplementedError()
- assert fixtures.getfuncargnames(g) == ("arg",)
+ assert getfuncargnames(g) == ("arg",)
def h(arg1, arg2="hello"):
raise NotImplementedError()
- assert fixtures.getfuncargnames(h) == ("arg1",)
+ assert getfuncargnames(h) == ("arg1",)
def j(arg1, arg2, arg3="hello"):
raise NotImplementedError()
- assert fixtures.getfuncargnames(j) == ("arg1", "arg2")
+ assert getfuncargnames(j) == ("arg1", "arg2")
def test_getfuncargnames_methods():
def f(self, arg1, arg2="hello"):
raise NotImplementedError()
- assert fixtures.getfuncargnames(A().f) == ("arg1",)
+ assert getfuncargnames(A().f) == ("arg1",)
def test_getfuncargnames_staticmethod():
def static(arg1, arg2, x=1):
raise NotImplementedError()
- assert fixtures.getfuncargnames(A.static, cls=A) == ("arg1", "arg2")
+ assert getfuncargnames(A.static, cls=A) == ("arg1", "arg2")
def test_getfuncargnames_partial():
class T:
test_ok = functools.partial(check, i=2)
- values = fixtures.getfuncargnames(T().test_ok, name="test_ok")
+ values = getfuncargnames(T().test_ok, name="test_ok")
assert values == ("arg1", "arg2")
class T:
test_ok = staticmethod(functools.partial(check, i=2))
- values = fixtures.getfuncargnames(T().test_ok, name="test_ok")
+ values = getfuncargnames(T().test_ok, name="test_ok")
assert values == ("arg1", "arg2")
p = testdir.copy_example()
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"])
- result = testdir.runpytest(next(p.visit("test_*.py")))
+ result = testdir.runpytest(str(next(Path(str(p)).rglob("test_*.py"))))
result.stdout.fnmatch_lines(["*1 passed*"])
def test_extend_fixture_conftest_conftest(self, testdir):
p = testdir.copy_example()
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"])
- result = testdir.runpytest(next(p.visit("test_*.py")))
+ result = testdir.runpytest(str(next(Path(str(p)).rglob("test_*.py"))))
result.stdout.fnmatch_lines(["*1 passed*"])
def test_extend_fixture_conftest_plugin(self, testdir):
result = testdir.runpytest(testfile)
result.stdout.fnmatch_lines(["*3 passed*"])
+ def test_override_fixture_reusing_super_fixture_parametrization(self, testdir):
+ """Override a fixture at a lower level, reusing the higher-level fixture that
+ is parametrized (#1953).
+ """
+ testdir.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2])
+ def foo(request):
+ return request.param
+ """
+ )
+ testdir.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def foo(foo):
+ return foo * 2
+
+ def test_spam(foo):
+ assert foo in (2, 4)
+ """
+ )
+ result = testdir.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_override_parametrize_fixture_and_indirect(self, testdir):
+ """Override a fixture at a lower level, reusing the higher-level fixture that
+ is parametrized, while also using indirect parametrization.
+ """
+ testdir.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2])
+ def foo(request):
+ return request.param
+ """
+ )
+ testdir.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture
+ def foo(foo):
+ return foo * 2
+
+ @pytest.fixture
+ def bar(request):
+ return request.param * 100
+
+ @pytest.mark.parametrize("bar", [42], indirect=True)
+ def test_spam(bar, foo):
+ assert bar == 4200
+ assert foo in (2, 4)
+ """
+ )
+ result = testdir.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_override_top_level_fixture_reusing_super_fixture_parametrization(
+ self, testdir
+ ):
+ """Same as the above test, but with another level of overwriting."""
+ testdir.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=['unused', 'unused'])
+ def foo(request):
+ return request.param
+ """
+ )
+ testdir.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[1, 2])
+ def foo(request):
+ return request.param
+
+ class Test:
+
+ @pytest.fixture
+ def foo(self, foo):
+ return foo * 2
+
+ def test_spam(self, foo):
+ assert foo in (2, 4)
+ """
+ )
+ result = testdir.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
+ def test_override_parametrized_fixture_with_new_parametrized_fixture(self, testdir):
+ """Overriding a parametrized fixture, while also parametrizing the new fixture and
+ simultaneously requesting the overwritten fixture as parameter, yields the same value
+ as ``request.param``.
+ """
+ testdir.makeconftest(
+ """
+ import pytest
+
+ @pytest.fixture(params=['ignored', 'ignored'])
+ def foo(request):
+ return request.param
+ """
+ )
+ testdir.makepyfile(
+ """
+ import pytest
+
+ @pytest.fixture(params=[10, 20])
+ def foo(foo, request):
+ assert request.param == foo
+ return foo * 2
+
+ def test_spam(foo):
+ assert foo in (20, 40)
+ """
+ )
+ result = testdir.runpytest()
+ result.stdout.fnmatch_lines(["*2 passed*"])
+
def test_autouse_fixture_plugin(self, testdir):
# A fixture from a plugin has no baseid set, which screwed up
# the autouse fixture handling.
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"])
- def test_funcargnames_compatattr(self, testdir):
- testdir.makepyfile(
- """
- import pytest
- def pytest_generate_tests(metafunc):
- with pytest.warns(pytest.PytestDeprecationWarning):
- assert metafunc.funcargnames == metafunc.fixturenames
- @pytest.fixture
- def fn(request):
- with pytest.warns(pytest.PytestDeprecationWarning):
- assert request._pyfuncitem.funcargnames == \
- request._pyfuncitem.fixturenames
- with pytest.warns(pytest.PytestDeprecationWarning):
- return request.funcargnames, request.fixturenames
-
- def test_hello(fn):
- assert fn[0] == fn[1]
- """
- )
- reprec = testdir.inline_run()
- reprec.assertoutcome(passed=1)
-
def test_setupdecorator_and_xunit(self, testdir):
testdir.makepyfile(
"""
reprec.assertoutcome(passed=2)
def test_callables_nocode(self, testdir):
- """
- an imported mock.call would break setup/factory discovery
- due to it being callable and __code__ not being a code object
- """
+ """An imported mock.call would break setup/factory discovery due to
+ it being callable and __code__ not being a code object."""
testdir.makepyfile(
"""
class _call(tuple):
)
def test_show_fixtures_different_files(self, testdir):
- """
- #833: --fixtures only shows fixtures from first file
- """
+ """`--fixtures` only shows fixtures from first file (#833)."""
testdir.makepyfile(
test_a='''
import pytest
)
-def test_fixture_duplicated_arguments() -> None:
- """Raise error if there are positional and keyword arguments for the same parameter (#1682)."""
- with pytest.raises(TypeError) as excinfo:
-
- @pytest.fixture("session", scope="session") # type: ignore[call-overload]
- def arg(arg):
- pass
-
- assert (
- str(excinfo.value)
- == "The fixture arguments are defined as positional and keyword: scope. "
- "Use only keyword arguments."
- )
-
- with pytest.raises(TypeError) as excinfo:
-
- @pytest.fixture( # type: ignore[call-overload]
- "function",
- ["p1"],
- True,
- ["id1"],
- "name",
- scope="session",
- params=["p1"],
- autouse=True,
- ids=["id1"],
- name="name",
- )
- def arg2(request):
- pass
-
- assert (
- str(excinfo.value)
- == "The fixture arguments are defined as positional and keyword: scope, params, autouse, ids, name. "
- "Use only keyword arguments."
- )
-
-
-def test_fixture_with_positionals() -> None:
- """Raise warning, but the positionals should still works (#1682)."""
- from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
-
- with pytest.warns(pytest.PytestDeprecationWarning) as warnings:
-
- @pytest.fixture("function", [0], True) # type: ignore[call-overload]
- def fixture_with_positionals():
- pass
-
- assert str(warnings[0].message) == str(FIXTURE_POSITIONAL_ARGUMENTS)
-
- assert fixture_with_positionals._pytestfixturefunction.scope == "function"
- assert fixture_with_positionals._pytestfixturefunction.params == (0,)
- assert fixture_with_positionals._pytestfixturefunction.autouse
-
-
-def test_fixture_with_too_many_positionals() -> None:
- with pytest.raises(TypeError) as excinfo:
-
- @pytest.fixture("function", [0], True, ["id"], "name", "extra") # type: ignore[call-overload]
- def fixture_with_positionals():
- pass
-
- assert (
- str(excinfo.value) == "fixture() takes 5 positional arguments but 6 were given"
- )
-
-
def test_indirect_fixture_does_not_break_scope(testdir):
"""Ensure that fixture scope is respected when using indirect fixtures (#570)"""
testdir.makepyfile(
from typing import Any
import pytest
-from _pytest import python
from _pytest import runner
+from _pytest._code import getfslineno
class TestOEJSKITSpecials:
def wrapped_func(x, y, z):
pass
- fs, lineno = python.getfslineno(wrapped_func)
- fs2, lineno2 = python.getfslineno(wrap)
+ fs, lineno = getfslineno(wrapped_func)
+ fs2, lineno2 = getfslineno(wrap)
assert lineno > lineno2, "getfslineno does not unwrap correctly"
import pytest
from _pytest import fixtures
from _pytest import python
+from _pytest.compat import _format_args
+from _pytest.compat import getfuncargnames
from _pytest.compat import NOTSET
from _pytest.outcomes import fail
from _pytest.pytester import Testdir
class TestMetafunc:
def Metafunc(self, func, config=None) -> python.Metafunc:
- # the unit tests of this class check if things work correctly
+ # The unit tests of this class check if things work correctly
# on the funcarg level, so we don't need a full blown
- # initialization
+ # initialization.
class FuncFixtureInfoMock:
name2fixturedefs = None
obj = attr.ib()
_nodeid = attr.ib()
- names = fixtures.getfuncargnames(func)
+ names = getfuncargnames(func)
fixtureinfo = FuncFixtureInfoMock(names) # type: Any
definition = DefinitionMock._create(func, "mock::nodeid") # type: Any
return python.Metafunc(definition, fixtureinfo, config)
metafunc.parametrize("request", [1])
def test_find_parametrized_scope(self) -> None:
- """unittest for _find_parametrized_scope (#3941)"""
+ """Unit test for _find_parametrized_scope (#3941)."""
from _pytest.python import _find_parametrized_scope
@attr.s
scope = attr.ib()
fixtures_defs = cast(
- Dict[str, Sequence[fixtures.FixtureDef]],
+ Dict[str, Sequence[fixtures.FixtureDef[object]]],
dict(
session_fix=[DummyFixtureDef("session")],
package_fix=[DummyFixtureDef("package")],
escaped.encode("ascii")
def test_unicode_idval(self) -> None:
- """This tests that Unicode strings outside the ASCII character set get
+ """Test that Unicode strings outside the ASCII character set get
escaped, using byte escapes if they're in that range or unicode
escapes if they're not.
"""
values = [
- ("", ""),
- ("ascii", "ascii"),
- ("ação", "a\\xe7\\xe3o"),
- ("josé@blah.com", "jos\\xe9@blah.com"),
+ ("", r""),
+ ("ascii", r"ascii"),
+ ("ação", r"a\xe7\xe3o"),
+ ("josé@blah.com", r"jos\xe9@blah.com"),
(
- "δοκ.ιμή@παράδειγμα.δοκιμή",
- "\\u03b4\\u03bf\\u03ba.\\u03b9\\u03bc\\u03ae@\\u03c0\\u03b1\\u03c1\\u03ac\\u03b4\\u03b5\\u03b9\\u03b3"
- "\\u03bc\\u03b1.\\u03b4\\u03bf\\u03ba\\u03b9\\u03bc\\u03ae",
+ r"δοκ.ιμή@παράδειγμα.δοκιμή",
+ r"\u03b4\u03bf\u03ba.\u03b9\u03bc\u03ae@\u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3"
+ r"\u03bc\u03b1.\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae",
),
]
for val, expected in values:
assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
def test_unicode_idval_with_config(self) -> None:
- """unittest for expected behavior to obtain ids with
+ """Unit test for expected behavior to obtain ids with
disable_test_id_escaping_and_forfeit_all_rights_to_community_support
- option. (#5294)
- """
+ option (#5294)."""
class MockConfig:
def __init__(self, config):
assert actual == expected
def test_bytes_idval(self) -> None:
- """unittest for the expected behavior to obtain ids for parametrized
- bytes values:
- - python2: non-ascii strings are considered bytes and formatted using
- "binary escape", where any byte < 127 is escaped into its hex form.
- - python3: bytes objects are always escaped using "binary escape".
- """
+ """Unit test for the expected behavior to obtain ids for parametrized
+ bytes values: bytes objects are always escaped using "binary escape"."""
values = [
- (b"", ""),
- (b"\xc3\xb4\xff\xe4", "\\xc3\\xb4\\xff\\xe4"),
- (b"ascii", "ascii"),
- ("αρά".encode(), "\\xce\\xb1\\xcf\\x81\\xce\\xac"),
+ (b"", r""),
+ (b"\xc3\xb4\xff\xe4", r"\xc3\xb4\xff\xe4"),
+ (b"ascii", r"ascii"),
+ ("αρά".encode(), r"\xce\xb1\xcf\x81\xce\xac"),
]
for val, expected in values:
assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected
def test_class_or_function_idval(self) -> None:
- """unittest for the expected behavior to obtain ids for parametrized
- values that are classes or functions: their __name__.
- """
+ """Unit test for the expected behavior to obtain ids for parametrized
+ values that are classes or functions: their __name__."""
class TestClass:
pass
assert result == ["a-a0", "a-a1", "a-a2"]
def test_idmaker_with_idfn_and_config(self) -> None:
- """unittest for expected behavior to create ids with idfn and
+ """Unit test for expected behavior to create ids with idfn and
disable_test_id_escaping_and_forfeit_all_rights_to_community_support
- option. (#5294)
+ option (#5294).
"""
class MockConfig:
assert result == [expected]
def test_idmaker_with_ids_and_config(self) -> None:
- """unittest for expected behavior to create ids with ids and
+ """Unit test for expected behavior to create ids with ids and
disable_test_id_escaping_and_forfeit_all_rights_to_community_support
- option. (#5294)
+ option (#5294).
"""
class MockConfig:
def function1():
pass
- assert fixtures._format_args(function1) == "()"
+ assert _format_args(function1) == "()"
def function2(arg1):
pass
- assert fixtures._format_args(function2) == "(arg1)"
+ assert _format_args(function2) == "(arg1)"
def function3(arg1, arg2="qwe"):
pass
- assert fixtures._format_args(function3) == "(arg1, arg2='qwe')"
+ assert _format_args(function3) == "(arg1, arg2='qwe')"
def function4(arg1, *args, **kwargs):
pass
- assert fixtures._format_args(function4) == "(arg1, *args, **kwargs)"
+ assert _format_args(function4) == "(arg1, *args, **kwargs)"
class TestMetafuncFunctional:
class TestMetafuncFunctionalAuto:
- """
- Tests related to automatically find out the correct scope for parametrized tests (#1832).
- """
+ """Tests related to automatically find out the correct scope for
+ parametrized tests (#1832)."""
def test_parametrize_auto_scope(self, testdir: Testdir) -> None:
testdir.makepyfile(
@pytest.mark.parametrize("method", ["function", "function_match", "with"])
def test_raises_cyclic_reference(self, method):
- """
- Ensure pytest.raises does not leave a reference cycle (#1965).
- """
+ """Ensure pytest.raises does not leave a reference cycle (#1965)."""
import gc
class T:
with pytest.raises(Exception, foo="bar"): # type: ignore[call-overload]
pass
assert "Unexpected keyword arguments" in str(excinfo.value)
+
+ def test_expected_exception_is_not_a_baseexception(self) -> None:
+ with pytest.raises(TypeError) as excinfo:
+ with pytest.raises("hello"): # type: ignore[call-overload]
+ pass # pragma: no cover
+ assert "must be a BaseException type, not str" in str(excinfo.value)
+
+ class NotAnException:
+ pass
+
+ with pytest.raises(TypeError) as excinfo:
+ with pytest.raises(NotAnException): # type: ignore[type-var]
+ pass # pragma: no cover
+ assert "must be a BaseException type, not NotAnException" in str(excinfo.value)
+
+ with pytest.raises(TypeError) as excinfo:
+ with pytest.raises(("hello", NotAnException)): # type: ignore[arg-type]
+ pass # pragma: no cover
+ assert "must be a BaseException type, not str" in str(excinfo.value)
import pytest
-# test for _argcomplete but not specific for any application
+# Test for _argcomplete but not specific for any application.
def equal_with_bash(prefix, ffc, fc, out=None):
return retval
-# copied from argcomplete.completers as import from there
-# also pulls in argcomplete.__init__ which opens filedescriptor 9
-# this gives an OSError at the end of testrun
+# Copied from argcomplete.completers as import from there.
+# Also pulls in argcomplete.__init__ which opens filedescriptor 9.
+# This gives an OSError at the end of testrun.
def _wrapcall(*args, **kargs):
class FilesCompleter:
- "File completer class, optionally takes a list of allowed extensions"
+ """File completer class, optionally takes a list of allowed extensions."""
def __init__(self, allowednames=(), directories=True):
# Fix if someone passes in a string instead of a list
@pytest.mark.skipif("sys.platform in ('win32', 'darwin')")
def test_remove_dir_prefix(self):
- """this is not compatible with compgen but it is with bash itself:
- ls /usr/<TAB>
- """
+ """This is not compatible with compgen but it is with bash itself: ls /usr/<TAB>."""
from _pytest._argcomplete import FastFilesCompleter
ffc = FastFilesCompleter()
@pytest.mark.parametrize("initial_conftest", [True, False])
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
def test_conftest_assertion_rewrite(self, testdir, initial_conftest, mode):
- """Test that conftest files are using assertion rewrite on import.
- (#1619)
- """
+ """Test that conftest files are using assertion rewrite on import (#1619)."""
testdir.tmpdir.join("foo/tests").ensure(dir=1)
conftest_path = "conftest.py" if initial_conftest else "foo/conftest.py"
contents = {
assert "b" not in line
def test_dict_omitting_with_verbosity_1(self) -> None:
- """ Ensure differing items are visible for verbosity=1 (#1512) """
+ """Ensure differing items are visible for verbosity=1 (#1512)."""
lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=1)
assert lines is not None
assert lines[1].startswith("Omitting 1 identical item")
def test_Sequence(self) -> None:
# Test comparing with a Sequence subclass.
- class TestSequence(collections.abc.MutableSequence):
+ # TODO(py36): Inherit from typing.MutableSequence[int].
+ class TestSequence(collections.abc.MutableSequence): # type: ignore[type-arg]
def __init__(self, iterable):
self.elements = list(iterable)
]
def test_one_repr_empty(self):
- """
- the faulty empty string repr did trigger
- an unbound local error in _diff_text
- """
+ """The faulty empty string repr did trigger an unbound local error in _diff_text."""
class A(str):
def __repr__(self):
assert last_line_before_trunc_msg.endswith("...")
def test_full_output_truncated(self, monkeypatch, testdir):
- """ Test against full runpytest() output. """
+ """Test against full runpytest() output."""
line_count = 7
line_len = 100
def test_exception_handling_no_traceback(testdir):
- """
- Handle chain exceptions in tasks submitted by the multiprocess module (#1984).
- """
+ """Handle chain exceptions in tasks submitted by the multiprocess module (#1984)."""
p1 = testdir.makepyfile(
"""
from multiprocessing import Pool
def test_dont_rewrite_if_hasattr_fails(self, request) -> None:
class Y:
- """ A class whos getattr fails, but not with `AttributeError` """
+ """A class whose getattr fails, but not with `AttributeError`."""
def __getattr__(self, attribute_name):
raise KeyError()
)
def f7() -> None:
- assert False or x()
+ assert False or x() # type: ignore[unreachable]
assert (
getmsg(f7, {"x": x})
def test_short_circuit_evaluation(self) -> None:
def f1() -> None:
- assert True or explode # type: ignore[name-defined] # noqa: F821
+ assert True or explode # type: ignore[name-defined,unreachable] # noqa: F821
getmsg(f1, must_pass=True)
assert getmsg(f1) == "assert ((3 % 2) and False)"
def f2() -> None:
- assert False or 4 % 2
+ assert False or 4 % 2 # type: ignore[unreachable]
assert getmsg(f2) == "assert (False or (4 % 2))"
assert testdir.runpytest_subprocess().ret == 0
def test_remember_rewritten_modules(self, pytestconfig, testdir, monkeypatch):
- """
- AssertionRewriteHook should remember rewritten modules so it
- doesn't give false positives (#2005).
- """
+ """`AssertionRewriteHook` should remember rewritten modules so it
+ doesn't give false positives (#2005)."""
monkeypatch.syspath_prepend(testdir.tmpdir)
testdir.makepyfile(test_remember_rewritten_modules="")
warnings = []
e = OSError()
e.errno = 10
raise e
- yield
+ yield # type:ignore[unreachable]
monkeypatch.setattr(
_pytest.assertion.rewrite, "atomic_write", atomic_write_failed
result.stdout.fnmatch_lines(["* 1 passed*"])
def test_get_data_support(self, testdir):
- """Implement optional PEP302 api (#808).
- """
+ """Implement optional PEP302 api (#808)."""
path = testdir.mkpydir("foo")
path.join("test_foo.py").write(
textwrap.dedent(
if prefix:
if sys.version_info < (3, 8):
pytest.skip("pycache_prefix not available in py<38")
- monkeypatch.setattr(sys, "pycache_prefix", prefix)
+ monkeypatch.setattr(sys, "pycache_prefix", prefix) # type:ignore
assert get_cache_dir(Path(source)) == Path(expected)
return sorted(config.cache.get("cache/lastfailed", {}))
def test_cache_cumulative(self, testdir):
- """
- Test workflow where user fixes errors gradually file by file using --lf.
- """
+ """Test workflow where user fixes errors gradually file by file using --lf."""
# 1. initial run
test_bar = testdir.makepyfile(
test_bar="""
from _pytest import capture
from _pytest.capture import _get_multicapture
from _pytest.capture import CaptureManager
+from _pytest.capture import CaptureResult
from _pytest.capture import MultiCapture
from _pytest.config import ExitCode
from _pytest.pytester import Testdir
# pylib 1.4.20.dev2 (rev 13d9af95547e)
-def StdCaptureFD(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
+def StdCaptureFD(
+ out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
return capture.MultiCapture(
in_=capture.FDCapture(0) if in_ else None,
out=capture.FDCapture(1) if out else None,
)
-def StdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
+def StdCapture(
+ out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
return capture.MultiCapture(
in_=capture.SysCapture(0) if in_ else None,
out=capture.SysCapture(1) if out else None,
)
-def TeeStdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
+def TeeStdCapture(
+ out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
return capture.MultiCapture(
in_=capture.SysCapture(0, tee=True) if in_ else None,
out=capture.SysCapture(1, tee=True) if out else None,
@pytest.mark.parametrize("fixture", ["capsys", "capfd"])
def test_fixture_use_by_other_fixtures(self, testdir, fixture):
- """
- Ensure that capsys and capfd can be used by other fixtures during setup and teardown.
- """
+ """Ensure that capsys and capfd can be used by other fixtures during
+ setup and teardown."""
testdir.makepyfile(
"""\
import sys
f.close() # just for completeness
+def test_captureresult() -> None:
+ cr = CaptureResult("out", "err")
+ assert len(cr) == 2
+ assert cr.out == "out"
+ assert cr.err == "err"
+ out, err = cr
+ assert out == "out"
+ assert err == "err"
+ assert cr[0] == "out"
+ assert cr[1] == "err"
+ assert cr == cr
+ assert cr == CaptureResult("out", "err")
+ assert cr != CaptureResult("wrong", "err")
+ assert cr == ("out", "err")
+ assert cr != ("out", "wrong")
+ assert hash(cr) == hash(CaptureResult("out", "err"))
+ assert hash(cr) == hash(("out", "err"))
+ assert hash(cr) != hash(("out", "wrong"))
+ assert cr < ("z",)
+ assert cr < ("z", "b")
+ assert cr < ("z", "b", "c")
+ assert cr.count("err") == 1
+ assert cr.count("wrong") == 0
+ assert cr.index("err") == 1
+ with pytest.raises(ValueError):
+ assert cr.index("wrong") == 0
+ assert next(iter(cr)) == "out"
+ assert cr._replace(err="replaced") == ("out", "replaced")
+
+
@pytest.fixture
def tmpfile(testdir) -> Generator[BinaryIO, None, None]:
f = testdir.makepyfile("").open("wb+")
captureclass = staticmethod(TeeStdCapture)
def test_capturing_error_recursive(self):
- """ for TeeStdCapture since we passthrough stderr/stdout, cap1
- should get all output, while cap2 should only get "cap2\n" """
+ r"""For TeeStdCapture since we passthrough stderr/stdout, cap1
+ should get all output, while cap2 should only get "cap2\n"."""
with self.getcapture() as cap1:
print("cap1")
from _pytest.config import ExitCode
from _pytest.main import _in_venv
from _pytest.main import Session
+from _pytest.pathlib import Path
from _pytest.pathlib import symlink_or_skip
from _pytest.pytester import Testdir
tmpdir.ensure(".whatever", "test_notfound.py")
tmpdir.ensure(".bzr", "test_notfound.py")
tmpdir.ensure("normal", "test_found.py")
- for x in tmpdir.visit("test_*.py"):
- x.write("def test_hello(): pass")
+ for x in Path(str(tmpdir)).rglob("test_*.py"):
+ x.write_text("def test_hello(): pass", "utf-8")
result = testdir.runpytest("--collect-only")
s = result.stdout.str()
assert len(wascalled) == 1
assert wascalled[0].ext == ".abc"
- @pytest.mark.filterwarnings("ignore:.*pytest_collect_directory.*")
- def test_pytest_collect_directory(self, testdir):
- wascalled = []
-
- class Plugin:
- def pytest_collect_directory(self, path):
- wascalled.append(path.basename)
-
- testdir.mkdir("hello")
- testdir.mkdir("world")
- pytest.main(testdir.tmpdir, plugins=[Plugin()])
- assert "hello" in wascalled
- assert "world" in wascalled
-
class TestPrunetraceback:
def test_custom_repr_failure(self, testdir):
class TestSession:
- def test_parsearg(self, testdir) -> None:
- p = testdir.makepyfile("def test_func(): pass")
- subdir = testdir.mkdir("sub")
- subdir.ensure("__init__.py")
- target = subdir.join(p.basename)
- p.move(target)
- subdir.chdir()
- config = testdir.parseconfig(p.basename)
- rcol = Session.from_config(config)
- assert rcol.fspath == subdir
- fspath, parts = rcol._parsearg(p.basename)
-
- assert fspath == target
- assert len(parts) == 0
- fspath, parts = rcol._parsearg(p.basename + "::test_func")
- assert fspath == target
- assert parts[0] == "test_func"
- assert len(parts) == 1
-
def test_collect_topdir(self, testdir):
p = testdir.makepyfile("def test_func(): pass")
id = "::".join([p.basename, "test_func"])
print(s)
def test_class_and_functions_discovery_using_glob(self, testdir):
- """
- tests that python_classes and python_functions config options work
- as prefixes and glob-like patterns (issue #600).
- """
+ """Test that Python_classes and Python_functions config options work
+ as prefixes and glob-like patterns (#600)."""
testdir.makeini(
"""
[pytest]
"* 1 failed in *",
]
)
+
+
+def test_does_not_crash_on_error_from_decorated_function(testdir: Testdir) -> None:
+ """Regression test for an issue around bad exception formatting due to
+ assertion rewriting mangling lineno's (#4984)."""
+ testdir.makepyfile(
+ """
+ @pytest.fixture
+ def a(): return 4
+ """
+ )
+ result = testdir.runpytest()
+ # Not INTERNAL_ERROR
+ assert result.ret == ExitCode.INTERRUPTED
from typing import Dict
from typing import List
from typing import Sequence
+from typing import Tuple
import attr
import py.path
import _pytest._code
import pytest
from _pytest.compat import importlib_metadata
+from _pytest.compat import TYPE_CHECKING
from _pytest.config import _get_plugin_specs_as_list
from _pytest.config import _iter_rewritable_modules
+from _pytest.config import _strtobool
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config import ExitCode
+from _pytest.config import parse_warning_filter
from _pytest.config.exceptions import UsageError
from _pytest.config.findpaths import determine_setup
from _pytest.config.findpaths import get_common_ancestor
from _pytest.config.findpaths import locate_config
+from _pytest.monkeypatch import MonkeyPatch
from _pytest.pathlib import Path
+from _pytest.pytester import Testdir
+
+if TYPE_CHECKING:
+ from typing import Type
class TestParseIni:
@pytest.mark.parametrize(
"section, filename", [("pytest", "pytest.ini"), ("tool:pytest", "setup.cfg")]
)
- def test_getcfg_and_config(self, testdir, tmpdir, section, filename):
- sub = tmpdir.mkdir("sub")
- sub.chdir()
- tmpdir.join(filename).write(
+ def test_getcfg_and_config(
+ self,
+ testdir: Testdir,
+ tmp_path: Path,
+ section: str,
+ filename: str,
+ monkeypatch: MonkeyPatch,
+ ) -> None:
+ sub = tmp_path / "sub"
+ sub.mkdir()
+ monkeypatch.chdir(sub)
+ (tmp_path / filename).write_text(
textwrap.dedent(
"""\
[{section}]
""".format(
section=section
)
- )
+ ),
+ encoding="utf-8",
)
_, _, cfg = locate_config([sub])
assert cfg["name"] == "value"
- config = testdir.parseconfigure(sub)
+ config = testdir.parseconfigure(str(sub))
assert config.inicfg["name"] == "value"
- def test_getcfg_empty_path(self):
- """correctly handle zero length arguments (a la pytest '')"""
- locate_config([""])
-
def test_setupcfg_uses_toolpytest_with_pytest(self, testdir):
p1 = testdir.makepyfile("def test(): pass")
testdir.makefile(
@pytest.mark.parametrize(
"ini_file_text, invalid_keys, warning_output, exception_text",
[
- (
+ pytest.param(
"""
- [pytest]
- unknown_ini = value1
- another_unknown_ini = value2
- """,
+ [pytest]
+ unknown_ini = value1
+ another_unknown_ini = value2
+ """,
["unknown_ini", "another_unknown_ini"],
[
"=*= warnings summary =*=",
- "*PytestConfigWarning:*Unknown config ini key: another_unknown_ini",
- "*PytestConfigWarning:*Unknown config ini key: unknown_ini",
+ "*PytestConfigWarning:*Unknown config option: another_unknown_ini",
+ "*PytestConfigWarning:*Unknown config option: unknown_ini",
],
- "Unknown config ini key: another_unknown_ini",
+ "Unknown config option: another_unknown_ini",
+ id="2-unknowns",
),
- (
+ pytest.param(
"""
- [pytest]
- unknown_ini = value1
- minversion = 5.0.0
- """,
+ [pytest]
+ unknown_ini = value1
+ minversion = 5.0.0
+ """,
["unknown_ini"],
[
"=*= warnings summary =*=",
- "*PytestConfigWarning:*Unknown config ini key: unknown_ini",
+ "*PytestConfigWarning:*Unknown config option: unknown_ini",
],
- "Unknown config ini key: unknown_ini",
+ "Unknown config option: unknown_ini",
+ id="1-unknown",
),
- (
+ pytest.param(
"""
- [some_other_header]
- unknown_ini = value1
- [pytest]
- minversion = 5.0.0
- """,
+ [some_other_header]
+ unknown_ini = value1
+ [pytest]
+ minversion = 5.0.0
+ """,
[],
[],
"",
+ id="unknown-in-other-header",
),
- (
+ pytest.param(
"""
- [pytest]
- minversion = 5.0.0
- """,
+ [pytest]
+ minversion = 5.0.0
+ """,
[],
[],
"",
+ id="no-unknowns",
),
- (
+ pytest.param(
"""
- [pytest]
- conftest_ini_key = 1
- """,
+ [pytest]
+ conftest_ini_key = 1
+ """,
[],
[],
"",
+ id="1-known",
),
],
)
- def test_invalid_ini_keys(
+ @pytest.mark.filterwarnings("default")
+ def test_invalid_config_options(
self, testdir, ini_file_text, invalid_keys, warning_output, exception_text
):
testdir.makeconftest(
"""
def pytest_addoption(parser):
parser.addini("conftest_ini_key", "")
- """
+ """
)
- testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text))
+ testdir.makepyfile("def test(): pass")
+ testdir.makeini(ini_file_text)
config = testdir.parseconfig()
assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys)
result = testdir.runpytest()
result.stdout.fnmatch_lines(warning_output)
+ result = testdir.runpytest("--strict-config")
if exception_text:
- with pytest.raises(pytest.fail.Exception, match=exception_text):
- testdir.runpytest("--strict-config")
+ result.stderr.fnmatch_lines("ERROR: " + exception_text)
+ assert result.ret == pytest.ExitCode.USAGE_ERROR
else:
- testdir.runpytest("--strict-config")
+ result.stderr.no_fnmatch_line(exception_text)
+ assert result.ret == pytest.ExitCode.OK
+
+ @pytest.mark.filterwarnings("default")
+ def test_silence_unknown_key_warning(self, testdir: Testdir) -> None:
+ """Unknown config key warnings can be silenced using filterwarnings (#7620)"""
+ testdir.makeini(
+ """
+ [pytest]
+ filterwarnings =
+ ignore:Unknown config option:pytest.PytestConfigWarning
+ foobar=1
+ """
+ )
+ result = testdir.runpytest()
+ result.stdout.no_fnmatch_line("*PytestConfigWarning*")
+
+ @pytest.mark.filterwarnings("default")
+ def test_disable_warnings_plugin_disables_config_warnings(
+ self, testdir: Testdir
+ ) -> None:
+ """Disabling 'warnings' plugin also disables config time warnings"""
+ testdir.makeconftest(
+ """
+ import pytest
+ def pytest_configure(config):
+ config.issue_config_time_warning(
+ pytest.PytestConfigWarning("custom config warning"),
+ stacklevel=2,
+ )
+ """
+ )
+ result = testdir.runpytest("-pno:warnings")
+ result.stdout.no_fnmatch_line("*PytestConfigWarning*")
@pytest.mark.parametrize(
"ini_file_text, exception_text",
testdir.makeini(ini_file_text)
if exception_text:
- with pytest.raises(pytest.fail.Exception, match=exception_text):
+ with pytest.raises(pytest.UsageError, match=exception_text):
testdir.parseconfig()
else:
testdir.parseconfig()
+ def test_early_config_cmdline(self, testdir, monkeypatch):
+ """early_config contains options registered by third-party plugins.
+
+ This is a regression involving pytest-cov (and possibly others) introduced in #7700.
+ """
+ testdir.makepyfile(
+ myplugin="""
+ def pytest_addoption(parser):
+ parser.addoption('--foo', default=None, dest='foo')
+
+ def pytest_load_initial_conftests(early_config, parser, args):
+ assert early_config.known_args_namespace.foo == "1"
+ """
+ )
+ monkeypatch.setenv("PYTEST_PLUGINS", "myplugin")
+ testdir.syspathinsert()
+ result = testdir.runpytest("--foo=1")
+ result.stdout.fnmatch_lines("* no tests ran in *")
+
class TestConfigCmdlineParsing:
def test_parsing_again_fails(self, testdir):
def test_invalid_options_show_extra_information(testdir):
- """display extra information when pytest exits due to unrecognized
- options in the command-line"""
+ """Display extra information when pytest exits due to unrecognized
+ options in the command-line."""
testdir.makeini(
"""
[pytest]
pm.register(m)
hc = pm.hook.pytest_load_initial_conftests
values = hc._nonwrappers + hc._wrappers
- expected = ["_pytest.config", m.__module__, "_pytest.capture"]
+ expected = ["_pytest.config", m.__module__, "_pytest.capture", "_pytest.warnings"]
assert [x.function.__module__ for x in values] == expected
class TestRootdir:
- def test_simple_noini(self, tmpdir):
- assert get_common_ancestor([tmpdir]) == tmpdir
- a = tmpdir.mkdir("a")
- assert get_common_ancestor([a, tmpdir]) == tmpdir
- assert get_common_ancestor([tmpdir, a]) == tmpdir
- with tmpdir.as_cwd():
- assert get_common_ancestor([]) == tmpdir
- no_path = tmpdir.join("does-not-exist")
- assert get_common_ancestor([no_path]) == tmpdir
- assert get_common_ancestor([no_path.join("a")]) == tmpdir
+ def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+ assert get_common_ancestor([tmp_path]) == tmp_path
+ a = tmp_path / "a"
+ a.mkdir()
+ assert get_common_ancestor([a, tmp_path]) == tmp_path
+ assert get_common_ancestor([tmp_path, a]) == tmp_path
+ monkeypatch.chdir(tmp_path)
+ assert get_common_ancestor([]) == tmp_path
+ no_path = tmp_path / "does-not-exist"
+ assert get_common_ancestor([no_path]) == tmp_path
+ assert get_common_ancestor([no_path / "a"]) == tmp_path
@pytest.mark.parametrize(
"name, contents",
pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"),
],
)
- def test_with_ini(self, tmpdir: py.path.local, name: str, contents: str) -> None:
- inifile = tmpdir.join(name)
- inifile.write(contents)
-
- a = tmpdir.mkdir("a")
- b = a.mkdir("b")
- for args in ([str(tmpdir)], [str(a)], [str(b)]):
- rootdir, parsed_inifile, _ = determine_setup(None, args)
- assert rootdir == tmpdir
- assert parsed_inifile == inifile
- rootdir, parsed_inifile, ini_config = determine_setup(None, [str(b), str(a)])
- assert rootdir == tmpdir
- assert parsed_inifile == inifile
+ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
+ inipath = tmp_path / name
+ inipath.write_text(contents, "utf-8")
+
+ a = tmp_path / "a"
+ a.mkdir()
+ b = a / "b"
+ b.mkdir()
+ for args in ([str(tmp_path)], [str(a)], [str(b)]):
+ rootpath, parsed_inipath, _ = determine_setup(None, args)
+ assert rootpath == tmp_path
+ assert parsed_inipath == inipath
+ rootpath, parsed_inipath, ini_config = determine_setup(None, [str(b), str(a)])
+ assert rootpath == tmp_path
+ assert parsed_inipath == inipath
assert ini_config == {"x": "10"}
- @pytest.mark.parametrize("name", "setup.cfg tox.ini".split())
- def test_pytestini_overrides_empty_other(self, tmpdir: py.path.local, name) -> None:
- inifile = tmpdir.ensure("pytest.ini")
- a = tmpdir.mkdir("a")
- a.ensure(name)
- rootdir, parsed_inifile, _ = determine_setup(None, [str(a)])
- assert rootdir == tmpdir
- assert parsed_inifile == inifile
-
- def test_setuppy_fallback(self, tmpdir: py.path.local) -> None:
- a = tmpdir.mkdir("a")
- a.ensure("setup.cfg")
- tmpdir.ensure("setup.py")
- rootdir, inifile, inicfg = determine_setup(None, [str(a)])
- assert rootdir == tmpdir
- assert inifile is None
+ @pytest.mark.parametrize("name", ["setup.cfg", "tox.ini"])
+ def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> None:
+ inipath = tmp_path / "pytest.ini"
+ inipath.touch()
+ a = tmp_path / "a"
+ a.mkdir()
+ (a / name).touch()
+ rootpath, parsed_inipath, _ = determine_setup(None, [str(a)])
+ assert rootpath == tmp_path
+ assert parsed_inipath == inipath
+
+ def test_setuppy_fallback(self, tmp_path: Path) -> None:
+ a = tmp_path / "a"
+ a.mkdir()
+ (a / "setup.cfg").touch()
+ (tmp_path / "setup.py").touch()
+ rootpath, inipath, inicfg = determine_setup(None, [str(a)])
+ assert rootpath == tmp_path
+ assert inipath is None
assert inicfg == {}
- def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None:
- monkeypatch.chdir(str(tmpdir))
- rootdir, inifile, inicfg = determine_setup(None, [str(tmpdir)])
- assert rootdir == tmpdir
- assert inifile is None
+ def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+ monkeypatch.chdir(tmp_path)
+ rootpath, inipath, inicfg = determine_setup(None, [str(tmp_path)])
+ assert rootpath == tmp_path
+ assert inipath is None
assert inicfg == {}
@pytest.mark.parametrize(
],
)
def test_with_specific_inifile(
- self, tmpdir: py.path.local, name: str, contents: str
+ self, tmp_path: Path, name: str, contents: str
) -> None:
- p = tmpdir.ensure(name)
- p.write(contents)
- rootdir, inifile, ini_config = determine_setup(str(p), [str(tmpdir)])
- assert rootdir == tmpdir
- assert inifile == p
+ p = tmp_path / name
+ p.touch()
+ p.write_text(contents, "utf-8")
+ rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)])
+ assert rootpath == tmp_path
+ assert inipath == p
assert ini_config == {"x": "10"}
- def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch) -> None:
- monkeypatch.chdir(str(tmpdir))
- a = tmpdir.mkdir("a")
- b = tmpdir.mkdir("b")
- rootdir, inifile, _ = determine_setup(None, [str(a), str(b)])
- assert rootdir == tmpdir
+ def test_with_arg_outside_cwd_without_inifile(
+ self, tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.chdir(tmp_path)
+ a = tmp_path / "a"
+ a.mkdir()
+ b = tmp_path / "b"
+ b.mkdir()
+ rootpath, inifile, _ = determine_setup(None, [str(a), str(b)])
+ assert rootpath == tmp_path
assert inifile is None
- def test_with_arg_outside_cwd_with_inifile(self, tmpdir) -> None:
- a = tmpdir.mkdir("a")
- b = tmpdir.mkdir("b")
- inifile = a.ensure("pytest.ini")
- rootdir, parsed_inifile, _ = determine_setup(None, [str(a), str(b)])
- assert rootdir == a
- assert inifile == parsed_inifile
+ def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None:
+ a = tmp_path / "a"
+ a.mkdir()
+ b = tmp_path / "b"
+ b.mkdir()
+ inipath = a / "pytest.ini"
+ inipath.touch()
+ rootpath, parsed_inipath, _ = determine_setup(None, [str(a), str(b)])
+ assert rootpath == a
+ assert inipath == parsed_inipath
@pytest.mark.parametrize("dirs", ([], ["does-not-exist"], ["a/does-not-exist"]))
- def test_with_non_dir_arg(self, dirs, tmpdir) -> None:
- with tmpdir.ensure(dir=True).as_cwd():
- rootdir, inifile, _ = determine_setup(None, dirs)
- assert rootdir == tmpdir
- assert inifile is None
+ def test_with_non_dir_arg(
+ self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ monkeypatch.chdir(tmp_path)
+ rootpath, inipath, _ = determine_setup(None, dirs)
+ assert rootpath == tmp_path
+ assert inipath is None
- def test_with_existing_file_in_subdir(self, tmpdir) -> None:
- a = tmpdir.mkdir("a")
- a.ensure("exist")
- with tmpdir.as_cwd():
- rootdir, inifile, _ = determine_setup(None, ["a/exist"])
- assert rootdir == tmpdir
- assert inifile is None
+ def test_with_existing_file_in_subdir(
+ self, tmp_path: Path, monkeypatch: MonkeyPatch
+ ) -> None:
+ a = tmp_path / "a"
+ a.mkdir()
+ (a / "exists").touch()
+ monkeypatch.chdir(tmp_path)
+ rootpath, inipath, _ = determine_setup(None, ["a/exist"])
+ assert rootpath == tmp_path
+ assert inipath is None
class TestOverrideIniArgs:
)
def test_addopts_from_ini_not_concatenated(self, testdir):
- """addopts from ini should not take values from normal args (#4265)."""
+ """`addopts` from ini should not take values from normal args (#4265)."""
testdir.makeini(
"""
[pytest]
def test_conftest_import_error_repr(tmpdir):
- """
- ConftestImportFailure should use a short error message and readable path to the failed
- conftest.py file
- """
+ """`ConftestImportFailure` should use a short error message and readable
+ path to the failed conftest.py file."""
path = tmpdir.join("foo/conftest.py")
with pytest.raises(
ConftestImportFailure,
assert exc.__traceback__ is not None
exc_info = (type(exc), exc, exc.__traceback__)
raise ConftestImportFailure(path, exc_info) from exc
+
+
+def test_strtobool():
+ assert _strtobool("YES")
+ assert not _strtobool("NO")
+ with pytest.raises(ValueError):
+ _strtobool("unknown")
+
+
+@pytest.mark.parametrize(
+ "arg, escape, expected",
+ [
+ ("ignore", False, ("ignore", "", Warning, "", 0)),
+ (
+ "ignore::DeprecationWarning",
+ False,
+ ("ignore", "", DeprecationWarning, "", 0),
+ ),
+ (
+ "ignore:some msg:DeprecationWarning",
+ False,
+ ("ignore", "some msg", DeprecationWarning, "", 0),
+ ),
+ (
+ "ignore::DeprecationWarning:mod",
+ False,
+ ("ignore", "", DeprecationWarning, "mod", 0),
+ ),
+ (
+ "ignore::DeprecationWarning:mod:42",
+ False,
+ ("ignore", "", DeprecationWarning, "mod", 42),
+ ),
+ ("error:some\\msg:::", True, ("error", "some\\\\msg", Warning, "", 0)),
+ ("error:::mod\\foo:", True, ("error", "", Warning, "mod\\\\foo\\Z", 0)),
+ ],
+)
+def test_parse_warning_filter(
+ arg: str, escape: bool, expected: "Tuple[str, str, Type[Warning], str, int]"
+) -> None:
+ assert parse_warning_filter(arg, escape=escape) == expected
+
+
+@pytest.mark.parametrize("arg", [":" * 5, "::::-1", "::::not-a-number"])
+def test_parse_warning_filter_failure(arg: str) -> None:
+ import warnings
+
+ with pytest.raises(warnings._OptionError):
+ parse_warning_filter(arg, escape=True)
def test_conftest_symlink(testdir):
- """
- conftest.py discovery follows normal path resolution and does not resolve symlinks.
- """
+ """`conftest.py` discovery follows normal path resolution and does not resolve symlinks."""
# Structure:
# /real
# /real/conftest.py
assert result.ret == ExitCode.USAGE_ERROR
-def test_conftest_existing_resultlog(testdir):
- x = testdir.mkdir("tests")
- x.join("conftest.py").write(
- textwrap.dedent(
- """\
- def pytest_addoption(parser):
- parser.addoption("--xyz", action="store_true")
- """
- )
- )
- testdir.makefile(ext=".log", result="") # Writes result.log
- result = testdir.runpytest("-h", "--resultlog", "result.log")
- result.stdout.fnmatch_lines(["*--xyz*"])
-
-
def test_conftest_existing_junitxml(testdir):
x = testdir.mkdir("tests")
x.join("conftest.py").write(
)
)
print("created directory structure:")
- for x in testdir.tmpdir.visit():
- print(" " + x.relto(testdir.tmpdir))
+ tmppath = Path(str(testdir.tmpdir))
+ for x in tmppath.rglob(""):
+ print(" " + str(x.relative_to(tmppath)))
return {"runner": runner, "package": package, "swc": swc, "snc": snc}
reprec.assertoutcome(failed=1)
def test_multiple_patterns(self, testdir):
- """Test support for multiple --doctest-glob arguments (#1255).
- """
+ """Test support for multiple --doctest-glob arguments (#1255)."""
testdir.maketxtfile(
xdoc="""
>>> 1
[("foo", "ascii"), ("öäü", "latin1"), ("öäü", "utf-8")],
)
def test_encoding(self, testdir, test_string, encoding):
- """Test support for doctest_encoding ini option.
- """
+ """Test support for doctest_encoding ini option."""
testdir.makeini(
"""
[pytest]
reprec.assertoutcome(failed=1, passed=0)
def test_contains_unicode(self, testdir):
- """Fix internal error with docstrings containing non-ascii characters.
- """
+ """Fix internal error with docstrings containing non-ascii characters."""
testdir.makepyfile(
'''\
def foo():
reprec.assertoutcome(skipped=1, failed=1, passed=0)
def test_junit_report_for_doctest(self, testdir):
- """
- #713: Fix --junit-xml option when used with --doctest-modules.
- """
+ """#713: Fix --junit-xml option when used with --doctest-modules."""
p = testdir.makepyfile(
"""
def foo():
result.stdout.fnmatch_lines(["* 1 passed *"])
def test_reportinfo(self, testdir):
- """
- Test case to make sure that DoctestItem.reportinfo() returns lineno.
- """
+ """Make sure that DoctestItem.reportinfo() returns lineno."""
p = testdir.makepyfile(
test_reportinfo="""
def foo(x):
SCOPES = ["module", "session", "class", "function"]
def test_doctest_module_session_fixture(self, testdir):
- """Test that session fixtures are initialized for doctest modules (#768)
- """
+ """Test that session fixtures are initialized for doctest modules (#768)."""
# session fixture which changes some global data, which will
# be accessed by doctests in a module
testdir.makeconftest(
def test_crash_near_exit(testdir):
"""Test that fault handler displays crashes that happen even after
- pytest is exiting (for example, when the interpreter is shutting down).
- """
+ pytest is exiting (for example, when the interpreter is shutting down)."""
testdir.makepyfile(
"""
import faulthandler
def test_disabled(testdir):
- """Test option to disable fault handler in the command line.
- """
+ """Test option to disable fault handler in the command line."""
testdir.makepyfile(
"""
import faulthandler
)
def test_timeout(testdir, enabled: bool) -> None:
"""Test option to dump tracebacks after a certain timeout.
+
If faulthandler is disabled, no traceback will be dumped.
"""
testdir.makepyfile(
@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"])
def test_cancel_timeout_on_hook(monkeypatch, hook_name):
"""Make sure that we are cancelling any scheduled traceback dumping due
- to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive
- exception (pytest-dev/pytest-faulthandler#14).
- """
+ to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any
+ other interactive exception (pytest-dev/pytest-faulthandler#14)."""
import faulthandler
from _pytest.faulthandler import FaultHandlerHooks
@pytest.mark.parametrize("faulthandler_timeout", [0, 2])
def test_already_initialized(faulthandler_timeout, testdir):
- """Test for faulthandler being initialized earlier than pytest (#6575)"""
+ """Test for faulthandler being initialized earlier than pytest (#6575)."""
testdir.makepyfile(
"""
def test():
from textwrap import dedent
-import py
-
import pytest
from _pytest.config.findpaths import get_common_ancestor
+from _pytest.config.findpaths import get_dirs_from_args
from _pytest.config.findpaths import load_config_dict_from_file
+from _pytest.pathlib import Path
class TestLoadConfigDictFromFile:
- def test_empty_pytest_ini(self, tmpdir):
+ def test_empty_pytest_ini(self, tmp_path: Path) -> None:
"""pytest.ini files are always considered for configuration, even if empty"""
- fn = tmpdir.join("pytest.ini")
- fn.write("")
+ fn = tmp_path / "pytest.ini"
+ fn.write_text("", encoding="utf-8")
assert load_config_dict_from_file(fn) == {}
- def test_pytest_ini(self, tmpdir):
+ def test_pytest_ini(self, tmp_path: Path) -> None:
"""[pytest] section in pytest.ini files is read correctly"""
- fn = tmpdir.join("pytest.ini")
- fn.write("[pytest]\nx=1")
+ fn = tmp_path / "pytest.ini"
+ fn.write_text("[pytest]\nx=1", encoding="utf-8")
assert load_config_dict_from_file(fn) == {"x": "1"}
- def test_custom_ini(self, tmpdir):
+ def test_custom_ini(self, tmp_path: Path) -> None:
"""[pytest] section in any .ini file is read correctly"""
- fn = tmpdir.join("custom.ini")
- fn.write("[pytest]\nx=1")
+ fn = tmp_path / "custom.ini"
+ fn.write_text("[pytest]\nx=1", encoding="utf-8")
assert load_config_dict_from_file(fn) == {"x": "1"}
- def test_custom_ini_without_section(self, tmpdir):
+ def test_custom_ini_without_section(self, tmp_path: Path) -> None:
"""Custom .ini files without [pytest] section are not considered for configuration"""
- fn = tmpdir.join("custom.ini")
- fn.write("[custom]")
+ fn = tmp_path / "custom.ini"
+ fn.write_text("[custom]", encoding="utf-8")
assert load_config_dict_from_file(fn) is None
- def test_custom_cfg_file(self, tmpdir):
+ def test_custom_cfg_file(self, tmp_path: Path) -> None:
"""Custom .cfg files without [tool:pytest] section are not considered for configuration"""
- fn = tmpdir.join("custom.cfg")
- fn.write("[custom]")
+ fn = tmp_path / "custom.cfg"
+ fn.write_text("[custom]", encoding="utf-8")
assert load_config_dict_from_file(fn) is None
- def test_valid_cfg_file(self, tmpdir):
+ def test_valid_cfg_file(self, tmp_path: Path) -> None:
"""Custom .cfg files with [tool:pytest] section are read correctly"""
- fn = tmpdir.join("custom.cfg")
- fn.write("[tool:pytest]\nx=1")
+ fn = tmp_path / "custom.cfg"
+ fn.write_text("[tool:pytest]\nx=1", encoding="utf-8")
assert load_config_dict_from_file(fn) == {"x": "1"}
- def test_unsupported_pytest_section_in_cfg_file(self, tmpdir):
+ def test_unsupported_pytest_section_in_cfg_file(self, tmp_path: Path) -> None:
""".cfg files with [pytest] section are no longer supported and should fail to alert users"""
- fn = tmpdir.join("custom.cfg")
- fn.write("[pytest]")
+ fn = tmp_path / "custom.cfg"
+ fn.write_text("[pytest]", encoding="utf-8")
with pytest.raises(pytest.fail.Exception):
load_config_dict_from_file(fn)
- def test_invalid_toml_file(self, tmpdir):
+ def test_invalid_toml_file(self, tmp_path: Path) -> None:
""".toml files without [tool.pytest.ini_options] are not considered for configuration."""
- fn = tmpdir.join("myconfig.toml")
- fn.write(
+ fn = tmp_path / "myconfig.toml"
+ fn.write_text(
dedent(
"""
[build_system]
x = 1
"""
- )
+ ),
+ encoding="utf-8",
)
assert load_config_dict_from_file(fn) is None
- def test_valid_toml_file(self, tmpdir):
+ def test_valid_toml_file(self, tmp_path: Path) -> None:
""".toml files with [tool.pytest.ini_options] are read correctly, including changing
data types to str/list for compatibility with other configuration options."""
- fn = tmpdir.join("myconfig.toml")
- fn.write(
+ fn = tmp_path / "myconfig.toml"
+ fn.write_text(
dedent(
"""
[tool.pytest.ini_options]
values = ["tests", "integration"]
name = "foo"
"""
- )
+ ),
+ encoding="utf-8",
)
assert load_config_dict_from_file(fn) == {
"x": "1",
class TestCommonAncestor:
- def test_has_ancestor(self, tmpdir):
- fn1 = tmpdir.join("foo/bar/test_1.py").ensure(file=1)
- fn2 = tmpdir.join("foo/zaz/test_2.py").ensure(file=1)
- assert get_common_ancestor([fn1, fn2]) == tmpdir.join("foo")
- assert get_common_ancestor([py.path.local(fn1.dirname), fn2]) == tmpdir.join(
- "foo"
- )
- assert get_common_ancestor(
- [py.path.local(fn1.dirname), py.path.local(fn2.dirname)]
- ) == tmpdir.join("foo")
- assert get_common_ancestor([fn1, py.path.local(fn2.dirname)]) == tmpdir.join(
- "foo"
- )
-
- def test_single_dir(self, tmpdir):
- assert get_common_ancestor([tmpdir]) == tmpdir
-
- def test_single_file(self, tmpdir):
- fn = tmpdir.join("foo.py").ensure(file=1)
- assert get_common_ancestor([fn]) == tmpdir
+ def test_has_ancestor(self, tmp_path: Path) -> None:
+ fn1 = tmp_path / "foo" / "bar" / "test_1.py"
+ fn1.parent.mkdir(parents=True)
+ fn1.touch()
+ fn2 = tmp_path / "foo" / "zaz" / "test_2.py"
+ fn2.parent.mkdir(parents=True)
+ fn2.touch()
+ assert get_common_ancestor([fn1, fn2]) == tmp_path / "foo"
+ assert get_common_ancestor([fn1.parent, fn2]) == tmp_path / "foo"
+ assert get_common_ancestor([fn1.parent, fn2.parent]) == tmp_path / "foo"
+ assert get_common_ancestor([fn1, fn2.parent]) == tmp_path / "foo"
+
+ def test_single_dir(self, tmp_path: Path) -> None:
+ assert get_common_ancestor([tmp_path]) == tmp_path
+
+ def test_single_file(self, tmp_path: Path) -> None:
+ fn = tmp_path / "foo.py"
+ fn.touch()
+ assert get_common_ancestor([fn]) == tmp_path
+
+
+def test_get_dirs_from_args(tmp_path):
+ """get_dirs_from_args() skips over non-existing directories and files"""
+ fn = tmp_path / "foo.py"
+ fn.touch()
+ d = tmp_path / "tests"
+ d.mkdir()
+ option = "--foobar=/foo.txt"
+ # xdist uses options in this format for its rsync feature (#7638)
+ xdist_rsync_option = "popen=c:/dest"
+ assert get_dirs_from_args(
+ [str(fn), str(tmp_path / "does_not_exist"), str(d), option, xdist_rsync_option]
+ ) == [fn.parent, d]
def test_none_help_param_raises_exception(testdir):
- """Tests a None help param raises a TypeError.
- """
+ """Test that a None help param raises a TypeError."""
testdir.makeconftest(
"""
def pytest_addoption(parser):
def test_empty_help_param(testdir):
- """Tests an empty help param is displayed correctly.
- """
+ """Test that an empty help param is displayed correctly."""
testdir.makeconftest(
"""
def pytest_addoption(parser):
@pytest.fixture(scope="session")
def schema():
- """Returns a xmlschema.XMLSchema object for the junit-10.xsd file"""
+ """Return an xmlschema.XMLSchema object for the junit-10.xsd file."""
fn = Path(__file__).parent / "example_scripts/junit-10.xsd"
with fn.open() as f:
return xmlschema.XMLSchema(f)
@pytest.fixture
def run_and_parse(testdir, schema):
- """
- Fixture that returns a function that can be used to execute pytest and return
- the parsed ``DomNode`` of the root xml node.
+ """Fixture that returns a function that can be used to execute pytest and
+ return the parsed ``DomNode`` of the root xml node.
The ``family`` parameter is used to configure the ``junit_family`` of the written report.
"xunit2" is also automatically validated against the schema.
node = dom.find_first_by_tag("testsuite")
node.assert_attr(errors=1, failures=1, tests=1)
first, second = dom.find_by_tag("testcase")
- if not first or not second or first == second:
- assert 0
+ assert first
+ assert second
+ assert first != second
fnode = first.find_first_by_tag("failure")
fnode.assert_attr(message="Exception: Call Exception")
snode = second.find_first_by_tag("error")
node = dom.find_first_by_tag("testsuite")
tnode = node.find_first_by_tag("testcase")
fnode = tnode.find_first_by_tag("failure")
- fnode.assert_attr(message="AssertionError: An error assert 0")
+ fnode.assert_attr(message="AssertionError: An error\nassert 0")
@parametrize_families
def test_failure_escape(self, testdir, run_and_parse, xunit_family):
assert "hx" in fnode.toxml()
def test_assertion_binchars(self, testdir, run_and_parse):
- """this test did fail when the escaping wasnt strict"""
+ """This test did fail when the escaping wasn't strict."""
testdir.makepyfile(
"""
# 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF)
for i in invalid:
- got = bin_xml_escape(chr(i)).uniobj
+ got = bin_xml_escape(chr(i))
if i <= 0xFF:
expected = "#x%02X" % i
else:
expected = "#x%04X" % i
assert got == expected
for i in valid:
- assert chr(i) == bin_xml_escape(chr(i)).uniobj
+ assert chr(i) == bin_xml_escape(chr(i))
def test_logxml_path_expansion(tmpdir, monkeypatch):
@pytest.mark.filterwarnings("default")
@pytest.mark.parametrize("fixture_name", ["record_xml_attribute", "record_property"])
def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse):
- """Ensure record_xml_attribute and record_property drop values when outside of legacy family
- """
+ """Ensure record_xml_attribute and record_property drop values when outside of legacy family."""
testdir.makeini(
"""
[pytest]
def test_random_report_log_xdist(testdir, monkeypatch, run_and_parse):
- """xdist calls pytest_runtest_logreport as they are executed by the workers,
+ """`xdist` calls pytest_runtest_logreport as they are executed by the workers,
with nodes from several nodes overlapping, so junitxml must cope with that
- to produce correct reports. #1064
- """
+ to produce correct reports (#1064)."""
pytest.importorskip("xdist")
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
testdir.makepyfile(
def test_link_resolve(testdir: pytester.Testdir) -> None:
- """
- See: https://github.com/pytest-dev/pytest/issues/5965
- """
+ """See: https://github.com/pytest-dev/pytest/issues/5965."""
sub1 = testdir.mkpydir("sub1")
p = sub1.join("test_foo.py")
p.write(
import argparse
+import os
+import re
from typing import Optional
+import py.path
+
import pytest
from _pytest.config import ExitCode
+from _pytest.config import UsageError
+from _pytest.main import resolve_collection_argument
from _pytest.main import validate_basetemp
+from _pytest.pathlib import Path
from _pytest.pytester import Testdir
def test_validate_basetemp_integration(testdir):
result = testdir.runpytest("--basetemp=.")
result.stderr.fnmatch_lines("*basetemp must not be*")
+
+
+class TestResolveCollectionArgument:
+ @pytest.fixture
+ def invocation_dir(self, testdir: Testdir) -> py.path.local:
+ testdir.syspathinsert(str(testdir.tmpdir / "src"))
+ testdir.chdir()
+
+ pkg = testdir.tmpdir.join("src/pkg").ensure_dir()
+ pkg.join("__init__.py").ensure()
+ pkg.join("test.py").ensure()
+ return testdir.tmpdir
+
+ @pytest.fixture
+ def invocation_path(self, invocation_dir: py.path.local) -> Path:
+ return Path(str(invocation_dir))
+
+ def test_file(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
+ """File and parts."""
+ assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == (
+ invocation_dir / "src/pkg/test.py",
+ [],
+ )
+ assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == (
+ invocation_dir / "src/pkg/test.py",
+ [""],
+ )
+ assert resolve_collection_argument(
+ invocation_path, "src/pkg/test.py::foo::bar"
+ ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"])
+ assert resolve_collection_argument(
+ invocation_path, "src/pkg/test.py::foo::bar::"
+ ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar", ""])
+
+ def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
+ """Directory and parts."""
+ assert resolve_collection_argument(invocation_path, "src/pkg") == (
+ invocation_dir / "src/pkg",
+ [],
+ )
+
+ with pytest.raises(
+ UsageError, match=r"directory argument cannot contain :: selection parts"
+ ):
+ resolve_collection_argument(invocation_path, "src/pkg::")
+
+ with pytest.raises(
+ UsageError, match=r"directory argument cannot contain :: selection parts"
+ ):
+ resolve_collection_argument(invocation_path, "src/pkg::foo::bar")
+
+ def test_pypath(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
+ """Dotted name and parts."""
+ assert resolve_collection_argument(
+ invocation_path, "pkg.test", as_pypath=True
+ ) == (invocation_dir / "src/pkg/test.py", [])
+ assert resolve_collection_argument(
+ invocation_path, "pkg.test::foo::bar", as_pypath=True
+ ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"])
+ assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == (
+ invocation_dir / "src/pkg",
+ [],
+ )
+
+ with pytest.raises(
+ UsageError, match=r"package argument cannot contain :: selection parts"
+ ):
+ resolve_collection_argument(
+ invocation_path, "pkg::foo::bar", as_pypath=True
+ )
+
+ def test_does_not_exist(self, invocation_path: Path) -> None:
+ """Given a file/module that does not exist raises UsageError."""
+ with pytest.raises(
+ UsageError, match=re.escape("file or directory not found: foobar")
+ ):
+ resolve_collection_argument(invocation_path, "foobar")
+
+ with pytest.raises(
+ UsageError,
+ match=re.escape(
+ "module or package not found: foobar (missing __init__.py?)"
+ ),
+ ):
+ resolve_collection_argument(invocation_path, "foobar", as_pypath=True)
+
+ def test_absolute_paths_are_resolved_correctly(
+ self, invocation_dir: py.path.local, invocation_path: Path
+ ) -> None:
+ """Absolute paths resolve back to absolute paths."""
+ full_path = str(invocation_dir / "src")
+ assert resolve_collection_argument(invocation_path, full_path) == (
+ py.path.local(os.path.abspath("src")),
+ [],
+ )
+
+ # ensure full paths given in the command-line without the drive letter resolve
+ # to the full path correctly (#7628)
+ drive, full_path_without_drive = os.path.splitdrive(full_path)
+ assert resolve_collection_argument(
+ invocation_path, full_path_without_drive
+ ) == (py.path.local(os.path.abspath("src")), [])
+
+
+def test_module_full_path_without_drive(testdir):
+ """Collect and run test using full path except for the drive letter (#7628).
+
+ Passing a full path without a drive letter would trigger a bug in py.path.local
+ where it would keep the full path without the drive letter around, instead of resolving
+ to the full path, resulting in fixtures node ids not matching against test node ids correctly.
+ """
+ testdir.makepyfile(
+ **{
+ "project/conftest.py": """
+ import pytest
+ @pytest.fixture
+ def fix(): return 1
+ """,
+ }
+ )
+
+ testdir.makepyfile(
+ **{
+ "project/tests/dummy_test.py": """
+ def test(fix):
+ assert fix == 1
+ """
+ }
+ )
+ fn = testdir.tmpdir.join("project/tests/dummy_test.py")
+ assert fn.isfile()
+
+ drive, path = os.path.splitdrive(str(fn))
+
+ result = testdir.runpytest(path, "-v")
+ result.stdout.fnmatch_lines(
+ [
+ os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *",
+ "* 1 passed in *",
+ ]
+ )
import pytest
from _pytest.config import ExitCode
-from _pytest.mark import EMPTY_PARAMETERSET_OPTION
from _pytest.mark import MarkGenerator as Mark
+from _pytest.mark.structures import EMPTY_PARAMETERSET_OPTION
from _pytest.nodes import Collector
from _pytest.nodes import Node
def test_parametrized_collected_from_command_line(testdir):
- """Parametrized test not collected if test named specified
- in command line issue#649.
- """
+ """Parametrized test not collected if test named specified in command
+ line issue#649."""
py_file = testdir.makepyfile(
"""
import pytest
def test_parametrize_iterator(testdir):
- """parametrize should work with generators (#5354)."""
+ """`parametrize` should work with generators (#5354)."""
py_file = testdir.makepyfile(
"""\
import pytest
reprec.assertoutcome(passed=1)
def assert_markers(self, items, **expected):
- """assert that given items have expected marker names applied to them.
- expected should be a dict of (item name -> seq of expected marker names)
+ """Assert that given items have expected marker names applied to them.
+ expected should be a dict of (item name -> seq of expected marker names).
- .. note:: this could be moved to ``testdir`` if proven to be useful
+ Note: this could be moved to ``testdir`` if proven to be useful
to other modules.
"""
-
items = {x.name: x for x in items}
for name, expected_markers in expected.items():
markers = {m.name for m in items[name].iter_markers()}
assert len(deselected_tests) == 1
def test_no_match_directories_outside_the_suite(self, testdir):
- """
- -k should not match against directories containing the test suite (#7040).
- """
+ """`-k` should not match against directories containing the test suite (#7040)."""
test_contents = """
def test_aaa(): pass
def test_ddd(): pass
-"""
-Test importing of all internal packages and modules.
+"""Test importing of all internal packages and modules.
This ensures all internal packages can be imported without needing the pytest
namespace being set, which is critical for the initialization of xdist.
monkeypatch.setattr(Sample, "hello", None)
assert Sample.hello is None
- monkeypatch.undo()
+ monkeypatch.undo() # type: ignore[unreachable]
assert Sample.hello()
@pytest.fixture
def mocked_urlopen_fail(self, monkeypatch):
- """
- monkeypatch the actual urlopen call to emulate a HTTP Error 400
- """
+ """Monkeypatch the actual urlopen call to emulate a HTTP Error 400."""
calls = []
import urllib.error
@pytest.fixture
def mocked_urlopen_invalid(self, monkeypatch):
- """
- monkeypatch the actual urlopen calls done by the internal plugin
+ """Monkeypatch the actual urlopen calls done by the internal plugin
function that connects to bpaste service, but return a url in an
- unexpected format
- """
+ unexpected format."""
calls = []
def mocked(url, data):
@pytest.fixture
def mocked_urlopen(self, monkeypatch):
- """
- monkeypatch the actual urlopen calls done by the internal plugin
- function that connects to bpaste service.
- """
+ """Monkeypatch the actual urlopen calls done by the internal plugin
+ function that connects to bpaste service."""
calls = []
def mocked(url, data):
import py
import pytest
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import commonpath
from _pytest.pathlib import ensure_deletable
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import get_extended_length_path_str
class TestFNMatcherPort:
- """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the same results as the
- original py.path.local.fnmatch method.
- """
+ """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the
+ same results as the original py.path.local.fnmatch method."""
@pytest.fixture(params=["pathlib", "py.path"])
def match(self, request):
return fn
def test_importmode_importlib(self, simple_module):
- """importlib mode does not change sys.path"""
+ """`importlib` mode does not change sys.path."""
module = import_path(simple_module, mode="importlib")
assert module.foo(2) == 42 # type: ignore[attr-defined]
assert simple_module.dirname not in sys.path
def test_importmode_twice_is_different_module(self, simple_module):
- """importlib mode always returns a new module"""
+ """`importlib` mode always returns a new module."""
module1 = import_path(simple_module, mode="importlib")
module2 = import_path(simple_module, mode="importlib")
assert module1 is not module2
def test_no_meta_path_found(self, simple_module, monkeypatch):
- """Even without any meta_path should still import module"""
+ """Even without any meta_path should still import module."""
monkeypatch.setattr(sys, "meta_path", [])
module = import_path(simple_module, mode="importlib")
assert module.foo(2) == 42 # type: ignore[attr-defined]
# check now that we can remove the lock file in normal circumstances
assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30)
assert not lock.is_file()
+
+
+def test_bestrelpath() -> None:
+ curdir = Path("/foo/bar/baz/path")
+ assert bestrelpath(curdir, curdir) == "."
+ assert bestrelpath(curdir, curdir / "hello" / "world") == "hello" + os.sep + "world"
+ assert bestrelpath(curdir, curdir.parent / "sister") == ".." + os.sep + "sister"
+ assert bestrelpath(curdir, curdir.parent) == ".."
+ assert bestrelpath(curdir, Path("hello")) == "hello"
+
+
+def test_commonpath() -> None:
+ path = Path("/foo/bar/baz/path")
+ subpath = path / "sampledir"
+ assert commonpath(path, subpath) == path
+ assert commonpath(subpath, path) == path
+ assert commonpath(Path(str(path) + "suffix"), path) == path.parent
+ assert commonpath(path, path.parent.parent) == path.parent.parent
def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister(
self, pytestpm
):
- """ From PR #4304 : The only way to unregister a module is documented at
+ """From PR #4304: The only way to unregister a module is documented at
the end of https://docs.pytest.org/en/stable/plugins.html.
- When unregister cacheprovider, then unregister stepwise too
+ When unregister cacheprovider, then unregister stepwise too.
"""
pytestpm.register(42, name="cacheprovider")
pytestpm.register(43, name="stepwise")
recorder = testdir.make_hook_recorder(item.config.pluginmanager)
assert not recorder.getfailures()
- pytest.xfail("internal reportrecorder tests need refactoring")
+ # (The silly condition is to fool mypy that the code below this is reachable)
+ if 1 + 1 == 2:
+ pytest.xfail("internal reportrecorder tests need refactoring")
class rep:
excinfo = None
def make_holder():
class apiclass:
def pytest_xyz(self, arg):
- "x"
+ """X"""
def pytest_xyz_noarg(self):
- "x"
+ """X"""
apimod = type(os)("api")
def pytest_xyz(arg):
- "x"
+ """X"""
def pytest_xyz_noarg():
- "x"
+ """X"""
apimod.pytest_xyz = pytest_xyz # type: ignore
apimod.pytest_xyz_noarg = pytest_xyz_noarg # type: ignore
import sys
+from typing import Sequence
+from typing import Union
import pytest
from _pytest._code.code import ExceptionChainRepr
+from _pytest._code.code import ExceptionRepr
+from _pytest.config import Config
from _pytest.pathlib import Path
+from _pytest.pytester import Testdir
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
class TestReportSerialization:
- def test_xdist_longrepr_to_str_issue_241(self, testdir):
- """
- Regarding issue pytest-xdist#241
+ def test_xdist_longrepr_to_str_issue_241(self, testdir: Testdir) -> None:
+ """Regarding issue pytest-xdist#241.
This test came originally from test_remote.py in xdist (ca03269).
"""
assert test_b_call.outcome == "passed"
assert test_b_call._to_json()["longrepr"] is None
- def test_xdist_report_longrepr_reprcrash_130(self, testdir) -> None:
+ def test_xdist_report_longrepr_reprcrash_130(self, testdir: Testdir) -> None:
"""Regarding issue pytest-xdist#130
This test came originally from test_remote.py in xdist (ca03269).
assert len(reports) == 3
rep = reports[1]
added_section = ("Failure Metadata", "metadata metadata", "*")
+ assert isinstance(rep.longrepr, ExceptionRepr)
rep.longrepr.sections.append(added_section)
d = rep._to_json()
a = TestReport._from_json(d)
- assert a.longrepr is not None
+ assert isinstance(a.longrepr, ExceptionRepr)
# Check assembled == rep
assert a.__dict__.keys() == rep.__dict__.keys()
for key in rep.__dict__.keys():
if key != "longrepr":
assert getattr(a, key) == getattr(rep, key)
+ assert rep.longrepr.reprcrash is not None
+ assert a.longrepr.reprcrash is not None
assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno
assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message
assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path
# Missing section attribute PR171
assert added_section in a.longrepr.sections
- def test_reprentries_serialization_170(self, testdir) -> None:
+ def test_reprentries_serialization_170(self, testdir: Testdir) -> None:
"""Regarding issue pytest-xdist#170
This test came originally from test_remote.py in xdist (ca03269).
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3
rep = reports[1]
+ assert isinstance(rep.longrepr, ExceptionRepr)
d = rep._to_json()
a = TestReport._from_json(d)
- assert a.longrepr is not None
+ assert isinstance(a.longrepr, ExceptionRepr)
rep_entries = rep.longrepr.reprtraceback.reprentries
a_entries = a.longrepr.reprtraceback.reprentries
for i in range(len(a_entries)):
- assert isinstance(rep_entries[i], ReprEntry)
- assert rep_entries[i].lines == a_entries[i].lines
- assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno
- assert (
- rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message
- )
- assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path
- assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args
- assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines
- assert rep_entries[i].style == a_entries[i].style
-
- def test_reprentries_serialization_196(self, testdir) -> None:
+ rep_entry = rep_entries[i]
+ assert isinstance(rep_entry, ReprEntry)
+ assert rep_entry.reprfileloc is not None
+ assert rep_entry.reprfuncargs is not None
+ assert rep_entry.reprlocals is not None
+
+ a_entry = a_entries[i]
+ assert isinstance(a_entry, ReprEntry)
+ assert a_entry.reprfileloc is not None
+ assert a_entry.reprfuncargs is not None
+ assert a_entry.reprlocals is not None
+
+ assert rep_entry.lines == a_entry.lines
+ assert rep_entry.reprfileloc.lineno == a_entry.reprfileloc.lineno
+ assert rep_entry.reprfileloc.message == a_entry.reprfileloc.message
+ assert rep_entry.reprfileloc.path == a_entry.reprfileloc.path
+ assert rep_entry.reprfuncargs.args == a_entry.reprfuncargs.args
+ assert rep_entry.reprlocals.lines == a_entry.reprlocals.lines
+ assert rep_entry.style == a_entry.style
+
+ def test_reprentries_serialization_196(self, testdir: Testdir) -> None:
"""Regarding issue pytest-xdist#196
This test came originally from test_remote.py in xdist (ca03269).
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3
rep = reports[1]
+ assert isinstance(rep.longrepr, ExceptionRepr)
d = rep._to_json()
a = TestReport._from_json(d)
- assert a.longrepr is not None
+ assert isinstance(a.longrepr, ExceptionRepr)
rep_entries = rep.longrepr.reprtraceback.reprentries
a_entries = a.longrepr.reprtraceback.reprentries
assert isinstance(rep_entries[i], ReprEntryNative)
assert rep_entries[i].lines == a_entries[i].lines
- def test_itemreport_outcomes(self, testdir):
- """
- This test came originally from test_remote.py in xdist (ca03269).
- """
+ def test_itemreport_outcomes(self, testdir: Testdir) -> None:
+ # This test came originally from test_remote.py in xdist (ca03269).
reprec = testdir.inline_runsource(
"""
import pytest
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
if newrep.skipped and not hasattr(newrep, "wasxfail"):
- assert newrep.longrepr is not None
+ assert isinstance(newrep.longrepr, tuple)
assert len(newrep.longrepr) == 3
assert newrep.outcome == rep.outcome
assert newrep.when == rep.when
if rep.failed:
assert newrep.longreprtext == rep.longreprtext
- def test_collectreport_passed(self, testdir):
+ def test_collectreport_passed(self, testdir: Testdir) -> None:
"""This test came originally from test_remote.py in xdist (ca03269)."""
reprec = testdir.inline_runsource("def test_func(): pass")
reports = reprec.getreports("pytest_collectreport")
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
- def test_collectreport_fail(self, testdir):
+ def test_collectreport_fail(self, testdir: Testdir) -> None:
"""This test came originally from test_remote.py in xdist (ca03269)."""
reprec = testdir.inline_runsource("qwe abc")
reports = reprec.getreports("pytest_collectreport")
if rep.failed:
assert newrep.longrepr == str(rep.longrepr)
- def test_extended_report_deserialization(self, testdir):
+ def test_extended_report_deserialization(self, testdir: Testdir) -> None:
"""This test came originally from test_remote.py in xdist (ca03269)."""
reprec = testdir.inline_runsource("qwe abc")
reports = reprec.getreports("pytest_collectreport")
assert reports
for rep in reports:
- rep.extra = True
+ rep.extra = True # type: ignore[attr-defined]
d = rep._to_json()
newrep = CollectReport._from_json(d)
assert newrep.extra
if rep.failed:
assert newrep.longrepr == str(rep.longrepr)
- def test_paths_support(self, testdir):
+ def test_paths_support(self, testdir: Testdir) -> None:
"""Report attributes which are py.path or pathlib objects should become strings."""
testdir.makepyfile(
"""
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3
test_a_call = reports[1]
- test_a_call.path1 = testdir.tmpdir
- test_a_call.path2 = Path(testdir.tmpdir)
+ test_a_call.path1 = testdir.tmpdir # type: ignore[attr-defined]
+ test_a_call.path2 = Path(testdir.tmpdir) # type: ignore[attr-defined]
data = test_a_call._to_json()
assert data["path1"] == str(testdir.tmpdir)
assert data["path2"] == str(testdir.tmpdir)
- def test_deserialization_failure(self, testdir):
+ def test_deserialization_failure(self, testdir: Testdir) -> None:
"""Check handling of failure during deserialization of report types."""
testdir.makepyfile(
"""
TestReport._from_json(data)
@pytest.mark.parametrize("report_class", [TestReport, CollectReport])
- def test_chained_exceptions(self, testdir, tw_mock, report_class):
+ def test_chained_exceptions(self, testdir: Testdir, tw_mock, report_class) -> None:
"""Check serialization/deserialization of report objects containing chained exceptions (#5786)"""
testdir.makepyfile(
"""
reprec = testdir.inline_run()
if report_class is TestReport:
- reports = reprec.getreports("pytest_runtest_logreport")
+ reports = reprec.getreports(
+ "pytest_runtest_logreport"
+ ) # type: Union[Sequence[TestReport], Sequence[CollectReport]]
# we have 3 reports: setup/call/teardown
assert len(reports) == 3
# get the call report
assert len(reports) == 2
report = reports[1]
- def check_longrepr(longrepr):
+ def check_longrepr(longrepr: ExceptionChainRepr) -> None:
"""Check the attributes of the given longrepr object according to the test file.
We can get away with testing both CollectReport and TestReport with this function because
assert report.failed
assert len(report.sections) == 0
+ assert isinstance(report.longrepr, ExceptionChainRepr)
report.longrepr.addsection("title", "contents", "=")
check_longrepr(report.longrepr)
# elsewhere and we do check the contents of the longrepr object after loading it.
loaded_report.longrepr.toterminal(tw_mock)
- def test_chained_exceptions_no_reprcrash(self, testdir, tw_mock) -> None:
+ def test_chained_exceptions_no_reprcrash(self, testdir: Testdir, tw_mock) -> None:
"""Regression test for tracebacks without a reprcrash (#5971)
This happens notably on exceptions raised by multiprocess.pool: the exception transfer
reports = reprec.getreports("pytest_runtest_logreport")
- def check_longrepr(longrepr) -> None:
+ def check_longrepr(longrepr: object) -> None:
assert isinstance(longrepr, ExceptionChainRepr)
assert len(longrepr.chain) == 2
entry1, entry2 = longrepr.chain
# for same reasons as previous test, ensure we don't blow up here
assert loaded_report.longrepr is not None
+ assert isinstance(loaded_report.longrepr, ExceptionChainRepr)
loaded_report.longrepr.toterminal(tw_mock)
- def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir):
+ def test_report_prevent_ConftestImportFailure_hiding_exception(
+ self, testdir: Testdir
+ ) -> None:
sub_dir = testdir.tmpdir.join("ns").ensure_dir()
sub_dir.join("conftest").new(ext=".py").write("import unknown")
class TestHooks:
"""Test that the hooks are working correctly for plugins"""
- def test_test_report(self, testdir, pytestconfig):
+ def test_test_report(self, testdir: Testdir, pytestconfig: Config) -> None:
testdir.makepyfile(
"""
def test_a(): assert False
assert new_rep.when == rep.when
assert new_rep.outcome == rep.outcome
- def test_collect_report(self, testdir, pytestconfig):
+ def test_collect_report(self, testdir: Testdir, pytestconfig: Config) -> None:
testdir.makepyfile(
"""
def test_a(): assert False
@pytest.mark.parametrize(
"hook_name", ["pytest_runtest_logreport", "pytest_collectreport"]
)
- def test_invalid_report_types(self, testdir, pytestconfig, hook_name):
+ def test_invalid_report_types(
+ self, testdir: Testdir, pytestconfig: Config, hook_name: str
+ ) -> None:
testdir.makepyfile(
"""
def test_a(): pass
+++ /dev/null
-import os
-from io import StringIO
-
-import _pytest._code
-import pytest
-from _pytest.resultlog import pytest_configure
-from _pytest.resultlog import pytest_unconfigure
-from _pytest.resultlog import ResultLog
-from _pytest.resultlog import resultlog_key
-
-pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated")
-
-
-def test_write_log_entry():
- reslog = ResultLog(None, None)
- reslog.logfile = StringIO()
- reslog.write_log_entry("name", ".", "")
- entry = reslog.logfile.getvalue()
- assert entry[-1] == "\n"
- entry_lines = entry.splitlines()
- assert len(entry_lines) == 1
- assert entry_lines[0] == ". name"
-
- reslog.logfile = StringIO()
- reslog.write_log_entry("name", "s", "Skipped")
- entry = reslog.logfile.getvalue()
- assert entry[-1] == "\n"
- entry_lines = entry.splitlines()
- assert len(entry_lines) == 2
- assert entry_lines[0] == "s name"
- assert entry_lines[1] == " Skipped"
-
- reslog.logfile = StringIO()
- reslog.write_log_entry("name", "s", "Skipped\n")
- entry = reslog.logfile.getvalue()
- assert entry[-1] == "\n"
- entry_lines = entry.splitlines()
- assert len(entry_lines) == 2
- assert entry_lines[0] == "s name"
- assert entry_lines[1] == " Skipped"
-
- reslog.logfile = StringIO()
- longrepr = " tb1\n tb 2\nE tb3\nSome Error"
- reslog.write_log_entry("name", "F", longrepr)
- entry = reslog.logfile.getvalue()
- assert entry[-1] == "\n"
- entry_lines = entry.splitlines()
- assert len(entry_lines) == 5
- assert entry_lines[0] == "F name"
- assert entry_lines[1:] == [" " + line for line in longrepr.splitlines()]
-
-
-class TestWithFunctionIntegration:
- # XXX (hpk) i think that the resultlog plugin should
- # provide a Parser object so that one can remain
- # ignorant regarding formatting details.
- def getresultlog(self, testdir, arg):
- resultlog = testdir.tmpdir.join("resultlog")
- testdir.plugins.append("resultlog")
- args = ["--resultlog=%s" % resultlog] + [arg]
- testdir.runpytest(*args)
- return [x for x in resultlog.readlines(cr=0) if x]
-
- def test_collection_report(self, testdir):
- ok = testdir.makepyfile(test_collection_ok="")
- fail = testdir.makepyfile(test_collection_fail="XXX")
- lines = self.getresultlog(testdir, ok)
- assert not lines
-
- lines = self.getresultlog(testdir, fail)
- assert lines
- assert lines[0].startswith("F ")
- assert lines[0].endswith("test_collection_fail.py"), lines[0]
- for x in lines[1:]:
- assert x.startswith(" ")
- assert "XXX" in "".join(lines[1:])
-
- def test_log_test_outcomes(self, testdir):
- mod = testdir.makepyfile(
- test_mod="""
- import pytest
- def test_pass(): pass
- def test_skip(): pytest.skip("hello")
- def test_fail(): raise ValueError("FAIL")
-
- @pytest.mark.xfail
- def test_xfail(): raise ValueError("XFAIL")
- @pytest.mark.xfail
- def test_xpass(): pass
-
- """
- )
- lines = self.getresultlog(testdir, mod)
- assert len(lines) >= 3
- assert lines[0].startswith(". ")
- assert lines[0].endswith("test_pass")
- assert lines[1].startswith("s "), lines[1]
- assert lines[1].endswith("test_skip")
- assert lines[2].find("hello") != -1
-
- assert lines[3].startswith("F ")
- assert lines[3].endswith("test_fail")
- tb = "".join(lines[4:8])
- assert tb.find('raise ValueError("FAIL")') != -1
-
- assert lines[8].startswith("x ")
- tb = "".join(lines[8:14])
- assert tb.find('raise ValueError("XFAIL")') != -1
-
- assert lines[14].startswith("X ")
- assert len(lines) == 15
-
- @pytest.mark.parametrize("style", ("native", "long", "short"))
- def test_internal_exception(self, style):
- # they are produced for example by a teardown failing
- # at the end of the run or a failing hook invocation
- try:
- raise ValueError
- except ValueError:
- excinfo = _pytest._code.ExceptionInfo.from_current()
- reslog = ResultLog(None, StringIO())
- reslog.pytest_internalerror(excinfo.getrepr(style=style))
- entry = reslog.logfile.getvalue()
- entry_lines = entry.splitlines()
-
- assert entry_lines[0].startswith("! ")
- if style != "native":
- assert os.path.basename(__file__)[:-9] in entry_lines[0] # .pyc/class
- assert entry_lines[-1][0] == " "
- assert "ValueError" in entry
-
-
-def test_generic(testdir, LineMatcher):
- testdir.plugins.append("resultlog")
- testdir.makepyfile(
- """
- import pytest
- def test_pass():
- pass
- def test_fail():
- assert 0
- def test_skip():
- pytest.skip("")
- @pytest.mark.xfail
- def test_xfail():
- assert 0
- @pytest.mark.xfail(run=False)
- def test_xfail_norun():
- assert 0
- """
- )
- testdir.runpytest("--resultlog=result.log")
- lines = testdir.tmpdir.join("result.log").readlines(cr=0)
- LineMatcher(lines).fnmatch_lines(
- [
- ". *:test_pass",
- "F *:test_fail",
- "s *:test_skip",
- "x *:test_xfail",
- "x *:test_xfail_norun",
- ]
- )
-
-
-def test_makedir_for_resultlog(testdir, LineMatcher):
- """--resultlog should automatically create directories for the log file"""
- testdir.plugins.append("resultlog")
- testdir.makepyfile(
- """
- import pytest
- def test_pass():
- pass
- """
- )
- testdir.runpytest("--resultlog=path/to/result.log")
- lines = testdir.tmpdir.join("path/to/result.log").readlines(cr=0)
- LineMatcher(lines).fnmatch_lines([". *:test_pass"])
-
-
-def test_no_resultlog_on_workers(testdir):
- config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog")
-
- assert resultlog_key not in config._store
- pytest_configure(config)
- assert resultlog_key in config._store
- pytest_unconfigure(config)
- assert resultlog_key not in config._store
-
- config.workerinput = {}
- pytest_configure(config)
- assert resultlog_key not in config._store
- pytest_unconfigure(config)
- assert resultlog_key not in config._store
-
-
-def test_unknown_teststatus(testdir):
- """Ensure resultlog correctly handles unknown status from pytest_report_teststatus
-
- Inspired on pytest-rerunfailures.
- """
- testdir.makepyfile(
- """
- def test():
- assert 0
- """
- )
- testdir.makeconftest(
- """
- import pytest
-
- def pytest_report_teststatus(report):
- if report.outcome == 'rerun':
- return "rerun", "r", "RERUN"
-
- @pytest.hookimpl(hookwrapper=True)
- def pytest_runtest_makereport():
- res = yield
- report = res.get_result()
- if report.when == "call":
- report.outcome = 'rerun'
- """
- )
- result = testdir.runpytest("--resultlog=result.log")
- result.stdout.fnmatch_lines(
- ["test_unknown_teststatus.py r *[[]100%[]]", "* 1 rerun *"]
- )
-
- lines = testdir.tmpdir.join("result.log").readlines(cr=0)
- assert lines[0] == "r test_unknown_teststatus.py::test"
-
-
-def test_failure_issue380(testdir):
- testdir.makeconftest(
- """
- import pytest
- class MyCollector(pytest.File):
- def collect(self):
- raise ValueError()
- def repr_failure(self, excinfo):
- return "somestring"
- def pytest_collect_file(path, parent):
- return MyCollector(parent=parent, fspath=path)
- """
- )
- testdir.makepyfile(
- """
- def test_func():
- pass
- """
- )
- result = testdir.runpytest("--resultlog=log")
- assert result.ret == 2
assert reps[5].failed
def test_exact_teardown_issue1206(self, testdir) -> None:
- """issue shadowing error with wrong number of arguments on teardown_method."""
+ """Issue shadowing error with wrong number of arguments on teardown_method."""
rec = testdir.inline_runsource(
"""
import pytest
def test_importorskip_module_level(testdir) -> None:
- """importorskip must be able to skip entire modules when used at module level"""
+ """`importorskip` must be able to skip entire modules when used at module level."""
testdir.makepyfile(
"""
import pytest
def test_importorskip_custom_reason(testdir) -> None:
- """make sure custom reasons are used"""
+ """Make sure custom reasons are used."""
testdir.makepyfile(
"""
import pytest
def test_store_except_info_on_error() -> None:
- """ Test that upon test failure, the exception info is stored on
- sys.last_traceback and friends.
- """
+ """Test that upon test failure, the exception info is stored on
+ sys.last_traceback and friends."""
# Simulate item that might raise a specific exception, depending on `raise_error` class var
class ItemMightRaise:
nodeid = "item_that_raises"
class TestReportContents:
- """
- Test user-level API of ``TestReport`` objects.
- """
+ """Test user-level API of ``TestReport`` objects."""
def getrunner(self):
return lambda item: runner.runtestprotocol(item, log=False)
-"""
- test correct setup/teardowns at
- module, class, and instance level
-"""
+"""Test correct setup/teardowns at module, class, and instance level."""
from typing import List
import pytest
def test_setup_teardown_function_level_with_optional_argument(
testdir, monkeypatch, arg: str,
) -> None:
- """parameter to setup/teardown xunit-style functions parameter is now optional (#1728)."""
+ """Parameter to setup/teardown xunit-style functions parameter is now optional (#1728)."""
import sys
trace_setups_teardowns = [] # type: List[str]
def test_show_fixtures_and_execute_test(testdir):
- """ Verifies that setups are shown and tests are executed. """
+ """Verify that setups are shown and tests are executed."""
p = testdir.makepyfile(
"""
import pytest
def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test):
- """ Verifies that fixtures are not executed. """
+ """Verify that fixtures are not executed."""
testdir.makepyfile(
"""
import pytest
def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir):
- """
- Verify that when a fixture lives for longer than a single test, --setup-plan
+ """Verify that when a fixture lives for longer than a single test, --setup-plan
correctly displays the SETUP/TEARDOWN indicators the right number of times.
As reported in https://github.com/pytest-dev/pytest/issues/2049
def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show(testdir):
- """
- Verify that SETUP/TEARDOWN messages match what comes out of --setup-show.
- """
+ """Verify that SETUP/TEARDOWN messages match what comes out of --setup-show."""
testdir.makepyfile(
"""
import pytest
assert callreport.wasxfail == "this is an xfail"
def test_xfail_using_platform(self, testdir):
- """
- Verify that platform can be used with xfail statements.
- """
+ """Verify that platform can be used with xfail statements."""
item = testdir.getitem(
"""
import pytest
result.stdout.fnmatch_lines([matchline])
def test_strict_sanity(self, testdir):
- """sanity check for xfail(strict=True): a failing test should behave
- exactly like a normal xfail.
- """
+ """Sanity check for xfail(strict=True): a failing test should behave
+ exactly like a normal xfail."""
p = testdir.makepyfile(
"""
import pytest
def test_module_level_skip_error(testdir):
- """
- Verify that using pytest.skip at module level causes a collection error
- """
+ """Verify that using pytest.skip at module level causes a collection error."""
testdir.makepyfile(
"""
import pytest
def test_module_level_skip_with_allow_module_level(testdir):
- """
- Verify that using pytest.skip(allow_module_level=True) is allowed
- """
+ """Verify that using pytest.skip(allow_module_level=True) is allowed."""
testdir.makepyfile(
"""
import pytest
def test_invalid_skip_keyword_parameter(testdir):
- """
- Verify that using pytest.skip() with unknown parameter raises an error
- """
+ """Verify that using pytest.skip() with unknown parameter raises an error."""
testdir.makepyfile(
"""
import pytest
-"""
-terminal reporting of the full testing process.
-"""
+"""Terminal reporting of the full testing process."""
import collections
import os
import sys
from _pytest._io.wcwidth import wcswidth
from _pytest.config import Config
from _pytest.config import ExitCode
+from _pytest.pathlib import Path
from _pytest.pytester import Testdir
from _pytest.reports import BaseReport
from _pytest.reports import CollectReport
)
def test_collectonly_missing_path(self, testdir):
- """this checks issue 115,
- failure in parseargs will cause session
- not to have the items attribute
- """
+ """Issue 115: failure in parseargs will cause session not to
+ have the items attribute."""
result = testdir.runpytest("--collect-only", "uhm_missing_path")
assert result.ret == 4
- result.stderr.fnmatch_lines(["*ERROR: file not found*"])
+ result.stderr.fnmatch_lines(
+ ["*ERROR: file or directory not found: uhm_missing_path"]
+ )
def test_collectonly_quiet(self, testdir):
testdir.makepyfile("def test_foo(): pass")
)
def test_setup_teardown_output_and_test_failure(self, testdir):
- """ Test for issue #442 """
+ """Test for issue #442."""
testdir.makepyfile(
"""
def setup_function(function):
@pytest.mark.parametrize("verbose", [True, False])
def test_color_yes_collection_on_non_atty(testdir, verbose):
- """skip collect progress report when working on non-terminals.
- #1397
- """
+ """#1397: Skip collect progress report when working on non-terminals."""
testdir.makepyfile(
"""
import pytest
class TestGenericReporting:
- """ this test class can be subclassed with a different option
- provider to run e.g. distributed tests.
- """
+ """Test class which can be subclassed with a different option provider to
+ run e.g. distributed tests."""
def test_collect_fail(self, testdir, option):
testdir.makepyfile("import xyz\n")
# dict value, not the actual contents, so tuples of anything
# suffice
# Important statuses -- the highest priority of these always wins
- ("red", [("1 failed", {"bold": True, "red": True})], {"failed": (1,)}),
+ ("red", [("1 failed", {"bold": True, "red": True})], {"failed": [1]}),
(
"red",
[
("1 failed", {"bold": True, "red": True}),
("1 passed", {"bold": False, "green": True}),
],
- {"failed": (1,), "passed": (1,)},
+ {"failed": [1], "passed": [1]},
),
- ("red", [("1 error", {"bold": True, "red": True})], {"error": (1,)}),
- ("red", [("2 errors", {"bold": True, "red": True})], {"error": (1, 2)}),
+ ("red", [("1 error", {"bold": True, "red": True})], {"error": [1]}),
+ ("red", [("2 errors", {"bold": True, "red": True})], {"error": [1, 2]}),
(
"red",
[
("1 passed", {"bold": False, "green": True}),
("1 error", {"bold": True, "red": True}),
],
- {"error": (1,), "passed": (1,)},
+ {"error": [1], "passed": [1]},
),
# (a status that's not known to the code)
- ("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": (1,)}),
+ ("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": [1]}),
(
"yellow",
[
("1 passed", {"bold": False, "green": True}),
("1 weird", {"bold": True, "yellow": True}),
],
- {"weird": (1,), "passed": (1,)},
+ {"weird": [1], "passed": [1]},
),
- ("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": (1,)}),
+ ("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": [1]}),
(
"yellow",
[
("1 passed", {"bold": False, "green": True}),
("1 warning", {"bold": True, "yellow": True}),
],
- {"warnings": (1,), "passed": (1,)},
+ {"warnings": [1], "passed": [1]},
),
(
"green",
[("5 passed", {"bold": True, "green": True})],
- {"passed": (1, 2, 3, 4, 5)},
+ {"passed": [1, 2, 3, 4, 5]},
),
# "Boring" statuses. These have no effect on the color of the summary
# line. Thus, if *every* test has a boring status, the summary line stays
# at its default color, i.e. yellow, to warn the user that the test run
# produced no useful information
- ("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": (1,)}),
+ ("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": [1]}),
(
"green",
[
("1 passed", {"bold": True, "green": True}),
("1 skipped", {"bold": False, "yellow": True}),
],
- {"skipped": (1,), "passed": (1,)},
+ {"skipped": [1], "passed": [1]},
),
(
"yellow",
[("1 deselected", {"bold": True, "yellow": True})],
- {"deselected": (1,)},
+ {"deselected": [1]},
),
(
"green",
("1 passed", {"bold": True, "green": True}),
("1 deselected", {"bold": False, "yellow": True}),
],
- {"deselected": (1,), "passed": (1,)},
+ {"deselected": [1], "passed": [1]},
),
- ("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": (1,)}),
+ ("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": [1]}),
(
"green",
[
("1 passed", {"bold": True, "green": True}),
("1 xfailed", {"bold": False, "yellow": True}),
],
- {"xfailed": (1,), "passed": (1,)},
+ {"xfailed": [1], "passed": [1]},
),
- ("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": (1,)}),
+ ("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": [1]}),
(
"yellow",
[
("1 passed", {"bold": False, "green": True}),
("1 xpassed", {"bold": True, "yellow": True}),
],
- {"xpassed": (1,), "passed": (1,)},
+ {"xpassed": [1], "passed": [1]},
),
# Likewise if no tests were found at all
("yellow", [("no tests ran", {"yellow": True})], {}),
# Test the empty-key special case
- ("yellow", [("no tests ran", {"yellow": True})], {"": (1,)}),
+ ("yellow", [("no tests ran", {"yellow": True})], {"": [1]}),
(
"green",
[("1 passed", {"bold": True, "green": True})],
- {"": (1,), "passed": (1,)},
+ {"": [1], "passed": [1]},
),
# A couple more complex combinations
(
("2 passed", {"bold": False, "green": True}),
("3 xfailed", {"bold": False, "yellow": True}),
],
- {"passed": (1, 2), "failed": (1,), "xfailed": (1, 2, 3)},
+ {"passed": [1, 2], "failed": [1], "xfailed": [1, 2, 3]},
),
(
"green",
("2 xfailed", {"bold": False, "yellow": True}),
],
{
- "passed": (1,),
- "skipped": (1, 2),
- "deselected": (1, 2, 3),
- "xfailed": (1, 2),
+ "passed": [1],
+ "skipped": [1, 2],
+ "deselected": [1, 2, 3],
+ "xfailed": [1, 2],
},
),
],
tr: TerminalReporter,
exp_line: List[Tuple[str, Dict[str, bool]]],
exp_color: str,
- stats_arg: Dict[str, List],
+ stats_arg: Dict[str, List[object]],
) -> None:
tr.stats = stats_arg
ev3.longrepr = longrepr
ev3.skipped = True
- values = _folded_skips(py.path.local(), [ev1, ev2, ev3])
+ values = _folded_skips(Path.cwd(), [ev1, ev2, ev3])
assert len(values) == 1
num, fspath, lineno_, reason = values[0]
assert num == 3
import stat
import sys
from typing import Callable
+from typing import cast
from typing import List
import attr
import pytest
from _pytest import pathlib
+from _pytest.config import Config
from _pytest.pathlib import cleanup_numbered_dir
from _pytest.pathlib import create_cleanup_lock
from _pytest.pathlib import make_numbered_dir
class TestTempdirHandler:
def test_mktemp(self, tmp_path):
- config = FakeConfig(tmp_path)
+ config = cast(Config, FakeConfig(tmp_path))
t = TempdirFactory(TempPathFactory.from_config(config))
tmp = t.mktemp("world")
assert tmp.relto(t.getbasetemp()) == "world0"
def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch):
"""#4425"""
monkeypatch.chdir(tmp_path)
- config = FakeConfig("hello")
+ config = cast(Config, FakeConfig("hello"))
t = TempPathFactory.from_config(config)
assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve()
import gc
+import sys
from typing import List
import pytest
@pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"])
def test_pdb_teardown_skipped(testdir, monkeypatch, mark: str) -> None:
- """
- With --pdb, setUp and tearDown should not be called for skipped tests.
- """
+ """With --pdb, setUp and tearDown should not be called for skipped tests."""
tracked = [] # type: List[str]
monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False)
testdir.copy_example("unittest/test_unittest_asynctest.py")
reprec = testdir.inline_run()
reprec.assertoutcome(failed=1, passed=2)
+
+
+def test_plain_unittest_does_not_support_async(testdir):
+ """Async functions in plain unittest.TestCase subclasses are not supported without plugins.
+
+ This test exists here to avoid introducing this support by accident, leading users
+ to expect that it works, rather than doing so intentionally as a feature.
+
+ See https://github.com/pytest-dev/pytest-asyncio/issues/180 for more context.
+ """
+ testdir.copy_example("unittest/test_unittest_plain_async.py")
+ result = testdir.runpytest_subprocess()
+ if hasattr(sys, "pypy_version_info"):
+ # in PyPy we can't reliable get the warning about the coroutine not being awaited,
+ # because it depends on the coroutine being garbage collected; given that
+ # we are running in a subprocess, that's difficult to enforce
+ expected_lines = ["*1 passed*"]
+ else:
+ expected_lines = [
+ "*RuntimeWarning: coroutine * was never awaited",
+ "*1 passed*",
+ ]
+ result.stdout.fnmatch_lines(expected_lines)
@pytest.fixture
def pyfile_with_warnings(testdir: Testdir, request: FixtureRequest) -> str:
- """
- Create a test file which calls a function in a module which generates warnings.
- """
+ """Create a test file which calls a function in a module which generates warnings."""
testdir.syspathinsert()
test_name = request.function.__name__
module_name = test_name.lstrip("test_") + "_module"
@pytest.mark.filterwarnings("default")
def test_normal_flow(testdir, pyfile_with_warnings):
- """
- Check that the warnings section is displayed.
- """
+ """Check that the warnings section is displayed."""
result = testdir.runpytest(pyfile_with_warnings)
result.stdout.fnmatch_lines(
[
@pytest.mark.parametrize("default_config", ["ini", "cmdline"])
def test_filterwarnings_mark(testdir, default_config):
- """
- Test ``filterwarnings`` mark works and takes precedence over command line and ini options.
- """
+ """Test ``filterwarnings`` mark works and takes precedence over command
+ line and ini options."""
if default_config == "ini":
testdir.makeini(
"""
def test_warning_captured_hook(testdir):
testdir.makeconftest(
"""
- from _pytest.warnings import _issue_warning_captured
def pytest_configure(config):
- _issue_warning_captured(UserWarning("config warning"), config.hook, stacklevel=2)
+ config.issue_config_time_warning(UserWarning("config warning"), stacklevel=2)
"""
)
testdir.makepyfile(
@pytest.mark.filterwarnings("always")
def test_collection_warnings(testdir):
- """
- Check that we also capture warnings issued during test collection (#3251).
- """
+ """Check that we also capture warnings issued during test collection (#3251)."""
testdir.makepyfile(
"""
import warnings
@pytest.mark.parametrize("ignore_on_cmdline", [True, False])
def test_option_precedence_cmdline_over_ini(testdir, ignore_on_cmdline):
- """filters defined in the command-line should take precedence over filters in ini files (#3946)."""
+ """Filters defined in the command-line should take precedence over filters in ini files (#3946)."""
testdir.makeini(
"""
[pytest]
@pytest.mark.parametrize("change_default", [None, "ini", "cmdline"])
+@pytest.mark.skip(
+ reason="This test should be enabled again before pytest 7.0 is released"
+)
def test_deprecation_warning_as_error(testdir, change_default):
"""This ensures that PytestDeprecationWarnings raised by pytest are turned into errors.
assert "config{sep}__init__.py".format(sep=os.sep) in file
assert func == "_preparse"
+ @pytest.mark.filterwarnings("default")
+ def test_conftest_warning_captured(self, testdir: Testdir) -> None:
+ """Warnings raised during importing of conftest.py files is captured (#2891)."""
+ testdir.makeconftest(
+ """
+ import warnings
+ warnings.warn(UserWarning("my custom warning"))
+ """
+ )
+ result = testdir.runpytest()
+ result.stdout.fnmatch_lines(
+ ["conftest.py:2", "*UserWarning: my custom warning*"]
+ )
+
def test_issue4445_import_plugin(self, testdir, capwarn):
- """#4445: Make sure the warning points to a reasonable location
- See origin of _issue_warning_captured at: _pytest.config.__init__.py:585
- """
+ """#4445: Make sure the warning points to a reasonable location"""
testdir.makepyfile(
some_plugin="""
import pytest
assert "skipped plugin 'some_plugin': thing" in str(warning.message)
assert "config{sep}__init__.py".format(sep=os.sep) in file
- assert func == "import_plugin"
-
- def test_issue4445_resultlog(self, testdir, capwarn):
- """#4445: Make sure the warning points to a reasonable location
- See origin of _issue_warning_captured at: _pytest.resultlog.py:35
- """
- testdir.makepyfile(
- """
- def test_dummy():
- pass
- """
- )
- # Use parseconfigure() because the warning in resultlog.py is triggered in
- # the pytest_configure hook
- testdir.parseconfigure(
- "--result-log={dir}".format(dir=testdir.tmpdir.join("result.log"))
- )
-
- # with stacklevel=2 the warning originates from resultlog.pytest_configure
- # and is thrown when --result-log is used
- warning, location = capwarn.captured.pop()
- file, _, func = location
-
- assert "--result-log is deprecated" in str(warning.message)
- assert "resultlog.py" in file
- assert func == "pytest_configure"
+ assert func == "_warn_about_skipped_plugins"
def test_issue4445_issue5928_mark_generator(self, testdir):
"""#4445 and #5928: Make sure the warning from an unknown mark points to
pypy3
py37-{pexpect,xdist,unittestextras,numpy,pluggymaster}
doctesting
+ plugins
py37-freeze
docs
docs-checklinks
rm -rf {envdir}/.pytest_cache
make regen
+[testenv:plugins]
+# use latest versions of all plugins, including pre-releases
+pip_pre=true
+# use latest pip and new dependency resolver (#7783)
+download=true
+install_command=python -m pip --use-feature=2020-resolver install {opts} {packages}
+changedir = testing/plugins_integration
+deps =
+ anyio[curio,trio]
+ django
+ pytest-asyncio
+ pytest-bdd
+ pytest-cov
+ pytest-django
+ pytest-flakes
+ pytest-html
+ pytest-mock
+ pytest-sugar
+ pytest-trio
+ pytest-twisted
+ twisted
+ pytest-xvfb
+setenv =
+ PYTHONPATH=.
+commands =
+ pip check
+ pytest bdd_wallet.py
+ pytest --cov=. simple_integration.py
+ pytest --ds=django_settings simple_integration.py
+ pytest --html=simple.html simple_integration.py
+ pytest pytest_anyio_integration.py
+ pytest pytest_asyncio_integration.py
+ pytest pytest_mock_integration.py
+ pytest pytest_trio_integration.py
+ pytest pytest_twisted_integration.py
+ pytest simple_integration.py --force-sugar --flakes
+
[testenv:py37-freeze]
changedir = testing/freeze
deps =
[flake8]
max-line-length = 120
-extend-ignore = E203
+extend-ignore =
+ ; whitespace before ':'
+ E203
+ ; Missing Docstrings
+ D100,D101,D102,D103,D104,D105,D106,D107
+ ; Whitespace Issues
+ D202,D203,D204,D205,D209,D213
+ ; Quotes Issues
+ D302
+ ; Docstring Content Issues
+ D400,D401,D401,D402,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D415,D416,D417
[isort]
; This config mimics what reorder-python-imports does.