Merge branch 'main' into report_xfails
This commit is contained in:
commit
c96f27d165
|
@ -26,7 +26,7 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v1.5.3
|
uses: hynek/build-and-inspect-python-package@v1.5.4
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: github.repository == 'pytest-dev/pytest'
|
if: github.repository == 'pytest-dev/pytest'
|
||||||
|
@ -47,7 +47,7 @@ jobs:
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Publish package to PyPI
|
- name: Publish package to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
uses: pypa/gh-action-pypi-publish@v1.8.11
|
||||||
|
|
||||||
- name: Push tag
|
- name: Push tag
|
||||||
run: |
|
run: |
|
||||||
|
@ -73,7 +73,7 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.8"
|
python-version: "3.8"
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
debug-only: false
|
debug-only: false
|
||||||
days-before-issue-stale: 14
|
days-before-issue-stale: 14
|
||||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v1.5.3
|
uses: hynek/build-and-inspect-python-package@v1.5.4
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [package]
|
needs: [package]
|
||||||
|
@ -156,7 +156,7 @@ jobs:
|
||||||
tox_env: "py312-xdist"
|
tox_env: "py312-xdist"
|
||||||
|
|
||||||
- name: "plugins"
|
- name: "plugins"
|
||||||
python: "3.9"
|
python: "3.12"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "plugins"
|
tox_env: "plugins"
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ jobs:
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python }}
|
- name: Set up Python ${{ matrix.python }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||||
|
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.10.1
|
rev: 23.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet]
|
||||||
|
@ -56,7 +56,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-use-type-annotations
|
- id: python-use-type-annotations
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.6.1
|
rev: v1.7.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|
5
AUTHORS
5
AUTHORS
|
@ -48,6 +48,7 @@ Ariel Pillemer
|
||||||
Armin Rigo
|
Armin Rigo
|
||||||
Aron Coyle
|
Aron Coyle
|
||||||
Aron Curzon
|
Aron Curzon
|
||||||
|
Arthur Richard
|
||||||
Ashish Kurmi
|
Ashish Kurmi
|
||||||
Aviral Verma
|
Aviral Verma
|
||||||
Aviv Palivoda
|
Aviv Palivoda
|
||||||
|
@ -188,6 +189,7 @@ Javier Romero
|
||||||
Jeff Rackauckas
|
Jeff Rackauckas
|
||||||
Jeff Widman
|
Jeff Widman
|
||||||
Jenni Rinker
|
Jenni Rinker
|
||||||
|
Jens Tröger
|
||||||
John Eddie Ayson
|
John Eddie Ayson
|
||||||
John Litborn
|
John Litborn
|
||||||
John Towler
|
John Towler
|
||||||
|
@ -275,6 +277,7 @@ Miro Hrončok
|
||||||
Nathaniel Compton
|
Nathaniel Compton
|
||||||
Nathaniel Waisbrot
|
Nathaniel Waisbrot
|
||||||
Ned Batchelder
|
Ned Batchelder
|
||||||
|
Neil Martin
|
||||||
Neven Mundar
|
Neven Mundar
|
||||||
Nicholas Devenish
|
Nicholas Devenish
|
||||||
Nicholas Murphy
|
Nicholas Murphy
|
||||||
|
@ -292,6 +295,7 @@ Ondřej Súkup
|
||||||
Oscar Benjamin
|
Oscar Benjamin
|
||||||
Parth Patel
|
Parth Patel
|
||||||
Patrick Hayes
|
Patrick Hayes
|
||||||
|
Patrick Lannigan
|
||||||
Paul Müller
|
Paul Müller
|
||||||
Paul Reece
|
Paul Reece
|
||||||
Pauli Virtanen
|
Pauli Virtanen
|
||||||
|
@ -338,6 +342,7 @@ Saiprasad Kale
|
||||||
Samuel Colvin
|
Samuel Colvin
|
||||||
Samuel Dion-Girardeau
|
Samuel Dion-Girardeau
|
||||||
Samuel Searles-Bryant
|
Samuel Searles-Bryant
|
||||||
|
Samuel Therrien (Avasam)
|
||||||
Samuele Pedroni
|
Samuele Pedroni
|
||||||
Sanket Duthade
|
Sanket Duthade
|
||||||
Sankt Petersbug
|
Sankt Petersbug
|
||||||
|
|
|
@ -199,7 +199,7 @@ Short version
|
||||||
#. Fork the repository.
|
#. Fork the repository.
|
||||||
#. Fetch tags from upstream if necessary (if you cloned only main `git fetch --tags https://github.com/pytest-dev/pytest`).
|
#. Fetch tags from upstream if necessary (if you cloned only main `git fetch --tags https://github.com/pytest-dev/pytest`).
|
||||||
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
||||||
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
#. Follow `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_ for naming.
|
||||||
#. Tests are run using ``tox``::
|
#. Tests are run using ``tox``::
|
||||||
|
|
||||||
tox -e linting,py39
|
tox -e linting,py39
|
||||||
|
@ -282,7 +282,7 @@ Here is a simple overview, with pytest-specific bits:
|
||||||
This command will run tests via the "tox" tool against Python 3.9
|
This command will run tests via the "tox" tool against Python 3.9
|
||||||
and also perform "lint" coding-style checks.
|
and also perform "lint" coding-style checks.
|
||||||
|
|
||||||
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming.
|
#. You can now edit your local working copy and run the tests again as necessary. Please follow `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_ for naming.
|
||||||
|
|
||||||
You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
|
You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
|
||||||
(e.g. enter pdb on failure) to pytest you can do::
|
(e.g. enter pdb on failure) to pytest you can do::
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
:target: https://codecov.io/gh/pytest-dev/pytest
|
:target: https://codecov.io/gh/pytest-dev/pytest
|
||||||
:alt: Code coverage Status
|
:alt: Code coverage Status
|
||||||
|
|
||||||
.. image:: https://github.com/pytest-dev/pytest/workflows/test/badge.svg
|
.. image:: https://github.com/pytest-dev/pytest/actions/workflows/test.yml/badge.svg
|
||||||
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
|
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
|
||||||
|
|
||||||
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg
|
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
Sanitized the handling of the ``default`` parameter when defining configuration options.
|
||||||
|
|
||||||
|
Previously if ``default`` was not supplied for :meth:`parser.addini <pytest.Parser.addini>` and the configuration option value was not defined in a test session, then calls to :func:`config.getini <pytest.Config.getini>` returned an *empty list* or an *empty string* depending on whether ``type`` was supplied or not respectively, which is clearly incorrect. Also, ``None`` was not honored even if ``default=None`` was used explicitly while defining the option.
|
||||||
|
|
||||||
|
Now the behavior of :meth:`parser.addini <pytest.Parser.addini>` is as follows:
|
||||||
|
|
||||||
|
* If ``default`` is NOT passed but ``type`` is provided, then a type-specific default will be returned. For example ``type=bool`` will return ``False``, ``type=str`` will return ``""``, etc.
|
||||||
|
* If ``default=None`` is passed and the option is not defined in a test session, then ``None`` will be returned, regardless of the ``type``.
|
||||||
|
* If neither ``default`` nor ``type`` are provided, assume ``type=str`` and return ``""`` as default (this is as per previous behavior).
|
||||||
|
|
||||||
|
The team decided to not introduce a deprecation period for this change, as doing so would be complicated both in terms of communicating this to the community as well as implementing it, and also because the team believes this change should not break existing plugins except in rare cases.
|
|
@ -0,0 +1,5 @@
|
||||||
|
Added the new :confval:`verbosity_assertions` configuration option for fine-grained control of failed assertions verbosity.
|
||||||
|
|
||||||
|
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
|
||||||
|
|
||||||
|
For plugin authors, :attr:`config.get_verbosity <pytest.Config.get_verbosity>` can be used to retrieve the verbosity level for a specific verbosity type.
|
|
@ -1 +1,5 @@
|
||||||
Improved very verbose diff output to color it as a diff instead of only red.
|
Improved very verbose diff output to color it as a diff instead of only red.
|
||||||
|
|
||||||
|
Improved the error reporting to better separate each section.
|
||||||
|
|
||||||
|
Improved the error reporting to syntax-highlight Python code when Pygments is available.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Handle an edge case where :data:`sys.stderr` and :data:`sys.__stderr__` might already be closed when :ref:`faulthandler` is tearing down.
|
|
@ -0,0 +1 @@
|
||||||
|
Improved the documentation and type signature for :func:`pytest.mark.xfail <pytest.mark.xfail>`'s ``condition`` param to use ``False`` as the default value.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Added :func:`LogCaptureFixture.filtering() <pytest.LogCaptureFixture.filtering>` context manager that
|
||||||
|
adds a given :class:`logging.Filter` object to the caplog fixture.
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed the selftests to pass correctly if ``FORCE_COLOR``, ``NO_COLOR`` or ``PY_COLORS`` is set in the calling environment.
|
|
@ -0,0 +1,3 @@
|
||||||
|
pytest's ``setup.py`` file is removed.
|
||||||
|
If you relied on this file, e.g. to install pytest using ``setup.py install``,
|
||||||
|
please see `Why you shouldn't invoke setup.py directly <https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html#summary>`_ for alternatives.
|
|
@ -0,0 +1,3 @@
|
||||||
|
The classes :class:`~_pytest.nodes.Node`, :class:`~pytest.Collector`, :class:`~pytest.Item`, :class:`~pytest.File`, :class:`~_pytest.nodes.FSCollector` are now marked abstract (see :mod:`abc`).
|
||||||
|
|
||||||
|
We do not expect this change to affect users and plugin authors, it will only cause errors when the code is already wrong or problematic.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Improved the very verbose diff for every standard library container types: the indentation is now consistent and the markers are on their own separate lines, which should reduce the diffs shown to users.
|
||||||
|
|
||||||
|
Previously, the default python pretty printer was used to generate the output, which puts opening and closing
|
||||||
|
markers on the same line as the first/last entry, in addition to not having consistent indentation.
|
|
@ -1,4 +1,4 @@
|
||||||
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
|
:func:`~pytest.warns` now re-emits unmatched warnings when the context
|
||||||
closes -- previously it would consume all warnings, hiding those that were not
|
closes -- previously it would consume all warnings, hiding those that were not
|
||||||
matched by the function.
|
matched by the function.
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ b) transitional: the old and new API don't conflict
|
||||||
|
|
||||||
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
||||||
|
|
||||||
A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationwarning`).
|
A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationWarning`).
|
||||||
|
|
||||||
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
||||||
|
|
||||||
|
|
|
@ -413,7 +413,7 @@ Improvements
|
||||||
|
|
||||||
|
|
||||||
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
||||||
enhance match comparison for :py:func:`_pytest._code.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
|
enhance match comparison for :py:func:`pytest.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
|
||||||
|
|
||||||
|
|
||||||
- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
|
- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
|
||||||
|
@ -422,7 +422,7 @@ Improvements
|
||||||
|
|
||||||
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
||||||
|
|
||||||
:mod:`tomli` is no longer a dependency on Python 3.11.
|
`tomli` is no longer a dependency on Python 3.11.
|
||||||
|
|
||||||
|
|
||||||
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
|
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
|
||||||
|
@ -457,7 +457,7 @@ Bug Fixes
|
||||||
|
|
||||||
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
|
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
|
||||||
|
|
||||||
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`pytest.Node.iter_markers` instead.
|
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` instead.
|
||||||
|
|
||||||
|
|
||||||
- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
|
- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
|
||||||
|
@ -710,7 +710,7 @@ Bug Fixes
|
||||||
- `#9355 <https://github.com/pytest-dev/pytest/issues/9355>`_: Fixed error message prints function decorators when using assert in Python 3.8 and above.
|
- `#9355 <https://github.com/pytest-dev/pytest/issues/9355>`_: Fixed error message prints function decorators when using assert in Python 3.8 and above.
|
||||||
|
|
||||||
|
|
||||||
- `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure :attr:`pytest.Config.inifile` is available during the :func:`pytest_cmdline_main <_pytest.hookspec.pytest_cmdline_main>` hook (regression during ``7.0.0rc1``).
|
- `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure `pytest.Config.inifile` is available during the :hook:`pytest_cmdline_main` hook (regression during ``7.0.0rc1``).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -855,7 +855,7 @@ Deprecations
|
||||||
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
||||||
|
|
||||||
|
|
||||||
- `#8447 <https://github.com/pytest-dev/pytest/issues/8447>`_: Defining a custom pytest node type which is both an :class:`pytest.Item <Item>` and a :class:`pytest.Collector <Collector>` (e.g. :class:`pytest.File <File>`) now issues a warning.
|
- `#8447 <https://github.com/pytest-dev/pytest/issues/8447>`_: Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
|
||||||
It was never sanely supported and triggers hard to debug errors.
|
It was never sanely supported and triggers hard to debug errors.
|
||||||
|
|
||||||
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
|
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
|
||||||
|
@ -897,7 +897,7 @@ Features
|
||||||
- `#7132 <https://github.com/pytest-dev/pytest/issues/7132>`_: Added two environment variables :envvar:`PYTEST_THEME` and :envvar:`PYTEST_THEME_MODE` to let the users customize the pygments theme used.
|
- `#7132 <https://github.com/pytest-dev/pytest/issues/7132>`_: Added two environment variables :envvar:`PYTEST_THEME` and :envvar:`PYTEST_THEME_MODE` to let the users customize the pygments theme used.
|
||||||
|
|
||||||
|
|
||||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: Added :meth:`cache.mkdir() <pytest.Cache.mkdir>`, which is similar to the existing :meth:`cache.makedir() <pytest.Cache.makedir>`,
|
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: Added :meth:`cache.mkdir() <pytest.Cache.mkdir>`, which is similar to the existing ``cache.makedir()``,
|
||||||
but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``.
|
but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``.
|
||||||
|
|
||||||
Added a ``paths`` type to :meth:`parser.addini() <pytest.Parser.addini>`,
|
Added a ``paths`` type to :meth:`parser.addini() <pytest.Parser.addini>`,
|
||||||
|
@ -923,7 +923,7 @@ Features
|
||||||
- ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`.
|
- ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`.
|
||||||
- ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`.
|
- ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`.
|
||||||
- ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`.
|
- ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`.
|
||||||
- ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.RunResult>` type used in :class:`~pytest.RunResult` and others.
|
- ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.LineMatcher>` type used in :class:`~pytest.RunResult` and others.
|
||||||
- ``pytest.TestReport`` for the :class:`TestReport <pytest.TestReport>` type used in various hooks.
|
- ``pytest.TestReport`` for the :class:`TestReport <pytest.TestReport>` type used in various hooks.
|
||||||
- ``pytest.CollectReport`` for the :class:`CollectReport <pytest.CollectReport>` type used in various hooks.
|
- ``pytest.CollectReport`` for the :class:`CollectReport <pytest.CollectReport>` type used in various hooks.
|
||||||
|
|
||||||
|
@ -956,7 +956,7 @@ Features
|
||||||
|
|
||||||
|
|
||||||
- `#8251 <https://github.com/pytest-dev/pytest/issues/8251>`_: Implement ``Node.path`` as a ``pathlib.Path``. Both the old ``fspath`` and this new attribute gets set no matter whether ``path`` or ``fspath`` (deprecated) is passed to the constructor. It is a replacement for the ``fspath`` attribute (which represents the same path as ``py.path.local``). While ``fspath`` is not deprecated yet
|
- `#8251 <https://github.com/pytest-dev/pytest/issues/8251>`_: Implement ``Node.path`` as a ``pathlib.Path``. Both the old ``fspath`` and this new attribute gets set no matter whether ``path`` or ``fspath`` (deprecated) is passed to the constructor. It is a replacement for the ``fspath`` attribute (which represents the same path as ``py.path.local``). While ``fspath`` is not deprecated yet
|
||||||
due to the ongoing migration of methods like :meth:`~_pytest.Item.reportinfo`, we expect to deprecate it in a future release.
|
due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`, we expect to deprecate it in a future release.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
||||||
|
@ -988,7 +988,7 @@ Features
|
||||||
See :ref:`plugin-stash` for details.
|
See :ref:`plugin-stash` for details.
|
||||||
|
|
||||||
|
|
||||||
- `#8953 <https://github.com/pytest-dev/pytest/issues/8953>`_: :class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a
|
- `#8953 <https://github.com/pytest-dev/pytest/issues/8953>`_: :class:`~pytest.RunResult` method :meth:`~pytest.RunResult.assert_outcomes` now accepts a
|
||||||
``warnings`` argument to assert the total number of warnings captured.
|
``warnings`` argument to assert the total number of warnings captured.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1000,7 +1000,7 @@ Features
|
||||||
used.
|
used.
|
||||||
|
|
||||||
|
|
||||||
- `#9113 <https://github.com/pytest-dev/pytest/issues/9113>`_: :class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a
|
- `#9113 <https://github.com/pytest-dev/pytest/issues/9113>`_: :class:`~pytest.RunResult` method :meth:`~pytest.RunResult.assert_outcomes` now accepts a
|
||||||
``deselected`` argument to assert the total number of deselected tests.
|
``deselected`` argument to assert the total number of deselected tests.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1013,7 +1013,7 @@ Improvements
|
||||||
|
|
||||||
- `#7480 <https://github.com/pytest-dev/pytest/issues/7480>`_: A deprecation scheduled to be removed in a major version X (e.g. pytest 7, 8, 9, ...) now uses warning category `PytestRemovedInXWarning`,
|
- `#7480 <https://github.com/pytest-dev/pytest/issues/7480>`_: A deprecation scheduled to be removed in a major version X (e.g. pytest 7, 8, 9, ...) now uses warning category `PytestRemovedInXWarning`,
|
||||||
a subclass of :class:`~pytest.PytestDeprecationWarning`,
|
a subclass of :class:`~pytest.PytestDeprecationWarning`,
|
||||||
instead of :class:`PytestDeprecationWarning` directly.
|
instead of :class:`~pytest.PytestDeprecationWarning` directly.
|
||||||
|
|
||||||
See :ref:`backwards-compatibility` for more details.
|
See :ref:`backwards-compatibility` for more details.
|
||||||
|
|
||||||
|
@ -1052,7 +1052,7 @@ Improvements
|
||||||
|
|
||||||
- `#8803 <https://github.com/pytest-dev/pytest/issues/8803>`_: It is now possible to add colors to custom log levels on cli log.
|
- `#8803 <https://github.com/pytest-dev/pytest/issues/8803>`_: It is now possible to add colors to custom log levels on cli log.
|
||||||
|
|
||||||
By using :func:`add_color_level <_pytest.logging.add_color_level>` from a ``pytest_configure`` hook, colors can be added::
|
By using ``add_color_level`` from a :hook:`pytest_configure` hook, colors can be added::
|
||||||
|
|
||||||
logging_plugin = config.pluginmanager.get_plugin('logging-plugin')
|
logging_plugin = config.pluginmanager.get_plugin('logging-plugin')
|
||||||
logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan')
|
logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan')
|
||||||
|
@ -1117,7 +1117,7 @@ Bug Fixes
|
||||||
|
|
||||||
- `#8503 <https://github.com/pytest-dev/pytest/issues/8503>`_: :meth:`pytest.MonkeyPatch.syspath_prepend` no longer fails when
|
- `#8503 <https://github.com/pytest-dev/pytest/issues/8503>`_: :meth:`pytest.MonkeyPatch.syspath_prepend` no longer fails when
|
||||||
``setuptools`` is not installed.
|
``setuptools`` is not installed.
|
||||||
It now only calls :func:`pkg_resources.fixup_namespace_packages` if
|
It now only calls ``pkg_resources.fixup_namespace_packages`` if
|
||||||
``pkg_resources`` was previously imported, because it is not needed otherwise.
|
``pkg_resources`` was previously imported, because it is not needed otherwise.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1344,7 +1344,7 @@ Features
|
||||||
|
|
||||||
This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future.
|
This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future.
|
||||||
|
|
||||||
Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface.
|
Internally, the old ``pytest.Testdir`` is now a thin wrapper around :class:`~pytest.Pytester`, preserving the old interface.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7695`: A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
|
- :issue:`7695`: A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
|
||||||
|
@ -1382,7 +1382,7 @@ Features
|
||||||
Improvements
|
Improvements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- :issue:`1265`: Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method.
|
- :issue:`1265`: Added an ``__str__`` implementation to the :class:`~pytest.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`2044`: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS".
|
- :issue:`2044`: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS".
|
||||||
|
@ -1446,7 +1446,7 @@ Bug Fixes
|
||||||
- :issue:`7911`: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
|
- :issue:`7911`: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7913`: Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved.
|
- :issue:`7913`: Fixed a crash or hang in :meth:`pytester.spawn <pytest.Pytester.spawn>` when the :mod:`readline` module is involved.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7951`: Fixed handling of recursive symlinks when collecting tests.
|
- :issue:`7951`: Fixed handling of recursive symlinks when collecting tests.
|
||||||
|
@ -1563,7 +1563,7 @@ Deprecations
|
||||||
if you use this and want a replacement.
|
if you use this and want a replacement.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7255`: The :hook:`pytest_warning_captured` hook is deprecated in favor
|
- :issue:`7255`: The ``pytest_warning_captured`` hook is deprecated in favor
|
||||||
of :hook:`pytest_warning_recorded`, and will be removed in a future version.
|
of :hook:`pytest_warning_recorded`, and will be removed in a future version.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1591,8 +1591,8 @@ Improvements
|
||||||
- :issue:`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.
|
- :issue:`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.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7685`: Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`.
|
- :issue:`7685`: Added two new attributes :attr:`rootpath <pytest.Config.rootpath>` and :attr:`inipath <pytest.Config.inipath>` to :class:`~pytest.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,
|
These attributes are :class:`pathlib.Path` versions of the existing ``rootdir`` and ``inifile`` attributes,
|
||||||
and should be preferred over them when possible.
|
and should be preferred over them when possible.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1663,7 +1663,7 @@ Trivial/Internal Changes
|
||||||
- :issue:`7587`: The dependency on the ``more-itertools`` package has been removed.
|
- :issue:`7587`: The dependency on the ``more-itertools`` package has been removed.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7631`: The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple,
|
- :issue:`7631`: The result type of :meth:`capfd.readouterr() <pytest.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple,
|
||||||
but should behave like one in all respects. This was done for technical reasons.
|
but should behave like one in all respects. This was done for technical reasons.
|
||||||
|
|
||||||
|
|
||||||
|
@ -2041,10 +2041,10 @@ Improvements
|
||||||
- :issue:`7128`: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. This is more consistent with how other tools show `--version`.
|
- :issue:`7128`: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. This is more consistent with how other tools show `--version`.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7133`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file.
|
- :issue:`7133`: :meth:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7159`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <_pytest.logging.LogCaptureFixture.at_level>` no longer affect
|
- :issue:`7159`: :meth:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <pytest.LogCaptureFixture.at_level>` no longer affect
|
||||||
the level of logs that are shown in the *Captured log report* report section.
|
the level of logs that are shown in the *Captured log report* report section.
|
||||||
|
|
||||||
|
|
||||||
|
@ -2139,7 +2139,7 @@ Bug Fixes
|
||||||
parameter when Python is called with the ``-bb`` flag.
|
parameter when Python is called with the ``-bb`` flag.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7143`: Fix :meth:`pytest.File.from_parent` so it forwards extra keyword arguments to the constructor.
|
- :issue:`7143`: Fix :meth:`pytest.File.from_parent <_pytest.nodes.Node.from_parent>` so it forwards extra keyword arguments to the constructor.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
|
- :issue:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
|
||||||
|
@ -2390,7 +2390,7 @@ Improvements
|
||||||
- :issue:`6384`: Make `--showlocals` work also with `--tb=short`.
|
- :issue:`6384`: Make `--showlocals` work also with `--tb=short`.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6653`: Add support for matching lines consecutively with :attr:`LineMatcher <_pytest.pytester.LineMatcher>`'s :func:`~_pytest.pytester.LineMatcher.fnmatch_lines` and :func:`~_pytest.pytester.LineMatcher.re_match_lines`.
|
- :issue:`6653`: Add support for matching lines consecutively with :class:`~pytest.LineMatcher`'s :func:`~pytest.LineMatcher.fnmatch_lines` and :func:`~pytest.LineMatcher.re_match_lines`.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6658`: Code is now highlighted in tracebacks when ``pygments`` is installed.
|
- :issue:`6658`: Code is now highlighted in tracebacks when ``pygments`` is installed.
|
||||||
|
@ -2458,7 +2458,7 @@ Bug Fixes
|
||||||
- :issue:`6597`: Fix node ids which contain a parametrized empty-string variable.
|
- :issue:`6597`: Fix node ids which contain a parametrized empty-string variable.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc.
|
- :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's ``testdir.runpytest`` etc.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger.
|
- :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger.
|
||||||
|
@ -2524,7 +2524,7 @@ Bug Fixes
|
||||||
``multiprocessing`` module.
|
``multiprocessing`` module.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6436`: :class:`FixtureDef <_pytest.fixtures.FixtureDef>` objects now properly register their finalizers with autouse and
|
- :issue:`6436`: :class:`~pytest.FixtureDef` objects now properly register their finalizers with autouse and
|
||||||
parameterized fixtures that execute before them in the fixture stack so they are torn
|
parameterized fixtures that execute before them in the fixture stack so they are torn
|
||||||
down at the right times, and in the right order.
|
down at the right times, and in the right order.
|
||||||
|
|
||||||
|
@ -2580,7 +2580,7 @@ Improvements
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
---------
|
---------
|
||||||
|
|
||||||
- :issue:`5914`: pytester: fix :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` when used after positive matching.
|
- :issue:`5914`: pytester: fix :py:func:`~pytest.LineMatcher.no_fnmatch_line` when used after positive matching.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6082`: Fix line detection for doctest samples inside :py:class:`python:property` docstrings, as a workaround to :bpo:`17446`.
|
- :issue:`6082`: Fix line detection for doctest samples inside :py:class:`python:property` docstrings, as a workaround to :bpo:`17446`.
|
||||||
|
@ -2644,8 +2644,8 @@ Features
|
||||||
rather than implicitly.
|
rather than implicitly.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`5914`: :fixture:`testdir` learned two new functions, :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` and
|
- :issue:`5914`: :fixture:`testdir` learned two new functions, :py:func:`~pytest.LineMatcher.no_fnmatch_line` and
|
||||||
:py:func:`~_pytest.pytester.LineMatcher.no_re_match_line`.
|
:py:func:`~pytest.LineMatcher.no_re_match_line`.
|
||||||
|
|
||||||
The functions are used to ensure the captured text *does not* match the given
|
The functions are used to ensure the captured text *does not* match the given
|
||||||
pattern.
|
pattern.
|
||||||
|
@ -6497,7 +6497,7 @@ Changes
|
||||||
* fix :issue:`2013`: turn RecordedWarning into ``namedtuple``,
|
* fix :issue:`2013`: turn RecordedWarning into ``namedtuple``,
|
||||||
to give it a comprehensible repr while preventing unwarranted modification.
|
to give it a comprehensible repr while preventing unwarranted modification.
|
||||||
|
|
||||||
* fix :issue:`2208`: ensure an iteration limit for _pytest.compat.get_real_func.
|
* fix :issue:`2208`: ensure an iteration limit for ``_pytest.compat.get_real_func``.
|
||||||
Thanks :user:`RonnyPfannschmidt` for the report and PR.
|
Thanks :user:`RonnyPfannschmidt` for the report and PR.
|
||||||
|
|
||||||
* Hooks are now verified after collection is complete, rather than right after loading installed plugins. This
|
* Hooks are now verified after collection is complete, rather than right after loading installed plugins. This
|
||||||
|
|
|
@ -169,6 +169,50 @@ extlinks = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nitpicky = True
|
||||||
|
nitpick_ignore = [
|
||||||
|
# TODO (fix in pluggy?)
|
||||||
|
("py:class", "HookCaller"),
|
||||||
|
("py:class", "HookspecMarker"),
|
||||||
|
("py:exc", "PluginValidationError"),
|
||||||
|
# Might want to expose/TODO (https://github.com/pytest-dev/pytest/issues/7469)
|
||||||
|
("py:class", "ExceptionRepr"),
|
||||||
|
("py:class", "Exit"),
|
||||||
|
("py:class", "SubRequest"),
|
||||||
|
("py:class", "SubRequest"),
|
||||||
|
("py:class", "TerminalReporter"),
|
||||||
|
("py:class", "_pytest._code.code.TerminalRepr"),
|
||||||
|
("py:class", "_pytest.fixtures.FixtureFunctionMarker"),
|
||||||
|
("py:class", "_pytest.logging.LogCaptureHandler"),
|
||||||
|
("py:class", "_pytest.mark.structures.ParameterSet"),
|
||||||
|
# Intentionally undocumented/private
|
||||||
|
("py:class", "_pytest._code.code.Traceback"),
|
||||||
|
("py:class", "_pytest._py.path.LocalPath"),
|
||||||
|
("py:class", "_pytest.capture.CaptureResult"),
|
||||||
|
("py:class", "_pytest.compat.NotSetType"),
|
||||||
|
("py:class", "_pytest.python.PyCollector"),
|
||||||
|
("py:class", "_pytest.python.PyobjMixin"),
|
||||||
|
("py:class", "_pytest.python_api.RaisesContext"),
|
||||||
|
("py:class", "_pytest.recwarn.WarningsChecker"),
|
||||||
|
("py:class", "_pytest.reports.BaseReport"),
|
||||||
|
# Undocumented third parties
|
||||||
|
("py:class", "_tracing.TagTracerSub"),
|
||||||
|
("py:class", "warnings.WarningMessage"),
|
||||||
|
# Undocumented type aliases
|
||||||
|
("py:class", "LEGACY_PATH"),
|
||||||
|
("py:class", "_PluggyPlugin"),
|
||||||
|
# TypeVars
|
||||||
|
("py:class", "_pytest._code.code.E"),
|
||||||
|
("py:class", "_pytest.fixtures.FixtureFunction"),
|
||||||
|
("py:class", "_pytest.nodes._NodeType"),
|
||||||
|
("py:class", "_pytest.python_api.E"),
|
||||||
|
("py:class", "_pytest.recwarn.T"),
|
||||||
|
("py:class", "_pytest.runner.TResult"),
|
||||||
|
("py:obj", "_pytest.fixtures.FixtureValue"),
|
||||||
|
("py:obj", "_pytest.stash.T"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
sys.path.append(os.path.abspath("_themes"))
|
sys.path.append(os.path.abspath("_themes"))
|
||||||
|
|
|
@ -177,7 +177,7 @@ arguments they only pass on to the superclass.
|
||||||
resolved in future versions as we slowly get rid of the :pypi:`py`
|
resolved in future versions as we slowly get rid of the :pypi:`py`
|
||||||
dependency (see :issue:`9283` for a longer discussion).
|
dependency (see :issue:`9283` for a longer discussion).
|
||||||
|
|
||||||
Due to the ongoing migration of methods like :meth:`~_pytest.Item.reportinfo`
|
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
|
||||||
which still is expected to return a ``py.path.local`` object, nodes still have
|
which still is expected to return a ``py.path.local`` object, nodes still have
|
||||||
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
|
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
|
||||||
no matter what argument was used in the constructor. We expect to deprecate the
|
no matter what argument was used in the constructor. We expect to deprecate the
|
||||||
|
@ -336,7 +336,7 @@ Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
|
||||||
|
|
||||||
.. deprecated:: 7.0
|
.. deprecated:: 7.0
|
||||||
|
|
||||||
Defining a custom pytest node type which is both an :class:`pytest.Item <Item>` and a :class:`pytest.Collector <Collector>` (e.g. :class:`pytest.File <File>`) now issues a warning.
|
Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
|
||||||
It was never sanely supported and triggers hard to debug errors.
|
It was never sanely supported and triggers hard to debug errors.
|
||||||
|
|
||||||
Some plugins providing linting/code analysis have been using this as a hack.
|
Some plugins providing linting/code analysis have been using this as a hack.
|
||||||
|
@ -348,8 +348,8 @@ Instead, a separate collector node should be used, which collects the item. See
|
||||||
|
|
||||||
.. _uncooperative-constructors-deprecated:
|
.. _uncooperative-constructors-deprecated:
|
||||||
|
|
||||||
Constructors of custom :class:`pytest.Node` subclasses should take ``**kwargs``
|
Constructors of custom :class:`~_pytest.nodes.Node` subclasses should take ``**kwargs``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. deprecated:: 7.0
|
.. deprecated:: 7.0
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
Parametrizing tests
|
Parametrizing tests
|
||||||
=================================================
|
=================================================
|
||||||
|
|
||||||
.. currentmodule:: _pytest.python
|
|
||||||
|
|
||||||
``pytest`` allows to easily parametrize test functions.
|
``pytest`` allows to easily parametrize test functions.
|
||||||
For basic docs, see :ref:`parametrize-basics`.
|
For basic docs, see :ref:`parametrize-basics`.
|
||||||
|
|
||||||
|
@ -185,7 +183,7 @@ A quick port of "testscenarios"
|
||||||
Here is a quick port to run tests configured with :pypi:`testscenarios`,
|
Here is a quick port to run tests configured with :pypi:`testscenarios`,
|
||||||
an add-on from Robert Collins for the standard unittest framework. We
|
an add-on from Robert Collins for the standard unittest framework. We
|
||||||
only have to work a bit to construct the correct arguments for pytest's
|
only have to work a bit to construct the correct arguments for pytest's
|
||||||
:py:func:`Metafunc.parametrize`:
|
:py:func:`Metafunc.parametrize <pytest.Metafunc.parametrize>`:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,7 @@ Now we'll get feedback on a bad argument:
|
||||||
|
|
||||||
|
|
||||||
If you need to provide more detailed error messages, you can use the
|
If you need to provide more detailed error messages, you can use the
|
||||||
``type`` parameter and raise ``pytest.UsageError``:
|
``type`` parameter and raise :exc:`pytest.UsageError`:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,7 @@ A note about fixture cleanup
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
pytest does not do any special processing for :data:`SIGTERM <signal.SIGTERM>` and
|
pytest does not do any special processing for :data:`SIGTERM <signal.SIGTERM>` and
|
||||||
:data:`SIGQUIT <signal.SIGQUIT>` signals (:data:`SIGINT <signal.SIGINT>` is handled naturally
|
``SIGQUIT`` signals (:data:`SIGINT <signal.SIGINT>` is handled naturally
|
||||||
by the Python runtime via :class:`KeyboardInterrupt`), so fixtures that manage external resources which are important
|
by the Python runtime via :class:`KeyboardInterrupt`), so fixtures that manage external resources which are important
|
||||||
to be cleared when the Python process is terminated (by those signals) might leak resources.
|
to be cleared when the Python process is terminated (by those signals) might leak resources.
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,6 @@ funcarg mechanism, see :ref:`historical funcargs and pytest.funcargs`.
|
||||||
If you are new to pytest, then you can simply ignore this
|
If you are new to pytest, then you can simply ignore this
|
||||||
section and read the other sections.
|
section and read the other sections.
|
||||||
|
|
||||||
.. currentmodule:: _pytest
|
|
||||||
|
|
||||||
Shortcomings of the previous ``pytest_funcarg__`` mechanism
|
Shortcomings of the previous ``pytest_funcarg__`` mechanism
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -46,7 +44,7 @@ There are several limitations and difficulties with this approach:
|
||||||
|
|
||||||
2. parametrizing the "db" resource is not straight forward:
|
2. parametrizing the "db" resource is not straight forward:
|
||||||
you need to apply a "parametrize" decorator or implement a
|
you need to apply a "parametrize" decorator or implement a
|
||||||
:py:func:`~hookspec.pytest_generate_tests` hook
|
:hook:`pytest_generate_tests` hook
|
||||||
calling :py:func:`~pytest.Metafunc.parametrize` which
|
calling :py:func:`~pytest.Metafunc.parametrize` which
|
||||||
performs parametrization at the places where the resource
|
performs parametrization at the places where the resource
|
||||||
is used. Moreover, you need to modify the factory to use an
|
is used. Moreover, you need to modify the factory to use an
|
||||||
|
@ -94,7 +92,7 @@ Direct parametrization of funcarg resource factories
|
||||||
|
|
||||||
Previously, funcarg factories could not directly cause parametrization.
|
Previously, funcarg factories could not directly cause parametrization.
|
||||||
You needed to specify a ``@parametrize`` decorator on your test function
|
You needed to specify a ``@parametrize`` decorator on your test function
|
||||||
or implement a ``pytest_generate_tests`` hook to perform
|
or implement a :hook:`pytest_generate_tests` hook to perform
|
||||||
parametrization, i.e. calling a test multiple times with different value
|
parametrization, i.e. calling a test multiple times with different value
|
||||||
sets. pytest-2.3 introduces a decorator for use on the factory itself:
|
sets. pytest-2.3 introduces a decorator for use on the factory itself:
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ More details can be found in the :pull:`original PR <3317>`.
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
in a future major release of pytest we will introduce class based markers,
|
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:`~_pytest.mark.Mark`.
|
at which point markers will no longer be limited to instances of :py:class:`~pytest.Mark`.
|
||||||
|
|
||||||
|
|
||||||
cache plugin integrated into the core
|
cache plugin integrated into the core
|
||||||
|
|
|
@ -98,6 +98,27 @@ and if you need to have access to the actual exception info you may use:
|
||||||
the actual exception raised. The main attributes of interest are
|
the actual exception raised. The main attributes of interest are
|
||||||
``.type``, ``.value`` and ``.traceback``.
|
``.type``, ``.value`` and ``.traceback``.
|
||||||
|
|
||||||
|
Note that ``pytest.raises`` will match the exception type or any subclasses (like the standard ``except`` statement).
|
||||||
|
If you want to check if a block of code is raising an exact exception type, you need to check that explicitly:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def test_foo_not_implemented():
|
||||||
|
def foo():
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError) as excinfo:
|
||||||
|
foo()
|
||||||
|
assert excinfo.type is RuntimeError
|
||||||
|
|
||||||
|
The :func:`pytest.raises` call will succeed, even though the function raises :class:`NotImplementedError`, because
|
||||||
|
:class:`NotImplementedError` is a subclass of :class:`RuntimeError`; however the following `assert` statement will
|
||||||
|
catch the problem.
|
||||||
|
|
||||||
|
Matching exception messages
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can pass a ``match`` keyword parameter to the context-manager to test
|
You can pass a ``match`` keyword parameter to the context-manager to test
|
||||||
that a regular expression matches on the string representation of an exception
|
that a regular expression matches on the string representation of an exception
|
||||||
(similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``):
|
(similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``):
|
||||||
|
@ -115,9 +136,15 @@ that a regular expression matches on the string representation of an exception
|
||||||
with pytest.raises(ValueError, match=r".* 123 .*"):
|
with pytest.raises(ValueError, match=r".* 123 .*"):
|
||||||
myfunc()
|
myfunc()
|
||||||
|
|
||||||
The regexp parameter of the ``match`` parameter is matched with the ``re.search``
|
Notes:
|
||||||
function, so in the above example ``match='123'`` would have worked as
|
|
||||||
well.
|
* The ``match`` parameter is matched with the :func:`re.search`
|
||||||
|
function, so in the above example ``match='123'`` would have worked as well.
|
||||||
|
* The ``match`` parameter also matches against `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``.
|
||||||
|
|
||||||
|
|
||||||
|
Matching exception groups
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
|
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
|
||||||
method to test for exceptions returned as part of an ``ExceptionGroup``:
|
method to test for exceptions returned as part of an ``ExceptionGroup``:
|
||||||
|
@ -165,32 +192,55 @@ exception at a specific level; exceptions contained directly in the top
|
||||||
assert not excinfo.group_contains(RuntimeError, depth=2)
|
assert not excinfo.group_contains(RuntimeError, depth=2)
|
||||||
assert not excinfo.group_contains(TypeError, depth=1)
|
assert not excinfo.group_contains(TypeError, depth=1)
|
||||||
|
|
||||||
There's an alternate form of the :func:`pytest.raises` function where you pass
|
Alternate form (legacy)
|
||||||
a function that will be executed with the given ``*args`` and ``**kwargs`` and
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
assert that the given exception is raised:
|
|
||||||
|
There is an alternate form where you pass
|
||||||
|
a function that will be executed, along ``*args`` and ``**kwargs``, and :func:`pytest.raises`
|
||||||
|
will execute the function with the arguments and assert that the given exception is raised:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
pytest.raises(ExpectedException, func, *args, **kwargs)
|
def func(x):
|
||||||
|
if x <= 0:
|
||||||
|
raise ValueError("x needs to be larger than zero")
|
||||||
|
|
||||||
|
|
||||||
|
pytest.raises(ValueError, func, x=-1)
|
||||||
|
|
||||||
The reporter will provide you with helpful output in case of failures such as *no
|
The reporter will provide you with helpful output in case of failures such as *no
|
||||||
exception* or *wrong exception*.
|
exception* or *wrong exception*.
|
||||||
|
|
||||||
Note that it is also possible to specify a "raises" argument to
|
This form was the original :func:`pytest.raises` API, developed before the ``with`` statement was
|
||||||
``pytest.mark.xfail``, which checks that the test is failing in a more
|
added to the Python language. Nowadays, this form is rarely used, with the context-manager form (using ``with``)
|
||||||
|
being considered more readable.
|
||||||
|
Nonetheless, this form is fully supported and not deprecated in any way.
|
||||||
|
|
||||||
|
xfail mark and pytest.raises
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
It is also possible to specify a ``raises`` argument to
|
||||||
|
:ref:`pytest.mark.xfail <pytest.mark.xfail ref>`, which checks that the test is failing in a more
|
||||||
specific way than just having any exception raised:
|
specific way than just having any exception raised:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
def f():
|
||||||
|
raise IndexError()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(raises=IndexError)
|
@pytest.mark.xfail(raises=IndexError)
|
||||||
def test_f():
|
def test_f():
|
||||||
f()
|
f()
|
||||||
|
|
||||||
Using :func:`pytest.raises` is likely to be better for cases where you are
|
|
||||||
testing exceptions your own code is deliberately raising, whereas using
|
This will only "xfail" if the test fails by raising ``IndexError`` or subclasses.
|
||||||
``@pytest.mark.xfail`` with a check function is probably better for something
|
|
||||||
like documenting unfixed bugs (where the test describes what "should" happen)
|
* Using :ref:`pytest.mark.xfail <pytest.mark.xfail ref>` with the ``raises`` parameter is probably better for something
|
||||||
or bugs in dependencies.
|
like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies.
|
||||||
|
|
||||||
|
* Using :func:`pytest.raises` is likely to be better for cases where you are
|
||||||
|
testing exceptions your own code is deliberately raising, which is the majority of cases.
|
||||||
|
|
||||||
|
|
||||||
.. _`assertwarns`:
|
.. _`assertwarns`:
|
||||||
|
|
|
@ -382,8 +382,6 @@ warnings: a WarningsRecorder instance. To view the recorded warnings, you can
|
||||||
iterate over this instance, call ``len`` on it to get the number of recorded
|
iterate over this instance, call ``len`` on it to get the number of recorded
|
||||||
warnings, or index into it to get a particular recorded warning.
|
warnings, or index into it to get a particular recorded warning.
|
||||||
|
|
||||||
.. currentmodule:: _pytest.warnings
|
|
||||||
|
|
||||||
Full API: :class:`~_pytest.recwarn.WarningsRecorder`.
|
Full API: :class:`~_pytest.recwarn.WarningsRecorder`.
|
||||||
|
|
||||||
.. _`warns use cases`:
|
.. _`warns use cases`:
|
||||||
|
|
|
@ -1271,7 +1271,7 @@ configured in multiple ways.
|
||||||
Extending the previous example, we can flag the fixture to create two
|
Extending the previous example, we can flag the fixture to create two
|
||||||
``smtp_connection`` fixture instances which will cause all tests using the fixture
|
``smtp_connection`` fixture instances which will cause all tests using the fixture
|
||||||
to run twice. The fixture function gets access to each parameter
|
to run twice. The fixture function gets access to each parameter
|
||||||
through the special :py:class:`request <FixtureRequest>` object:
|
through the special :py:class:`request <pytest.FixtureRequest>` object:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
|
@ -241,7 +241,7 @@ through ``add_color_level()``. Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.hookimpl
|
@pytest.hookimpl(trylast=True)
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
|
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
|
||||||
|
|
||||||
|
|
|
@ -286,6 +286,20 @@ situations, for example you are shown even fixtures that start with ``_`` if you
|
||||||
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
|
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
|
||||||
however some plugins might make use of higher verbosity.
|
however some plugins might make use of higher verbosity.
|
||||||
|
|
||||||
|
.. _`pytest.fine_grained_verbosity`:
|
||||||
|
|
||||||
|
Fine-grained verbosity
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently.
|
||||||
|
This is done by setting a verbosity level in the configuration file for the specific aspect of the output.
|
||||||
|
|
||||||
|
:confval:`verbosity_assertions`: Controls how verbose the assertion output should be when pytest is executed. Running
|
||||||
|
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
|
||||||
|
the file is shown by a single character in the output.
|
||||||
|
|
||||||
|
(Note: currently this is the only option available, but more might be added in the future).
|
||||||
|
|
||||||
.. _`pytest.detailed_failed_tests_usage`:
|
.. _`pytest.detailed_failed_tests_usage`:
|
||||||
|
|
||||||
Producing a detailed summary report
|
Producing a detailed summary report
|
||||||
|
|
|
@ -59,10 +59,6 @@ The remaining hook functions will not be called in this case.
|
||||||
hook wrappers: executing around other hooks
|
hook wrappers: executing around other hooks
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
.. currentmodule:: _pytest.core
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pytest plugins can implement hook wrappers which wrap the execution
|
pytest plugins can implement hook wrappers which wrap the execution
|
||||||
of other hook implementations. A hook wrapper is a generator function
|
of other hook implementations. A hook wrapper is a generator function
|
||||||
which yields exactly once. When pytest invokes hooks it first executes
|
which yields exactly once. When pytest invokes hooks it first executes
|
||||||
|
@ -165,6 +161,7 @@ Here is the order of execution:
|
||||||
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
|
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
|
||||||
in which case it will influence the ordering of hook wrappers among each other.
|
in which case it will influence the ordering of hook wrappers among each other.
|
||||||
|
|
||||||
|
.. _`declaringhooks`:
|
||||||
|
|
||||||
Declaring new hooks
|
Declaring new hooks
|
||||||
------------------------
|
------------------------
|
||||||
|
@ -174,13 +171,11 @@ Declaring new hooks
|
||||||
This is a quick overview on how to add new hooks and how they work in general, but a more complete
|
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/>`__.
|
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
|
Plugins and ``conftest.py`` files may declare new hooks that can then be
|
||||||
implemented by other plugins in order to alter behaviour or interact with
|
implemented by other plugins in order to alter behaviour or interact with
|
||||||
the new plugin:
|
the new plugin:
|
||||||
|
|
||||||
.. autofunction:: pytest_addhooks
|
.. autofunction:: _pytest.hookspec.pytest_addhooks
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
Hooks are usually declared as do-nothing functions that contain only
|
Hooks are usually declared as do-nothing functions that contain only
|
||||||
|
|
|
@ -11,9 +11,6 @@ Fixtures reference
|
||||||
.. seealso:: :ref:`about-fixtures`
|
.. seealso:: :ref:`about-fixtures`
|
||||||
.. seealso:: :ref:`how-to-fixtures`
|
.. seealso:: :ref:`how-to-fixtures`
|
||||||
|
|
||||||
|
|
||||||
.. currentmodule:: _pytest.python
|
|
||||||
|
|
||||||
.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection
|
.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,15 +73,13 @@ Built-in fixtures
|
||||||
:class:`pathlib.Path` objects.
|
:class:`pathlib.Path` objects.
|
||||||
|
|
||||||
:fixture:`tmpdir`
|
:fixture:`tmpdir`
|
||||||
Provide a :class:`py.path.local` object to a temporary
|
Provide a `py.path.local <https://py.readthedocs.io/en/latest/path.html>`_ object to a temporary
|
||||||
directory which is unique to each test function;
|
directory which is unique to each test function;
|
||||||
replaced by :fixture:`tmp_path`.
|
replaced by :fixture:`tmp_path`.
|
||||||
|
|
||||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
|
||||||
|
|
||||||
:fixture:`tmpdir_factory`
|
:fixture:`tmpdir_factory`
|
||||||
Make session-scoped temporary directories and return
|
Make session-scoped temporary directories and return
|
||||||
:class:`py.path.local` objects;
|
``py.path.local`` objects;
|
||||||
replaced by :fixture:`tmp_path_factory`.
|
replaced by :fixture:`tmp_path_factory`.
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,7 +93,7 @@ Fixture availability is determined from the perspective of the test. A fixture
|
||||||
is only available for tests to request if they are in the scope that fixture is
|
is only available for tests to request if they are in the scope that fixture is
|
||||||
defined in. If a fixture is defined inside a class, it can only be requested by
|
defined in. If a fixture is defined inside a class, it can only be requested by
|
||||||
tests inside that class. But if a fixture is defined inside the global scope of
|
tests inside that class. But if a fixture is defined inside the global scope of
|
||||||
the module, than every test in that module, even if it's defined inside a class,
|
the module, then every test in that module, even if it's defined inside a class,
|
||||||
can request it.
|
can request it.
|
||||||
|
|
||||||
Similarly, a test can also only be affected by an autouse fixture if that test
|
Similarly, a test can also only be affected by an autouse fixture if that test
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -79,7 +79,7 @@ pytest.xfail
|
||||||
pytest.exit
|
pytest.exit
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
.. autofunction:: pytest.exit(reason, [returncode=False, msg=None])
|
.. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
|
||||||
|
|
||||||
pytest.main
|
pytest.main
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
@ -239,17 +239,18 @@ pytest.mark.xfail
|
||||||
|
|
||||||
Marks a test function as *expected to fail*.
|
Marks a test function as *expected to fail*.
|
||||||
|
|
||||||
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=xfail_strict)
|
.. py:function:: pytest.mark.xfail(condition=False, *, reason=None, raises=None, run=True, strict=xfail_strict)
|
||||||
|
|
||||||
:type condition: bool or str
|
:keyword Union[bool, str] condition:
|
||||||
:param condition:
|
|
||||||
Condition for marking the test function as xfail (``True/False`` or a
|
Condition for marking the test function as xfail (``True/False`` or a
|
||||||
:ref:`condition string <string conditions>`). If a bool, you also have
|
:ref:`condition string <string conditions>`). If a ``bool``, you also have
|
||||||
to specify ``reason`` (see :ref:`condition string <string conditions>`).
|
to specify ``reason`` (see :ref:`condition string <string conditions>`).
|
||||||
:keyword str reason:
|
:keyword str reason:
|
||||||
Reason why the test function is marked as xfail.
|
Reason why the test function is marked as xfail.
|
||||||
:keyword Type[Exception] raises:
|
:keyword Type[Exception] raises:
|
||||||
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test.
|
Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test.
|
||||||
|
Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works).
|
||||||
|
|
||||||
:keyword bool run:
|
:keyword bool run:
|
||||||
Whether the test function should actually be executed. If ``False``, the function will always xfail and will
|
Whether 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).
|
not be executed (useful if a function is segfaulting).
|
||||||
|
@ -611,10 +612,30 @@ Hooks
|
||||||
|
|
||||||
**Tutorial**: :ref:`writing-plugins`
|
**Tutorial**: :ref:`writing-plugins`
|
||||||
|
|
||||||
.. currentmodule:: _pytest.hookspec
|
|
||||||
|
|
||||||
Reference to all hooks which can be implemented by :ref:`conftest.py files <localplugin>` and :ref:`plugins <plugins>`.
|
Reference to all hooks which can be implemented by :ref:`conftest.py files <localplugin>` and :ref:`plugins <plugins>`.
|
||||||
|
|
||||||
|
@pytest.hookimpl
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. function:: pytest.hookimpl
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
pytest's decorator for marking functions as hook implementations.
|
||||||
|
|
||||||
|
See :ref:`writinghooks` and :func:`pluggy.HookimplMarker`.
|
||||||
|
|
||||||
|
@pytest.hookspec
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. function:: pytest.hookspec
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
pytest's decorator for marking functions as hook specifications.
|
||||||
|
|
||||||
|
See :ref:`declaringhooks` and :func:`pluggy.HookspecMarker`.
|
||||||
|
|
||||||
|
.. currentmodule:: _pytest.hookspec
|
||||||
|
|
||||||
Bootstrapping hooks
|
Bootstrapping hooks
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -800,6 +821,7 @@ Node
|
||||||
|
|
||||||
.. autoclass:: _pytest.nodes.Node()
|
.. autoclass:: _pytest.nodes.Node()
|
||||||
:members:
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Collector
|
Collector
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
@ -1136,7 +1158,10 @@ When set (regardless of value), pytest will use color in terminal output.
|
||||||
Exceptions
|
Exceptions
|
||||||
----------
|
----------
|
||||||
|
|
||||||
.. autoclass:: pytest.UsageError()
|
.. autoexception:: pytest.UsageError()
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoexception:: pytest.FixtureLookupError()
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
.. _`warnings ref`:
|
.. _`warnings ref`:
|
||||||
|
@ -1821,6 +1846,19 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
clean_db
|
clean_db
|
||||||
|
|
||||||
|
|
||||||
|
.. confval:: verbosity_assertions
|
||||||
|
|
||||||
|
Set a verbosity level specifically for assertion related output, overriding the application wide level.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
verbosity_assertions = 2
|
||||||
|
|
||||||
|
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
|
||||||
|
"auto" can be used to explicitly use the global verbosity level.
|
||||||
|
|
||||||
|
|
||||||
.. confval:: xfail_strict
|
.. confval:: xfail_strict
|
||||||
|
|
||||||
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
|
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = [
|
requires = [
|
||||||
# sync with setup.py until we discard non-pep-517/518
|
|
||||||
"setuptools>=45.0",
|
"setuptools>=45.0",
|
||||||
"setuptools-scm[toml]>=6.2.3",
|
"setuptools-scm[toml]>=6.2.3",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,675 @@
|
||||||
|
# This module was imported from the cpython standard library
|
||||||
|
# (https://github.com/python/cpython/) at commit
|
||||||
|
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Original Author: Fred L. Drake, Jr.
|
||||||
|
# fdrake@acm.org
|
||||||
|
#
|
||||||
|
# This is a simple little module I wrote to make life easier. I didn't
|
||||||
|
# see anything quite like it in the library, though I may have overlooked
|
||||||
|
# something. I wrote this when I was trying to read some heavily nested
|
||||||
|
# tuples with fairly non-descriptive content. This is modeled very much
|
||||||
|
# after Lisp/Scheme - style pretty-printing of lists. If you find it
|
||||||
|
# useful, thank small children who sleep at night.
|
||||||
|
import collections as _collections
|
||||||
|
import dataclasses as _dataclasses
|
||||||
|
import re
|
||||||
|
import types as _types
|
||||||
|
from io import StringIO as _StringIO
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
|
from typing import IO
|
||||||
|
from typing import Iterator
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Set
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class _safe_key:
|
||||||
|
"""Helper function for key functions when sorting unorderable objects.
|
||||||
|
|
||||||
|
The wrapped-object will fallback to a Py2.x style comparison for
|
||||||
|
unorderable types (sorting first comparing the type name and then by
|
||||||
|
the obj ids). Does not work recursively, so dict.items() must have
|
||||||
|
_safe_key applied to both the key and the value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ["obj"]
|
||||||
|
|
||||||
|
def __init__(self, obj):
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
try:
|
||||||
|
return self.obj < other.obj
|
||||||
|
except TypeError:
|
||||||
|
return (str(type(self.obj)), id(self.obj)) < (
|
||||||
|
str(type(other.obj)),
|
||||||
|
id(other.obj),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_tuple(t):
|
||||||
|
"""Helper function for comparing 2-tuples"""
|
||||||
|
return _safe_key(t[0]), _safe_key(t[1])
|
||||||
|
|
||||||
|
|
||||||
|
class PrettyPrinter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
indent: int = 4,
|
||||||
|
width: int = 80,
|
||||||
|
depth: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Handle pretty printing operations onto a stream using a set of
|
||||||
|
configured parameters.
|
||||||
|
|
||||||
|
indent
|
||||||
|
Number of spaces to indent for each level of nesting.
|
||||||
|
|
||||||
|
width
|
||||||
|
Attempted maximum number of columns in the output.
|
||||||
|
|
||||||
|
depth
|
||||||
|
The maximum depth to print out nested structures.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if indent < 0:
|
||||||
|
raise ValueError("indent must be >= 0")
|
||||||
|
if depth is not None and depth <= 0:
|
||||||
|
raise ValueError("depth must be > 0")
|
||||||
|
if not width:
|
||||||
|
raise ValueError("width must be != 0")
|
||||||
|
self._depth = depth
|
||||||
|
self._indent_per_level = indent
|
||||||
|
self._width = width
|
||||||
|
|
||||||
|
def pformat(self, object: Any) -> str:
|
||||||
|
sio = _StringIO()
|
||||||
|
self._format(object, sio, 0, 0, set(), 0)
|
||||||
|
return sio.getvalue()
|
||||||
|
|
||||||
|
def _format(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
objid = id(object)
|
||||||
|
if objid in context:
|
||||||
|
stream.write(_recursion(object))
|
||||||
|
return
|
||||||
|
|
||||||
|
p = self._dispatch.get(type(object).__repr__, None)
|
||||||
|
if p is not None:
|
||||||
|
context.add(objid)
|
||||||
|
p(self, object, stream, indent, allowance, context, level + 1)
|
||||||
|
context.remove(objid)
|
||||||
|
elif (
|
||||||
|
_dataclasses.is_dataclass(object)
|
||||||
|
and not isinstance(object, type)
|
||||||
|
and object.__dataclass_params__.repr
|
||||||
|
and
|
||||||
|
# Check dataclass has generated repr method.
|
||||||
|
hasattr(object.__repr__, "__wrapped__")
|
||||||
|
and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
|
||||||
|
):
|
||||||
|
context.add(objid)
|
||||||
|
self._pprint_dataclass(
|
||||||
|
object, stream, indent, allowance, context, level + 1
|
||||||
|
)
|
||||||
|
context.remove(objid)
|
||||||
|
else:
|
||||||
|
stream.write(self._repr(object, context, level))
|
||||||
|
|
||||||
|
def _pprint_dataclass(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
cls_name = object.__class__.__name__
|
||||||
|
items = [
|
||||||
|
(f.name, getattr(object, f.name))
|
||||||
|
for f in _dataclasses.fields(object)
|
||||||
|
if f.repr
|
||||||
|
]
|
||||||
|
stream.write(cls_name + "(")
|
||||||
|
self._format_namespace_items(items, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch: Dict[
|
||||||
|
Callable[..., str],
|
||||||
|
Callable[["PrettyPrinter", Any, IO[str], int, int, Set[int], int], None],
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
def _pprint_dict(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
write = stream.write
|
||||||
|
write("{")
|
||||||
|
items = sorted(object.items(), key=_safe_tuple)
|
||||||
|
self._format_dict_items(items, stream, indent, allowance, context, level)
|
||||||
|
write("}")
|
||||||
|
|
||||||
|
_dispatch[dict.__repr__] = _pprint_dict
|
||||||
|
|
||||||
|
def _pprint_ordered_dict(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
cls = object.__class__
|
||||||
|
stream.write(cls.__name__ + "(")
|
||||||
|
self._pprint_dict(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
|
||||||
|
|
||||||
|
def _pprint_list(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write("[")
|
||||||
|
self._format_items(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write("]")
|
||||||
|
|
||||||
|
_dispatch[list.__repr__] = _pprint_list
|
||||||
|
|
||||||
|
def _pprint_tuple(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write("(")
|
||||||
|
self._format_items(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[tuple.__repr__] = _pprint_tuple
|
||||||
|
|
||||||
|
def _pprint_set(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
typ = object.__class__
|
||||||
|
if typ is set:
|
||||||
|
stream.write("{")
|
||||||
|
endchar = "}"
|
||||||
|
else:
|
||||||
|
stream.write(typ.__name__ + "({")
|
||||||
|
endchar = "})"
|
||||||
|
object = sorted(object, key=_safe_key)
|
||||||
|
self._format_items(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write(endchar)
|
||||||
|
|
||||||
|
_dispatch[set.__repr__] = _pprint_set
|
||||||
|
_dispatch[frozenset.__repr__] = _pprint_set
|
||||||
|
|
||||||
|
def _pprint_str(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
write = stream.write
|
||||||
|
if not len(object):
|
||||||
|
write(repr(object))
|
||||||
|
return
|
||||||
|
chunks = []
|
||||||
|
lines = object.splitlines(True)
|
||||||
|
if level == 1:
|
||||||
|
indent += 1
|
||||||
|
allowance += 1
|
||||||
|
max_width1 = max_width = self._width - indent
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
rep = repr(line)
|
||||||
|
if i == len(lines) - 1:
|
||||||
|
max_width1 -= allowance
|
||||||
|
if len(rep) <= max_width1:
|
||||||
|
chunks.append(rep)
|
||||||
|
else:
|
||||||
|
# A list of alternating (non-space, space) strings
|
||||||
|
parts = re.findall(r"\S*\s*", line)
|
||||||
|
assert parts
|
||||||
|
assert not parts[-1]
|
||||||
|
parts.pop() # drop empty last part
|
||||||
|
max_width2 = max_width
|
||||||
|
current = ""
|
||||||
|
for j, part in enumerate(parts):
|
||||||
|
candidate = current + part
|
||||||
|
if j == len(parts) - 1 and i == len(lines) - 1:
|
||||||
|
max_width2 -= allowance
|
||||||
|
if len(repr(candidate)) > max_width2:
|
||||||
|
if current:
|
||||||
|
chunks.append(repr(current))
|
||||||
|
current = part
|
||||||
|
else:
|
||||||
|
current = candidate
|
||||||
|
if current:
|
||||||
|
chunks.append(repr(current))
|
||||||
|
if len(chunks) == 1:
|
||||||
|
write(rep)
|
||||||
|
return
|
||||||
|
if level == 1:
|
||||||
|
write("(")
|
||||||
|
for i, rep in enumerate(chunks):
|
||||||
|
if i > 0:
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
write(rep)
|
||||||
|
if level == 1:
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[str.__repr__] = _pprint_str
|
||||||
|
|
||||||
|
def _pprint_bytes(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
write = stream.write
|
||||||
|
if len(object) <= 4:
|
||||||
|
write(repr(object))
|
||||||
|
return
|
||||||
|
parens = level == 1
|
||||||
|
if parens:
|
||||||
|
indent += 1
|
||||||
|
allowance += 1
|
||||||
|
write("(")
|
||||||
|
delim = ""
|
||||||
|
for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
|
||||||
|
write(delim)
|
||||||
|
write(rep)
|
||||||
|
if not delim:
|
||||||
|
delim = "\n" + " " * indent
|
||||||
|
if parens:
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[bytes.__repr__] = _pprint_bytes
|
||||||
|
|
||||||
|
def _pprint_bytearray(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
write = stream.write
|
||||||
|
write("bytearray(")
|
||||||
|
self._pprint_bytes(
|
||||||
|
bytes(object), stream, indent + 10, allowance + 1, context, level + 1
|
||||||
|
)
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[bytearray.__repr__] = _pprint_bytearray
|
||||||
|
|
||||||
|
def _pprint_mappingproxy(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write("mappingproxy(")
|
||||||
|
self._format(object.copy(), stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
|
||||||
|
|
||||||
|
def _pprint_simplenamespace(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if type(object) is _types.SimpleNamespace:
|
||||||
|
# The SimpleNamespace repr is "namespace" instead of the class
|
||||||
|
# name, so we do the same here. For subclasses; use the class name.
|
||||||
|
cls_name = "namespace"
|
||||||
|
else:
|
||||||
|
cls_name = object.__class__.__name__
|
||||||
|
items = object.__dict__.items()
|
||||||
|
stream.write(cls_name + "(")
|
||||||
|
self._format_namespace_items(items, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
|
||||||
|
|
||||||
|
def _format_dict_items(
|
||||||
|
self,
|
||||||
|
items: List[Tuple[Any, Any]],
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
write = stream.write
|
||||||
|
item_indent = indent + self._indent_per_level
|
||||||
|
delimnl = "\n" + " " * item_indent
|
||||||
|
for key, ent in items:
|
||||||
|
write(delimnl)
|
||||||
|
write(self._repr(key, context, level))
|
||||||
|
write(": ")
|
||||||
|
self._format(ent, stream, item_indent, 1, context, level)
|
||||||
|
write(",")
|
||||||
|
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
|
||||||
|
def _format_namespace_items(
|
||||||
|
self,
|
||||||
|
items: List[Tuple[Any, Any]],
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
write = stream.write
|
||||||
|
item_indent = indent + self._indent_per_level
|
||||||
|
delimnl = "\n" + " " * item_indent
|
||||||
|
for key, ent in items:
|
||||||
|
write(delimnl)
|
||||||
|
write(key)
|
||||||
|
write("=")
|
||||||
|
if id(ent) in context:
|
||||||
|
# Special-case representation of recursion to match standard
|
||||||
|
# recursive dataclass repr.
|
||||||
|
write("...")
|
||||||
|
else:
|
||||||
|
self._format(
|
||||||
|
ent,
|
||||||
|
stream,
|
||||||
|
item_indent + len(key) + 1,
|
||||||
|
1,
|
||||||
|
context,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
|
||||||
|
write(",")
|
||||||
|
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
|
||||||
|
def _format_items(
|
||||||
|
self,
|
||||||
|
items: List[Any],
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
write = stream.write
|
||||||
|
item_indent = indent + self._indent_per_level
|
||||||
|
delimnl = "\n" + " " * item_indent
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
write(delimnl)
|
||||||
|
self._format(item, stream, item_indent, 1, context, level)
|
||||||
|
write(",")
|
||||||
|
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
|
||||||
|
def _repr(self, object: Any, context: Set[int], level: int) -> str:
|
||||||
|
return self._safe_repr(object, context.copy(), self._depth, level)
|
||||||
|
|
||||||
|
def _pprint_default_dict(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
rdf = self._repr(object.default_factory, context, level)
|
||||||
|
stream.write(f"{object.__class__.__name__}({rdf}, ")
|
||||||
|
self._pprint_dict(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
|
||||||
|
|
||||||
|
def _pprint_counter(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write(object.__class__.__name__ + "(")
|
||||||
|
|
||||||
|
if object:
|
||||||
|
stream.write("{")
|
||||||
|
items = object.most_common()
|
||||||
|
self._format_dict_items(items, stream, indent, allowance, context, level)
|
||||||
|
stream.write("}")
|
||||||
|
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.Counter.__repr__] = _pprint_counter
|
||||||
|
|
||||||
|
def _pprint_chain_map(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
|
||||||
|
stream.write(object.__class__.__name__ + "(")
|
||||||
|
self._format_items(object.maps, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
|
||||||
|
|
||||||
|
def _pprint_deque(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write(object.__class__.__name__ + "(")
|
||||||
|
if object.maxlen is not None:
|
||||||
|
stream.write("maxlen=%d, " % object.maxlen)
|
||||||
|
stream.write("[")
|
||||||
|
|
||||||
|
self._format_items(object, stream, indent, allowance + 1, context, level)
|
||||||
|
stream.write("])")
|
||||||
|
|
||||||
|
_dispatch[_collections.deque.__repr__] = _pprint_deque
|
||||||
|
|
||||||
|
def _pprint_user_dict(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
|
||||||
|
|
||||||
|
def _pprint_user_list(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserList.__repr__] = _pprint_user_list
|
||||||
|
|
||||||
|
def _pprint_user_string(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserString.__repr__] = _pprint_user_string
|
||||||
|
|
||||||
|
def _safe_repr(
|
||||||
|
self, object: Any, context: Set[int], maxlevels: Optional[int], level: int
|
||||||
|
) -> str:
|
||||||
|
typ = type(object)
|
||||||
|
if typ in _builtin_scalars:
|
||||||
|
return repr(object)
|
||||||
|
|
||||||
|
r = getattr(typ, "__repr__", None)
|
||||||
|
|
||||||
|
if issubclass(typ, dict) and r is dict.__repr__:
|
||||||
|
if not object:
|
||||||
|
return "{}"
|
||||||
|
objid = id(object)
|
||||||
|
if maxlevels and level >= maxlevels:
|
||||||
|
return "{...}"
|
||||||
|
if objid in context:
|
||||||
|
return _recursion(object)
|
||||||
|
context.add(objid)
|
||||||
|
components: List[str] = []
|
||||||
|
append = components.append
|
||||||
|
level += 1
|
||||||
|
for k, v in sorted(object.items(), key=_safe_tuple):
|
||||||
|
krepr = self._safe_repr(k, context, maxlevels, level)
|
||||||
|
vrepr = self._safe_repr(v, context, maxlevels, level)
|
||||||
|
append(f"{krepr}: {vrepr}")
|
||||||
|
context.remove(objid)
|
||||||
|
return "{%s}" % ", ".join(components)
|
||||||
|
|
||||||
|
if (issubclass(typ, list) and r is list.__repr__) or (
|
||||||
|
issubclass(typ, tuple) and r is tuple.__repr__
|
||||||
|
):
|
||||||
|
if issubclass(typ, list):
|
||||||
|
if not object:
|
||||||
|
return "[]"
|
||||||
|
format = "[%s]"
|
||||||
|
elif len(object) == 1:
|
||||||
|
format = "(%s,)"
|
||||||
|
else:
|
||||||
|
if not object:
|
||||||
|
return "()"
|
||||||
|
format = "(%s)"
|
||||||
|
objid = id(object)
|
||||||
|
if maxlevels and level >= maxlevels:
|
||||||
|
return format % "..."
|
||||||
|
if objid in context:
|
||||||
|
return _recursion(object)
|
||||||
|
context.add(objid)
|
||||||
|
components = []
|
||||||
|
append = components.append
|
||||||
|
level += 1
|
||||||
|
for o in object:
|
||||||
|
orepr = self._safe_repr(o, context, maxlevels, level)
|
||||||
|
append(orepr)
|
||||||
|
context.remove(objid)
|
||||||
|
return format % ", ".join(components)
|
||||||
|
|
||||||
|
return repr(object)
|
||||||
|
|
||||||
|
|
||||||
|
_builtin_scalars = frozenset(
|
||||||
|
{str, bytes, bytearray, float, complex, bool, type(None), int}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _recursion(object: Any) -> str:
|
||||||
|
return f"<Recursion on {type(object).__name__} with id={id(object)}>"
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]:
|
||||||
|
current = b""
|
||||||
|
last = len(object) // 4 * 4
|
||||||
|
for i in range(0, len(object), 4):
|
||||||
|
part = object[i : i + 4]
|
||||||
|
candidate = current + part
|
||||||
|
if i == last:
|
||||||
|
width -= allowance
|
||||||
|
if len(repr(candidate)) > width:
|
||||||
|
if current:
|
||||||
|
yield repr(current)
|
||||||
|
current = part
|
||||||
|
else:
|
||||||
|
current = candidate
|
||||||
|
if current:
|
||||||
|
yield repr(current)
|
|
@ -1,8 +1,5 @@
|
||||||
import pprint
|
import pprint
|
||||||
import reprlib
|
import reprlib
|
||||||
from typing import Any
|
|
||||||
from typing import Dict
|
|
||||||
from typing import IO
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,49 +129,3 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
|
||||||
return repr(obj)
|
return repr(obj)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return _format_repr_exception(exc, obj)
|
return _format_repr_exception(exc, obj)
|
||||||
|
|
||||||
|
|
||||||
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
|
||||||
"""PrettyPrinter that always dispatches (regardless of width)."""
|
|
||||||
|
|
||||||
def _format(
|
|
||||||
self,
|
|
||||||
object: object,
|
|
||||||
stream: IO[str],
|
|
||||||
indent: int,
|
|
||||||
allowance: int,
|
|
||||||
context: Dict[int, Any],
|
|
||||||
level: int,
|
|
||||||
) -> None:
|
|
||||||
# Type ignored because _dispatch is private.
|
|
||||||
p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
objid = id(object)
|
|
||||||
if objid in context or p is None:
|
|
||||||
# Type ignored because _format is private.
|
|
||||||
super()._format( # type: ignore[misc]
|
|
||||||
object,
|
|
||||||
stream,
|
|
||||||
indent,
|
|
||||||
allowance,
|
|
||||||
context,
|
|
||||||
level,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
context[objid] = 1
|
|
||||||
p(self, object, stream, indent, allowance, context, level + 1)
|
|
||||||
del context[objid]
|
|
||||||
|
|
||||||
|
|
||||||
def _pformat_dispatch(
|
|
||||||
object: object,
|
|
||||||
indent: int = 1,
|
|
||||||
width: int = 80,
|
|
||||||
depth: Optional[int] = None,
|
|
||||||
*,
|
|
||||||
compact: bool = False,
|
|
||||||
) -> str:
|
|
||||||
return AlwaysDispatchingPrettyPrinter(
|
|
||||||
indent=indent, width=width, depth=depth, compact=compact
|
|
||||||
).pformat(object)
|
|
||||||
|
|
|
@ -223,7 +223,15 @@ class TerminalWriter:
|
||||||
style=os.getenv("PYTEST_THEME"),
|
style=os.getenv("PYTEST_THEME"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return highlighted
|
# pygments terminal formatter may add a newline when there wasn't one.
|
||||||
|
# We don't want this, remove.
|
||||||
|
if highlighted[-1] == "\n" and source[-1] != "\n":
|
||||||
|
highlighted = highlighted[:-1]
|
||||||
|
|
||||||
|
# Some lexers will not set the initial color explicitly
|
||||||
|
# which may lead to the previous color being propagated to the
|
||||||
|
# start of the expression, so reset first.
|
||||||
|
return "\x1b[0m" + highlighted
|
||||||
except pygments.util.ClassNotFound:
|
except pygments.util.ClassNotFound:
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
||||||
|
|
|
@ -42,6 +42,14 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
help="Enables the pytest_assertion_pass hook. "
|
help="Enables the pytest_assertion_pass hook. "
|
||||||
"Make sure to delete any previously generated pyc cache files.",
|
"Make sure to delete any previously generated pyc cache files.",
|
||||||
)
|
)
|
||||||
|
Config._add_verbosity_ini(
|
||||||
|
parser,
|
||||||
|
Config.VERBOSITY_ASSERTIONS,
|
||||||
|
help=(
|
||||||
|
"Specify a verbosity level for assertions, overriding the main level. "
|
||||||
|
"Higher levels will provide more detailed explanation when an assertion fails."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_assert_rewrite(*names: str) -> None:
|
def register_assert_rewrite(*names: str) -> None:
|
||||||
|
|
|
@ -426,7 +426,10 @@ def _saferepr(obj: object) -> str:
|
||||||
|
|
||||||
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
|
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
|
||||||
"""Get `maxsize` configuration for saferepr based on the given config object."""
|
"""Get `maxsize` configuration for saferepr based on the given config object."""
|
||||||
verbosity = config.getoption("verbose") if config is not None else 0
|
if config is None:
|
||||||
|
verbosity = 0
|
||||||
|
else:
|
||||||
|
verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
return None
|
return None
|
||||||
if verbosity >= 1:
|
if verbosity >= 1:
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
"""Utilities for truncating assertion output.
|
"""Utilities for truncating assertion output.
|
||||||
|
|
||||||
Current default behaviour is to truncate assertion explanations at
|
Current default behaviour is to truncate assertion explanations at
|
||||||
~8 terminal lines, unless running in "-vv" mode or running on CI.
|
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
|
||||||
"""
|
"""
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ def truncate_if_required(
|
||||||
|
|
||||||
def _should_truncate_item(item: Item) -> bool:
|
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
|
verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
return verbose < 2 and not util.running_on_ci()
|
return verbose < 2 and not util.running_on_ci()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ from unicodedata import normalize
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest._io.saferepr import _pformat_dispatch
|
from _pytest._io.pprint import PrettyPrinter
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest._io.saferepr import saferepr_unlimited
|
from _pytest._io.saferepr import saferepr_unlimited
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
@ -168,7 +168,7 @@ def assertrepr_compare(
|
||||||
config, op: str, left: Any, right: Any, use_ascii: bool = False
|
config, op: str, left: Any, right: Any, use_ascii: bool = False
|
||||||
) -> Optional[List[str]]:
|
) -> Optional[List[str]]:
|
||||||
"""Return specialised explanations for some operators/operands."""
|
"""Return specialised explanations for some operators/operands."""
|
||||||
verbose = config.getoption("verbose")
|
verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
|
|
||||||
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
|
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
|
||||||
# See issue #3246.
|
# See issue #3246.
|
||||||
|
@ -192,12 +192,12 @@ def assertrepr_compare(
|
||||||
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
|
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
|
||||||
|
|
||||||
summary = f"{left_repr} {op} {right_repr}"
|
summary = f"{left_repr} {op} {right_repr}"
|
||||||
|
highlighter = config.get_terminal_writer()._highlight
|
||||||
|
|
||||||
explanation = None
|
explanation = None
|
||||||
try:
|
try:
|
||||||
if op == "==":
|
if op == "==":
|
||||||
writer = config.get_terminal_writer()
|
explanation = _compare_eq_any(left, right, highlighter, verbose)
|
||||||
explanation = _compare_eq_any(left, right, writer._highlight, verbose)
|
|
||||||
elif op == "not in":
|
elif op == "not in":
|
||||||
if istext(left) and istext(right):
|
if istext(left) and istext(right):
|
||||||
explanation = _notin_text(left, right, verbose)
|
explanation = _notin_text(left, right, verbose)
|
||||||
|
@ -206,16 +206,16 @@ def assertrepr_compare(
|
||||||
explanation = ["Both sets are equal"]
|
explanation = ["Both sets are equal"]
|
||||||
elif op == ">=":
|
elif op == ">=":
|
||||||
if isset(left) and isset(right):
|
if isset(left) and isset(right):
|
||||||
explanation = _compare_gte_set(left, right, verbose)
|
explanation = _compare_gte_set(left, right, highlighter, verbose)
|
||||||
elif op == "<=":
|
elif op == "<=":
|
||||||
if isset(left) and isset(right):
|
if isset(left) and isset(right):
|
||||||
explanation = _compare_lte_set(left, right, verbose)
|
explanation = _compare_lte_set(left, right, highlighter, verbose)
|
||||||
elif op == ">":
|
elif op == ">":
|
||||||
if isset(left) and isset(right):
|
if isset(left) and isset(right):
|
||||||
explanation = _compare_gt_set(left, right, verbose)
|
explanation = _compare_gt_set(left, right, highlighter, verbose)
|
||||||
elif op == "<":
|
elif op == "<":
|
||||||
if isset(left) and isset(right):
|
if isset(left) and isset(right):
|
||||||
explanation = _compare_lt_set(left, right, verbose)
|
explanation = _compare_lt_set(left, right, highlighter, verbose)
|
||||||
|
|
||||||
except outcomes.Exit:
|
except outcomes.Exit:
|
||||||
raise
|
raise
|
||||||
|
@ -230,6 +230,8 @@ def assertrepr_compare(
|
||||||
if not explanation:
|
if not explanation:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if explanation[0] != "":
|
||||||
|
explanation = [""] + explanation
|
||||||
return [summary] + explanation
|
return [summary] + explanation
|
||||||
|
|
||||||
|
|
||||||
|
@ -257,11 +259,11 @@ def _compare_eq_any(
|
||||||
# used in older code bases before dataclasses/attrs were available.
|
# used in older code bases before dataclasses/attrs were available.
|
||||||
explanation = _compare_eq_cls(left, right, highlighter, verbose)
|
explanation = _compare_eq_cls(left, right, highlighter, verbose)
|
||||||
elif issequence(left) and issequence(right):
|
elif issequence(left) and issequence(right):
|
||||||
explanation = _compare_eq_sequence(left, right, verbose)
|
explanation = _compare_eq_sequence(left, right, highlighter, verbose)
|
||||||
elif isset(left) and isset(right):
|
elif isset(left) and isset(right):
|
||||||
explanation = _compare_eq_set(left, right, verbose)
|
explanation = _compare_eq_set(left, right, highlighter, verbose)
|
||||||
elif isdict(left) and isdict(right):
|
elif isdict(left) and isdict(right):
|
||||||
explanation = _compare_eq_dict(left, right, verbose)
|
explanation = _compare_eq_dict(left, right, highlighter, verbose)
|
||||||
|
|
||||||
if isiterable(left) and isiterable(right):
|
if isiterable(left) and isiterable(right):
|
||||||
expl = _compare_eq_iterable(left, right, highlighter, verbose)
|
expl = _compare_eq_iterable(left, right, highlighter, verbose)
|
||||||
|
@ -318,18 +320,6 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
|
|
||||||
"""Move opening/closing parenthesis/bracket to own lines."""
|
|
||||||
opening = lines[0][:1]
|
|
||||||
if opening in ["(", "[", "{"]:
|
|
||||||
lines[0] = " " + lines[0][1:]
|
|
||||||
lines[:] = [opening] + lines
|
|
||||||
closing = lines[-1][-1:]
|
|
||||||
if closing in [")", "]", "}"]:
|
|
||||||
lines[-1] = lines[-1][:-1] + ","
|
|
||||||
lines[:] = lines + [closing]
|
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_iterable(
|
def _compare_eq_iterable(
|
||||||
left: Iterable[Any],
|
left: Iterable[Any],
|
||||||
right: Iterable[Any],
|
right: Iterable[Any],
|
||||||
|
@ -341,21 +331,10 @@ def _compare_eq_iterable(
|
||||||
# dynamic import to speedup pytest
|
# dynamic import to speedup pytest
|
||||||
import difflib
|
import difflib
|
||||||
|
|
||||||
left_formatting = pprint.pformat(left).splitlines()
|
left_formatting = PrettyPrinter().pformat(left).splitlines()
|
||||||
right_formatting = pprint.pformat(right).splitlines()
|
right_formatting = PrettyPrinter().pformat(right).splitlines()
|
||||||
|
|
||||||
# Re-format for different output lengths.
|
explanation = ["", "Full diff:"]
|
||||||
lines_left = len(left_formatting)
|
|
||||||
lines_right = len(right_formatting)
|
|
||||||
if lines_left != lines_right:
|
|
||||||
left_formatting = _pformat_dispatch(left).splitlines()
|
|
||||||
right_formatting = _pformat_dispatch(right).splitlines()
|
|
||||||
|
|
||||||
if lines_left > 1 or lines_right > 1:
|
|
||||||
_surrounding_parens_on_own_lines(left_formatting)
|
|
||||||
_surrounding_parens_on_own_lines(right_formatting)
|
|
||||||
|
|
||||||
explanation = ["Full diff:"]
|
|
||||||
# "right" is the expected base against which we compare "left",
|
# "right" is the expected base against which we compare "left",
|
||||||
# see https://github.com/pytest-dev/pytest/issues/3333
|
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||||
explanation.extend(
|
explanation.extend(
|
||||||
|
@ -371,7 +350,10 @@ def _compare_eq_iterable(
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_sequence(
|
def _compare_eq_sequence(
|
||||||
left: Sequence[Any], right: Sequence[Any], verbose: int = 0
|
left: Sequence[Any],
|
||||||
|
right: Sequence[Any],
|
||||||
|
highlighter: _HighlightFunc,
|
||||||
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
|
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
|
||||||
explanation: List[str] = []
|
explanation: List[str] = []
|
||||||
|
@ -394,7 +376,10 @@ def _compare_eq_sequence(
|
||||||
left_value = left[i]
|
left_value = left[i]
|
||||||
right_value = right[i]
|
right_value = right[i]
|
||||||
|
|
||||||
explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"]
|
explanation.append(
|
||||||
|
f"At index {i} diff:"
|
||||||
|
f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
if comparing_bytes:
|
if comparing_bytes:
|
||||||
|
@ -414,68 +399,91 @@ def _compare_eq_sequence(
|
||||||
extra = saferepr(right[len_left])
|
extra = saferepr(right[len_left])
|
||||||
|
|
||||||
if len_diff == 1:
|
if len_diff == 1:
|
||||||
explanation += [f"{dir_with_more} contains one more item: {extra}"]
|
explanation += [
|
||||||
|
f"{dir_with_more} contains one more item: {highlighter(extra)}"
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
explanation += [
|
explanation += [
|
||||||
"%s contains %d more items, first extra item: %s"
|
"%s contains %d more items, first extra item: %s"
|
||||||
% (dir_with_more, len_diff, extra)
|
% (dir_with_more, len_diff, highlighter(extra))
|
||||||
]
|
]
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_set(
|
def _compare_eq_set(
|
||||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
left: AbstractSet[Any],
|
||||||
|
right: AbstractSet[Any],
|
||||||
|
highlighter: _HighlightFunc,
|
||||||
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
explanation = []
|
explanation = []
|
||||||
explanation.extend(_set_one_sided_diff("left", left, right))
|
explanation.extend(_set_one_sided_diff("left", left, right, highlighter))
|
||||||
explanation.extend(_set_one_sided_diff("right", right, left))
|
explanation.extend(_set_one_sided_diff("right", right, left, highlighter))
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_gt_set(
|
def _compare_gt_set(
|
||||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
left: AbstractSet[Any],
|
||||||
|
right: AbstractSet[Any],
|
||||||
|
highlighter: _HighlightFunc,
|
||||||
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
explanation = _compare_gte_set(left, right, verbose)
|
explanation = _compare_gte_set(left, right, highlighter)
|
||||||
if not explanation:
|
if not explanation:
|
||||||
return ["Both sets are equal"]
|
return ["Both sets are equal"]
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_lt_set(
|
def _compare_lt_set(
|
||||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
left: AbstractSet[Any],
|
||||||
|
right: AbstractSet[Any],
|
||||||
|
highlighter: _HighlightFunc,
|
||||||
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
explanation = _compare_lte_set(left, right, verbose)
|
explanation = _compare_lte_set(left, right, highlighter)
|
||||||
if not explanation:
|
if not explanation:
|
||||||
return ["Both sets are equal"]
|
return ["Both sets are equal"]
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_gte_set(
|
def _compare_gte_set(
|
||||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
left: AbstractSet[Any],
|
||||||
|
right: AbstractSet[Any],
|
||||||
|
highlighter: _HighlightFunc,
|
||||||
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
return _set_one_sided_diff("right", right, left)
|
return _set_one_sided_diff("right", right, left, highlighter)
|
||||||
|
|
||||||
|
|
||||||
def _compare_lte_set(
|
def _compare_lte_set(
|
||||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
left: AbstractSet[Any],
|
||||||
|
right: AbstractSet[Any],
|
||||||
|
highlighter: _HighlightFunc,
|
||||||
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
return _set_one_sided_diff("left", left, right)
|
return _set_one_sided_diff("left", left, right, highlighter)
|
||||||
|
|
||||||
|
|
||||||
def _set_one_sided_diff(
|
def _set_one_sided_diff(
|
||||||
posn: str, set1: AbstractSet[Any], set2: AbstractSet[Any]
|
posn: str,
|
||||||
|
set1: AbstractSet[Any],
|
||||||
|
set2: AbstractSet[Any],
|
||||||
|
highlighter: _HighlightFunc,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
explanation = []
|
explanation = []
|
||||||
diff = set1 - set2
|
diff = set1 - set2
|
||||||
if diff:
|
if diff:
|
||||||
explanation.append(f"Extra items in the {posn} set:")
|
explanation.append(f"Extra items in the {posn} set:")
|
||||||
for item in diff:
|
for item in diff:
|
||||||
explanation.append(saferepr(item))
|
explanation.append(highlighter(saferepr(item)))
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_dict(
|
def _compare_eq_dict(
|
||||||
left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
|
left: Mapping[Any, Any],
|
||||||
|
right: Mapping[Any, Any],
|
||||||
|
highlighter: _HighlightFunc,
|
||||||
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
explanation: List[str] = []
|
explanation: List[str] = []
|
||||||
set_left = set(left)
|
set_left = set(left)
|
||||||
|
@ -486,12 +494,16 @@ def _compare_eq_dict(
|
||||||
explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
|
explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
|
||||||
elif same:
|
elif same:
|
||||||
explanation += ["Common items:"]
|
explanation += ["Common items:"]
|
||||||
explanation += pprint.pformat(same).splitlines()
|
explanation += highlighter(pprint.pformat(same)).splitlines()
|
||||||
diff = {k for k in common if left[k] != right[k]}
|
diff = {k for k in common if left[k] != right[k]}
|
||||||
if diff:
|
if diff:
|
||||||
explanation += ["Differing items:"]
|
explanation += ["Differing items:"]
|
||||||
for k in diff:
|
for k in diff:
|
||||||
explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})]
|
explanation += [
|
||||||
|
highlighter(saferepr({k: left[k]}))
|
||||||
|
+ " != "
|
||||||
|
+ highlighter(saferepr({k: right[k]}))
|
||||||
|
]
|
||||||
extra_left = set_left - set_right
|
extra_left = set_left - set_right
|
||||||
len_extra_left = len(extra_left)
|
len_extra_left = len(extra_left)
|
||||||
if len_extra_left:
|
if len_extra_left:
|
||||||
|
@ -500,7 +512,7 @@ def _compare_eq_dict(
|
||||||
% (len_extra_left, "" if len_extra_left == 1 else "s")
|
% (len_extra_left, "" if len_extra_left == 1 else "s")
|
||||||
)
|
)
|
||||||
explanation.extend(
|
explanation.extend(
|
||||||
pprint.pformat({k: left[k] for k in extra_left}).splitlines()
|
highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
|
||||||
)
|
)
|
||||||
extra_right = set_right - set_left
|
extra_right = set_right - set_left
|
||||||
len_extra_right = len(extra_right)
|
len_extra_right = len(extra_right)
|
||||||
|
@ -510,7 +522,7 @@ def _compare_eq_dict(
|
||||||
% (len_extra_right, "" if len_extra_right == 1 else "s")
|
% (len_extra_right, "" if len_extra_right == 1 else "s")
|
||||||
)
|
)
|
||||||
explanation.extend(
|
explanation.extend(
|
||||||
pprint.pformat({k: right[k] for k in extra_right}).splitlines()
|
highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines()
|
||||||
)
|
)
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
@ -549,17 +561,17 @@ def _compare_eq_cls(
|
||||||
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
|
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
|
||||||
elif same:
|
elif same:
|
||||||
explanation += ["Matching attributes:"]
|
explanation += ["Matching attributes:"]
|
||||||
explanation += pprint.pformat(same).splitlines()
|
explanation += highlighter(pprint.pformat(same)).splitlines()
|
||||||
if diff:
|
if diff:
|
||||||
explanation += ["Differing attributes:"]
|
explanation += ["Differing attributes:"]
|
||||||
explanation += pprint.pformat(diff).splitlines()
|
explanation += highlighter(pprint.pformat(diff)).splitlines()
|
||||||
for field in diff:
|
for field in diff:
|
||||||
field_left = getattr(left, field)
|
field_left = getattr(left, field)
|
||||||
field_right = getattr(right, field)
|
field_right = getattr(right, field)
|
||||||
explanation += [
|
explanation += [
|
||||||
"",
|
"",
|
||||||
"Drill down into differing attribute %s:" % field,
|
f"Drill down into differing attribute {field}:",
|
||||||
("%s%s: %r != %r") % (indent, field, field_left, field_right),
|
f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}",
|
||||||
]
|
]
|
||||||
explanation += [
|
explanation += [
|
||||||
indent + line
|
indent + line
|
||||||
|
|
|
@ -588,7 +588,7 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class CaptureResult(NamedTuple, Generic[AnyStr]):
|
class CaptureResult(NamedTuple, Generic[AnyStr]):
|
||||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||||
|
|
||||||
out: AnyStr
|
out: AnyStr
|
||||||
err: AnyStr
|
err: AnyStr
|
||||||
|
@ -598,7 +598,7 @@ else:
|
||||||
class CaptureResult(
|
class CaptureResult(
|
||||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||||
):
|
):
|
||||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import Final
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
@ -69,7 +70,7 @@ from _pytest.warning_types import warn_explicit_for
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest._code.code import _TracebackStyle
|
from _pytest._code.code import _TracebackStyle
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
from .argparsing import Argument
|
from .argparsing import Argument, Parser
|
||||||
|
|
||||||
|
|
||||||
_PluggyPlugin = object
|
_PluggyPlugin = object
|
||||||
|
@ -448,6 +449,7 @@ class PytestPluginManager(PluginManager):
|
||||||
def parse_hookimpl_opts(
|
def parse_hookimpl_opts(
|
||||||
self, plugin: _PluggyPlugin, name: str
|
self, plugin: _PluggyPlugin, name: str
|
||||||
) -> Optional[HookimplOpts]:
|
) -> Optional[HookimplOpts]:
|
||||||
|
""":meta private:"""
|
||||||
# pytest hooks are always prefixed with "pytest_",
|
# pytest hooks are always prefixed with "pytest_",
|
||||||
# so we avoid accessing possibly non-readable attributes
|
# so we avoid accessing possibly non-readable attributes
|
||||||
# (see issue #1073).
|
# (see issue #1073).
|
||||||
|
@ -471,6 +473,7 @@ class PytestPluginManager(PluginManager):
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
|
def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
|
||||||
|
""":meta private:"""
|
||||||
opts = super().parse_hookspec_opts(module_or_class, name)
|
opts = super().parse_hookspec_opts(module_or_class, name)
|
||||||
if opts is None:
|
if opts is None:
|
||||||
method = getattr(module_or_class, name)
|
method = getattr(module_or_class, name)
|
||||||
|
@ -1495,6 +1498,27 @@ class Config:
|
||||||
def getini(self, name: str):
|
def getini(self, name: str):
|
||||||
"""Return configuration value from an :ref:`ini file <configfiles>`.
|
"""Return configuration value from an :ref:`ini file <configfiles>`.
|
||||||
|
|
||||||
|
If a configuration value is not defined in an
|
||||||
|
:ref:`ini file <configfiles>`, then the ``default`` value provided while
|
||||||
|
registering the configuration through
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>` will be returned.
|
||||||
|
Please note that you can even provide ``None`` as a valid
|
||||||
|
default value.
|
||||||
|
|
||||||
|
If ``default`` is not provided while registering using
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>`, then a default value
|
||||||
|
based on the ``type`` parameter passed to
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>` will be returned.
|
||||||
|
The default values based on ``type`` are:
|
||||||
|
``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
|
||||||
|
``bool`` : ``False``
|
||||||
|
``string`` : empty string ``""``
|
||||||
|
|
||||||
|
If neither the ``default`` nor the ``type`` parameter is passed
|
||||||
|
while registering the configuration through
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>`, then the configuration
|
||||||
|
is treated as a string and a default empty string '' is returned.
|
||||||
|
|
||||||
If the specified name hasn't been registered through a prior
|
If the specified name hasn't been registered through a prior
|
||||||
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
|
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
|
||||||
plugin), a ValueError is raised.
|
plugin), a ValueError is raised.
|
||||||
|
@ -1521,11 +1545,7 @@ class Config:
|
||||||
try:
|
try:
|
||||||
value = self.inicfg[name]
|
value = self.inicfg[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if default is not None:
|
|
||||||
return default
|
return default
|
||||||
if type is None:
|
|
||||||
return ""
|
|
||||||
return []
|
|
||||||
else:
|
else:
|
||||||
value = override_value
|
value = override_value
|
||||||
# Coerce the values based on types.
|
# Coerce the values based on types.
|
||||||
|
@ -1633,6 +1653,78 @@ class Config:
|
||||||
"""Deprecated, use getoption(skip=True) instead."""
|
"""Deprecated, use getoption(skip=True) instead."""
|
||||||
return self.getoption(name, skip=True)
|
return self.getoption(name, skip=True)
|
||||||
|
|
||||||
|
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
|
||||||
|
VERBOSITY_ASSERTIONS: Final = "assertions"
|
||||||
|
_VERBOSITY_INI_DEFAULT: Final = "auto"
|
||||||
|
|
||||||
|
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||||
|
r"""Retrieve the verbosity level for a fine-grained verbosity type.
|
||||||
|
|
||||||
|
:param verbosity_type: Verbosity type to get level for. If a level is
|
||||||
|
configured for the given type, that value will be returned. If the
|
||||||
|
given type is not a known verbosity type, the global verbosity
|
||||||
|
level will be returned. If the given type is None (default), the
|
||||||
|
global verbosity level will be returned.
|
||||||
|
|
||||||
|
To configure a level for a fine-grained verbosity type, the
|
||||||
|
configuration file should have a setting for the configuration name
|
||||||
|
and a numeric value for the verbosity level. A special value of "auto"
|
||||||
|
can be used to explicitly use the global verbosity level.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
# content of pytest.ini
|
||||||
|
[pytest]
|
||||||
|
verbosity_assertions = 2
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest -v
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(config.get_verbosity()) # 1
|
||||||
|
print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
|
||||||
|
"""
|
||||||
|
global_level = self.option.verbose
|
||||||
|
assert isinstance(global_level, int)
|
||||||
|
if verbosity_type is None:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
ini_name = Config._verbosity_ini_name(verbosity_type)
|
||||||
|
if ini_name not in self._parser._inidict:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
level = self.getini(ini_name)
|
||||||
|
if level == Config._VERBOSITY_INI_DEFAULT:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
return int(level)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _verbosity_ini_name(verbosity_type: str) -> str:
|
||||||
|
return f"verbosity_{verbosity_type}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_verbosity_ini(parser: "Parser", verbosity_type: str, help: str) -> None:
|
||||||
|
"""Add a output verbosity configuration option for the given output type.
|
||||||
|
|
||||||
|
:param parser: Parser for command line arguments and ini-file values.
|
||||||
|
:param verbosity_type: Fine-grained verbosity category.
|
||||||
|
:param help: Description of the output this type controls.
|
||||||
|
|
||||||
|
The value should be retrieved via a call to
|
||||||
|
:py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
|
||||||
|
"""
|
||||||
|
parser.addini(
|
||||||
|
Config._verbosity_ini_name(verbosity_type),
|
||||||
|
help=help,
|
||||||
|
type="string",
|
||||||
|
default=Config._VERBOSITY_INI_DEFAULT,
|
||||||
|
)
|
||||||
|
|
||||||
def _warn_about_missing_assertion(self, mode: str) -> None:
|
def _warn_about_missing_assertion(self, mode: str) -> None:
|
||||||
if not _assertion_supported():
|
if not _assertion_supported():
|
||||||
if mode == "plain":
|
if mode == "plain":
|
||||||
|
|
|
@ -27,6 +27,14 @@ from _pytest.deprecated import check_ispytest
|
||||||
FILE_OR_DIR = "file_or_dir"
|
FILE_OR_DIR = "file_or_dir"
|
||||||
|
|
||||||
|
|
||||||
|
class NotSet:
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "<notset>"
|
||||||
|
|
||||||
|
|
||||||
|
NOT_SET = NotSet()
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class Parser:
|
class Parser:
|
||||||
"""Parser for command line arguments and ini-file values.
|
"""Parser for command line arguments and ini-file values.
|
||||||
|
@ -90,7 +98,7 @@ class Parser:
|
||||||
:param opts:
|
:param opts:
|
||||||
Option names, can be short or long options.
|
Option names, can be short or long options.
|
||||||
:param attrs:
|
:param attrs:
|
||||||
Same attributes as the argparse library's :py:func:`add_argument()
|
Same attributes as the argparse library's :meth:`add_argument()
|
||||||
<argparse.ArgumentParser.add_argument>` function accepts.
|
<argparse.ArgumentParser.add_argument>` function accepts.
|
||||||
|
|
||||||
After command line parsing, options are available on the pytest config
|
After command line parsing, options are available on the pytest config
|
||||||
|
@ -176,7 +184,7 @@ class Parser:
|
||||||
type: Optional[
|
type: Optional[
|
||||||
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
||||||
] = None,
|
] = None,
|
||||||
default: Any = None,
|
default: Any = NOT_SET,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register an ini-file option.
|
"""Register an ini-file option.
|
||||||
|
|
||||||
|
@ -203,10 +211,30 @@ class Parser:
|
||||||
:py:func:`config.getini(name) <pytest.Config.getini>`.
|
:py:func:`config.getini(name) <pytest.Config.getini>`.
|
||||||
"""
|
"""
|
||||||
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
|
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
|
||||||
|
if default is NOT_SET:
|
||||||
|
default = get_ini_default_for_type(type)
|
||||||
|
|
||||||
self._inidict[name] = (help, type, default)
|
self._inidict[name] = (help, type, default)
|
||||||
self._ininames.append(name)
|
self._ininames.append(name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ini_default_for_type(
|
||||||
|
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Used by addini to get the default value for a given ini-option type, when
|
||||||
|
default is not supplied.
|
||||||
|
"""
|
||||||
|
if type is None:
|
||||||
|
return ""
|
||||||
|
elif type in ("paths", "pathlist", "args", "linelist"):
|
||||||
|
return []
|
||||||
|
elif type == "bool":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class ArgumentError(Exception):
|
class ArgumentError(Exception):
|
||||||
"""Raised if an Argument instance is created with invalid or
|
"""Raised if an Argument instance is created with invalid or
|
||||||
inconsistent arguments."""
|
inconsistent arguments."""
|
||||||
|
@ -372,7 +400,7 @@ class OptionGroup:
|
||||||
:param opts:
|
:param opts:
|
||||||
Option names, can be short or long options.
|
Option names, can be short or long options.
|
||||||
:param attrs:
|
:param attrs:
|
||||||
Same attributes as the argparse library's :py:func:`add_argument()
|
Same attributes as the argparse library's :meth:`add_argument()
|
||||||
<argparse.ArgumentParser.add_argument>` function accepts.
|
<argparse.ArgumentParser.add_argument>` function accepts.
|
||||||
"""
|
"""
|
||||||
conflict = set(opts).intersection(
|
conflict = set(opts).intersection(
|
||||||
|
|
|
@ -9,8 +9,8 @@ from _pytest.nodes import Item
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
|
||||||
|
fault_handler_original_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_stderr_fd_key = StashKey[int]()
|
fault_handler_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_originally_enabled_key = StashKey[bool]()
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
|
@ -24,8 +24,15 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
# at teardown we want to restore the original faulthandler fileno
|
||||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
# but faulthandler has no api to return the original fileno
|
||||||
|
# so here we stash the stderr fileno to be used at teardown
|
||||||
|
# sys.stderr and sys.__stderr__ may be closed or patched during the session
|
||||||
|
# so we can't rely on their values being good at that point (#11572).
|
||||||
|
stderr_fileno = get_stderr_fileno()
|
||||||
|
if faulthandler.is_enabled():
|
||||||
|
config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
|
||||||
|
config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
|
||||||
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,9 +44,10 @@ def pytest_unconfigure(config: Config) -> None:
|
||||||
if fault_handler_stderr_fd_key in config.stash:
|
if fault_handler_stderr_fd_key in config.stash:
|
||||||
os.close(config.stash[fault_handler_stderr_fd_key])
|
os.close(config.stash[fault_handler_stderr_fd_key])
|
||||||
del config.stash[fault_handler_stderr_fd_key]
|
del config.stash[fault_handler_stderr_fd_key]
|
||||||
if config.stash.get(fault_handler_originally_enabled_key, False):
|
|
||||||
# Re-enable the faulthandler if it was originally enabled.
|
# Re-enable the faulthandler if it was originally enabled.
|
||||||
faulthandler.enable(file=get_stderr_fileno())
|
if fault_handler_original_stderr_fd_key in config.stash:
|
||||||
|
faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
|
||||||
|
del config.stash[fault_handler_original_stderr_fd_key]
|
||||||
|
|
||||||
|
|
||||||
def get_stderr_fileno() -> int:
|
def get_stderr_fileno() -> int:
|
||||||
|
|
|
@ -135,7 +135,9 @@ def get_scope_node(
|
||||||
import _pytest.python
|
import _pytest.python
|
||||||
|
|
||||||
if scope is Scope.Function:
|
if scope is Scope.Function:
|
||||||
return node.getparent(nodes.Item)
|
# Type ignored because this is actually safe, see:
|
||||||
|
# https://github.com/python/mypy/issues/4717
|
||||||
|
return node.getparent(nodes.Item) # type: ignore[type-abstract]
|
||||||
elif scope is Scope.Class:
|
elif scope is Scope.Class:
|
||||||
return node.getparent(_pytest.python.Class)
|
return node.getparent(_pytest.python.Class)
|
||||||
elif scope is Scope.Module:
|
elif scope is Scope.Module:
|
||||||
|
|
|
@ -55,7 +55,7 @@ hookspec = HookspecMarker("pytest")
|
||||||
@hookspec(historic=True)
|
@hookspec(historic=True)
|
||||||
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
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)``.
|
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
|
||||||
|
|
||||||
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
||||||
|
|
||||||
|
@ -96,8 +96,8 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||||
<pytest.Parser.addini>`.
|
<pytest.Parser.addini>`.
|
||||||
|
|
||||||
:param pytest.PytestPluginManager pluginmanager:
|
:param pytest.PytestPluginManager pluginmanager:
|
||||||
The pytest plugin manager, which can be used to install :py:func:`hookspec`'s
|
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
|
||||||
or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks
|
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
|
||||||
to change how command line options are added.
|
to change how command line options are added.
|
||||||
|
|
||||||
Options can later be accessed through the
|
Options can later be accessed through the
|
||||||
|
@ -858,8 +858,8 @@ def pytest_warning_recorded(
|
||||||
"""Process a warning captured by the internal pytest warnings plugin.
|
"""Process a warning captured by the internal pytest warnings plugin.
|
||||||
|
|
||||||
:param warning_message:
|
:param warning_message:
|
||||||
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
|
The captured warning. This is the same object produced by :class:`warnings.catch_warnings`,
|
||||||
the same attributes as the parameters of :py:func:`warnings.showwarning`.
|
and contains the same attributes as the parameters of :py:func:`warnings.showwarning`.
|
||||||
|
|
||||||
:param when:
|
:param when:
|
||||||
Indicates when the warning was captured. Possible values:
|
Indicates when the warning was captured. Possible values:
|
||||||
|
@ -940,10 +940,10 @@ def pytest_exception_interact(
|
||||||
interactively handled.
|
interactively handled.
|
||||||
|
|
||||||
May be called during collection (see :hook:`pytest_make_collect_report`),
|
May be called during collection (see :hook:`pytest_make_collect_report`),
|
||||||
in which case ``report`` is a :class:`CollectReport`.
|
in which case ``report`` is a :class:`~pytest.CollectReport`.
|
||||||
|
|
||||||
May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
|
May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
|
||||||
in which case ``report`` is a :class:`TestReport`.
|
in which case ``report`` is a :class:`~pytest.TestReport`.
|
||||||
|
|
||||||
This hook is not called if the exception that was raised is an internal
|
This hook is not called if the exception that was raised is an internal
|
||||||
exception like ``skip.Exception``.
|
exception like ``skip.Exception``.
|
||||||
|
|
|
@ -88,7 +88,6 @@ class Testdir:
|
||||||
return self._pytester.chdir()
|
return self._pytester.chdir()
|
||||||
|
|
||||||
def finalize(self) -> None:
|
def finalize(self) -> None:
|
||||||
"""See :meth:`Pytester._finalize`."""
|
|
||||||
return self._pytester._finalize()
|
return self._pytester._finalize()
|
||||||
|
|
||||||
def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
|
def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
|
||||||
|
@ -269,7 +268,7 @@ class LegacyTestdirPlugin:
|
||||||
@final
|
@final
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class TempdirFactory:
|
class TempdirFactory:
|
||||||
"""Backward compatibility wrapper that implements :class:`py.path.local`
|
"""Backward compatibility wrapper that implements ``py.path.local``
|
||||||
for :class:`TempPathFactory`.
|
for :class:`TempPathFactory`.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
@ -288,11 +287,11 @@ class TempdirFactory:
|
||||||
self._tmppath_factory = tmppath_factory
|
self._tmppath_factory = tmppath_factory
|
||||||
|
|
||||||
def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
|
def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
|
||||||
"""Same as :meth:`TempPathFactory.mktemp`, but returns a :class:`py.path.local` object."""
|
"""Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object."""
|
||||||
return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
|
return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
|
||||||
|
|
||||||
def getbasetemp(self) -> LEGACY_PATH:
|
def getbasetemp(self) -> LEGACY_PATH:
|
||||||
"""Same as :meth:`TempPathFactory.getbasetemp`, but returns a :class:`py.path.local` object."""
|
"""Same as :meth:`TempPathFactory.getbasetemp`, but returns a ``py.path.local`` object."""
|
||||||
return legacy_path(self._tmppath_factory.getbasetemp().resolve())
|
return legacy_path(self._tmppath_factory.getbasetemp().resolve())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,15 +11,18 @@ from datetime import timezone
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from logging import LogRecord
|
from logging import LogRecord
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import TracebackType
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
from typing import Generic
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
@ -62,7 +65,7 @@ class DatetimeFormatter(logging.Formatter):
|
||||||
:func:`time.strftime` in case of microseconds in format string.
|
:func:`time.strftime` in case of microseconds in format string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def formatTime(self, record: LogRecord, datefmt=None) -> str:
|
def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str:
|
||||||
if datefmt and "%f" in datefmt:
|
if datefmt and "%f" in datefmt:
|
||||||
ct = self.converter(record.created)
|
ct = self.converter(record.created)
|
||||||
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
||||||
|
@ -331,7 +334,7 @@ _HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
|
||||||
|
|
||||||
|
|
||||||
# Not using @contextmanager for performance reasons.
|
# Not using @contextmanager for performance reasons.
|
||||||
class catching_logs:
|
class catching_logs(Generic[_HandlerType]):
|
||||||
"""Context manager that prepares the whole logging machinery properly."""
|
"""Context manager that prepares the whole logging machinery properly."""
|
||||||
|
|
||||||
__slots__ = ("handler", "level", "orig_level")
|
__slots__ = ("handler", "level", "orig_level")
|
||||||
|
@ -340,7 +343,7 @@ class catching_logs:
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
self.level = level
|
self.level = level
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> _HandlerType:
|
||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
if self.level is not None:
|
if self.level is not None:
|
||||||
self.handler.setLevel(self.level)
|
self.handler.setLevel(self.level)
|
||||||
|
@ -350,7 +353,12 @@ class catching_logs:
|
||||||
root_logger.setLevel(min(self.orig_level, self.level))
|
root_logger.setLevel(min(self.orig_level, self.level))
|
||||||
return self.handler
|
return self.handler
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Optional[Type[BaseException]],
|
||||||
|
exc_val: Optional[BaseException],
|
||||||
|
exc_tb: Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
if self.level is not None:
|
if self.level is not None:
|
||||||
root_logger.setLevel(self.orig_level)
|
root_logger.setLevel(self.orig_level)
|
||||||
|
@ -421,7 +429,7 @@ class LogCaptureFixture:
|
||||||
return self._item.stash[caplog_handler_key]
|
return self._item.stash[caplog_handler_key]
|
||||||
|
|
||||||
def get_records(
|
def get_records(
|
||||||
self, when: "Literal['setup', 'call', 'teardown']"
|
self, when: Literal["setup", "call", "teardown"]
|
||||||
) -> List[logging.LogRecord]:
|
) -> 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.
|
||||||
|
|
||||||
|
@ -522,7 +530,7 @@ class LogCaptureFixture:
|
||||||
The levels of the loggers changed by this function will be
|
The levels of the loggers changed by this function will be
|
||||||
restored to their initial values at the end of the test.
|
restored to their initial values at the end of the test.
|
||||||
|
|
||||||
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
Will enable the requested logging level if it was disabled via :func:`logging.disable`.
|
||||||
|
|
||||||
:param level: The level.
|
:param level: The level.
|
||||||
:param logger: The logger to update. If not given, the root logger.
|
:param logger: The logger to update. If not given, the root logger.
|
||||||
|
@ -546,7 +554,7 @@ class LogCaptureFixture:
|
||||||
the end of the 'with' statement the level is restored to its original
|
the end of the 'with' statement the level is restored to its original
|
||||||
value.
|
value.
|
||||||
|
|
||||||
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
Will enable the requested logging level if it was disabled via :func:`logging.disable`.
|
||||||
|
|
||||||
:param level: The level.
|
:param level: The level.
|
||||||
:param logger: The logger to update. If not given, the root logger.
|
:param logger: The logger to update. If not given, the root logger.
|
||||||
|
@ -564,6 +572,22 @@ class LogCaptureFixture:
|
||||||
self.handler.setLevel(handler_orig_level)
|
self.handler.setLevel(handler_orig_level)
|
||||||
logging.disable(original_disable_level)
|
logging.disable(original_disable_level)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def filtering(self, filter_: logging.Filter) -> Generator[None, None, None]:
|
||||||
|
"""Context manager that temporarily adds the given filter to the caplog's
|
||||||
|
:meth:`handler` for the 'with' statement block, and removes that filter at the
|
||||||
|
end of the block.
|
||||||
|
|
||||||
|
:param filter_: A custom :class:`logging.Filter` object.
|
||||||
|
|
||||||
|
.. versionadded:: 7.5
|
||||||
|
"""
|
||||||
|
self.handler.addFilter(filter_)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.handler.removeFilter(filter_)
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
|
def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
|
||||||
|
@ -726,7 +750,7 @@ class LoggingPlugin:
|
||||||
if old_stream:
|
if old_stream:
|
||||||
old_stream.close()
|
old_stream.close()
|
||||||
|
|
||||||
def _log_cli_enabled(self):
|
def _log_cli_enabled(self) -> bool:
|
||||||
"""Return whether live logging is enabled."""
|
"""Return whether live logging is enabled."""
|
||||||
enabled = self._config.getoption(
|
enabled = self._config.getoption(
|
||||||
"--log-cli-level"
|
"--log-cli-level"
|
||||||
|
|
|
@ -457,7 +457,7 @@ if TYPE_CHECKING:
|
||||||
@overload
|
@overload
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
condition: Union[str, bool] = ...,
|
condition: Union[str, bool] = False,
|
||||||
*conditions: Union[str, bool],
|
*conditions: Union[str, bool],
|
||||||
reason: str = ...,
|
reason: str = ...,
|
||||||
run: bool = ...,
|
run: bool = ...,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import abc
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import warnings
|
import warnings
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
|
@ -121,7 +123,7 @@ def _imply_path(
|
||||||
_NodeType = TypeVar("_NodeType", bound="Node")
|
_NodeType = TypeVar("_NodeType", bound="Node")
|
||||||
|
|
||||||
|
|
||||||
class NodeMeta(type):
|
class NodeMeta(abc.ABCMeta):
|
||||||
"""Metaclass used by :class:`Node` to enforce that direct construction raises
|
"""Metaclass used by :class:`Node` to enforce that direct construction raises
|
||||||
:class:`Failed`.
|
:class:`Failed`.
|
||||||
|
|
||||||
|
@ -165,7 +167,7 @@ class NodeMeta(type):
|
||||||
return super().__call__(*k, **known_kw)
|
return super().__call__(*k, **known_kw)
|
||||||
|
|
||||||
|
|
||||||
class Node(metaclass=NodeMeta):
|
class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
r"""Base class of :class:`Collector` and :class:`Item`, the components of
|
r"""Base class of :class:`Collector` and :class:`Item`, the components of
|
||||||
the test collection tree.
|
the test collection tree.
|
||||||
|
|
||||||
|
@ -176,8 +178,8 @@ class Node(metaclass=NodeMeta):
|
||||||
# Implemented in the legacypath plugin.
|
# Implemented in the legacypath plugin.
|
||||||
#: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
|
#: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
|
||||||
#: for methods not migrated to ``pathlib.Path`` yet, such as
|
#: for methods not migrated to ``pathlib.Path`` yet, such as
|
||||||
#: :meth:`Item.reportinfo`. Will be deprecated in a future release, prefer
|
#: :meth:`Item.reportinfo <pytest.Item.reportinfo>`. Will be deprecated in
|
||||||
#: using :attr:`path` instead.
|
#: a future release, prefer using :attr:`path` instead.
|
||||||
fspath: LEGACY_PATH
|
fspath: LEGACY_PATH
|
||||||
|
|
||||||
# Use __slots__ to make attribute access faster.
|
# Use __slots__ to make attribute access faster.
|
||||||
|
@ -228,7 +230,7 @@ class Node(metaclass=NodeMeta):
|
||||||
if path is None and fspath is None:
|
if path is None and fspath is None:
|
||||||
path = getattr(parent, "path", None)
|
path = getattr(parent, "path", None)
|
||||||
#: Filesystem path where this node was collected from (can be None).
|
#: Filesystem path where this node was collected from (can be None).
|
||||||
self.path: Path = _imply_path(type(self), path, fspath=fspath)
|
self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath)
|
||||||
|
|
||||||
# The explicit annotation is to avoid publicly exposing NodeKeywords.
|
# The explicit annotation is to avoid publicly exposing NodeKeywords.
|
||||||
#: Keywords/markers collected from all scopes.
|
#: Keywords/markers collected from all scopes.
|
||||||
|
@ -534,7 +536,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
|
||||||
return getattr(node, "fspath", "unknown location"), -1
|
return getattr(node, "fspath", "unknown location"), -1
|
||||||
|
|
||||||
|
|
||||||
class Collector(Node):
|
class Collector(Node, abc.ABC):
|
||||||
"""Base class of all collectors.
|
"""Base class of all collectors.
|
||||||
|
|
||||||
Collector create children through `collect()` and thus iteratively build
|
Collector create children through `collect()` and thus iteratively build
|
||||||
|
@ -544,6 +546,7 @@ class Collector(Node):
|
||||||
class CollectError(Exception):
|
class CollectError(Exception):
|
||||||
"""An error during collection, contains a custom message."""
|
"""An error during collection, contains a custom message."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
def collect(self) -> Iterable[Union["Item", "Collector"]]:
|
def collect(self) -> Iterable[Union["Item", "Collector"]]:
|
||||||
"""Collect children (items and collectors) for this collector."""
|
"""Collect children (items and collectors) for this collector."""
|
||||||
raise NotImplementedError("abstract")
|
raise NotImplementedError("abstract")
|
||||||
|
@ -588,7 +591,7 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class FSCollector(Collector):
|
class FSCollector(Collector, abc.ABC):
|
||||||
"""Base class for filesystem collectors."""
|
"""Base class for filesystem collectors."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -666,14 +669,14 @@ class FSCollector(Collector):
|
||||||
return self.session.isinitpath(path)
|
return self.session.isinitpath(path)
|
||||||
|
|
||||||
|
|
||||||
class File(FSCollector):
|
class File(FSCollector, abc.ABC):
|
||||||
"""Base class for collecting tests from a file.
|
"""Base class for collecting tests from a file.
|
||||||
|
|
||||||
:ref:`non-python tests`.
|
:ref:`non-python tests`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Item(Node):
|
class Item(Node, abc.ABC):
|
||||||
"""Base class of all test invocation items.
|
"""Base class of all test invocation items.
|
||||||
|
|
||||||
Note that for a single function there might be multiple test invocation items.
|
Note that for a single function there might be multiple test invocation items.
|
||||||
|
@ -739,6 +742,7 @@ class Item(Node):
|
||||||
PytestWarning,
|
PytestWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
"""Run the test case for this item.
|
"""Run the test case for this item.
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ def exit(
|
||||||
only because `msg` is deprecated.
|
only because `msg` is deprecated.
|
||||||
|
|
||||||
:param returncode:
|
:param returncode:
|
||||||
Return code to be used when exiting pytest.
|
Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
|
||||||
|
|
||||||
:param msg:
|
:param msg:
|
||||||
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
||||||
|
|
|
@ -121,13 +121,18 @@ def pytest_configure(config: Config) -> None:
|
||||||
|
|
||||||
class LsofFdLeakChecker:
|
class LsofFdLeakChecker:
|
||||||
def get_open_files(self) -> List[Tuple[str, str]]:
|
def get_open_files(self) -> List[Tuple[str, str]]:
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
# New in Python 3.11, ignores utf-8 mode
|
||||||
|
encoding = locale.getencoding()
|
||||||
|
else:
|
||||||
|
encoding = locale.getpreferredencoding(False)
|
||||||
out = subprocess.run(
|
out = subprocess.run(
|
||||||
("lsof", "-Ffn0", "-p", str(os.getpid())),
|
("lsof", "-Ffn0", "-p", str(os.getpid())),
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
check=True,
|
check=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding=locale.getpreferredencoding(False),
|
encoding=encoding,
|
||||||
).stdout
|
).stdout
|
||||||
|
|
||||||
def isopen(line: str) -> bool:
|
def isopen(line: str) -> bool:
|
||||||
|
@ -1039,7 +1044,7 @@ class Pytester:
|
||||||
The calling test instance (class containing the test method) must
|
The calling test instance (class containing the test method) must
|
||||||
provide a ``.getrunner()`` method which should return a runner which
|
provide a ``.getrunner()`` method which should return a runner which
|
||||||
can run the test protocol for a single item, e.g.
|
can run the test protocol for a single item, e.g.
|
||||||
:py:func:`_pytest.runner.runtestprotocol`.
|
``_pytest.runner.runtestprotocol``.
|
||||||
"""
|
"""
|
||||||
# used from runner functional tests
|
# used from runner functional tests
|
||||||
item = self.getitem(source)
|
item = self.getitem(source)
|
||||||
|
@ -1390,7 +1395,7 @@ class Pytester:
|
||||||
:param stdin:
|
:param stdin:
|
||||||
Optional standard input.
|
Optional standard input.
|
||||||
|
|
||||||
- If it is :py:attr:`CLOSE_STDIN` (Default), then this method calls
|
- If it is ``CLOSE_STDIN`` (Default), then this method calls
|
||||||
:py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and
|
:py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and
|
||||||
the standard input is closed immediately after the new command is
|
the standard input is closed immediately after the new command is
|
||||||
started.
|
started.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Python test discovery, setup and run of test functions."""
|
"""Python test discovery, setup and run of test functions."""
|
||||||
|
import abc
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
@ -380,7 +381,7 @@ del _EmptyClass
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
class PyCollector(PyobjMixin, nodes.Collector):
|
class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
|
||||||
def funcnamefilter(self, name: str) -> bool:
|
def funcnamefilter(self, name: str) -> bool:
|
||||||
return self._matches_prefix_or_glob_option("python_functions", name)
|
return self._matches_prefix_or_glob_option("python_functions", name)
|
||||||
|
|
||||||
|
|
|
@ -30,15 +30,6 @@ if TYPE_CHECKING:
|
||||||
from numpy import ndarray
|
from numpy import ndarray
|
||||||
|
|
||||||
|
|
||||||
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
|
|
||||||
at_str = f" at {at}" if at else ""
|
|
||||||
return TypeError(
|
|
||||||
"cannot make approximate comparisons to non-numeric values: {!r} {}".format(
|
|
||||||
value, at_str
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _compare_approx(
|
def _compare_approx(
|
||||||
full_object: object,
|
full_object: object,
|
||||||
message_data: Sequence[Tuple[str, str, str]],
|
message_data: Sequence[Tuple[str, str, str]],
|
||||||
|
@ -804,35 +795,35 @@ def raises( # noqa: F811
|
||||||
def raises( # noqa: F811
|
def raises( # noqa: F811
|
||||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
|
expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
|
||||||
) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
|
) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
|
||||||
r"""Assert that a code block/function call raises an exception.
|
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
|
||||||
|
|
||||||
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
|
:param expected_exception:
|
||||||
The expected exception type, or a tuple if one of multiple possible
|
The expected exception type, or a tuple if one of multiple possible
|
||||||
exception types are expected.
|
exception types are expected. Note that subclasses of the passed exceptions
|
||||||
:kwparam str | typing.Pattern[str] | None match:
|
will also match.
|
||||||
|
|
||||||
|
:kwparam str | re.Pattern[str] | None match:
|
||||||
If specified, a string containing a regular expression,
|
If specified, a string containing a regular expression,
|
||||||
or a regular expression object, that is tested against the string
|
or a regular expression object, that is tested against the string
|
||||||
representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
|
representation of the exception and its :pep:`678` `__notes__`
|
||||||
using :func:`re.search`.
|
using :func:`re.search`.
|
||||||
|
|
||||||
To match a literal string that may contain :ref:`special characters
|
To match a literal string that may contain :ref:`special characters
|
||||||
<re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
|
<re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
|
||||||
|
|
||||||
(This is only used when :py:func:`pytest.raises` is used as a context manager,
|
(This is only used when ``pytest.raises`` is used as a context manager,
|
||||||
and passed through to the function otherwise.
|
and passed through to the function otherwise.
|
||||||
When using :py:func:`pytest.raises` as a function, you can use:
|
When using ``pytest.raises`` as a function, you can use:
|
||||||
``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
|
``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
|
||||||
|
|
||||||
.. currentmodule:: _pytest._code
|
|
||||||
|
|
||||||
Use ``pytest.raises`` as a context manager, which will capture the exception of the given
|
Use ``pytest.raises`` as a context manager, which will capture the exception of the given
|
||||||
type::
|
type, or any of its subclasses::
|
||||||
|
|
||||||
>>> import pytest
|
>>> import pytest
|
||||||
>>> with pytest.raises(ZeroDivisionError):
|
>>> with pytest.raises(ZeroDivisionError):
|
||||||
... 1/0
|
... 1/0
|
||||||
|
|
||||||
If the code block does not raise the expected exception (``ZeroDivisionError`` in the example
|
If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example
|
||||||
above), or no exception at all, the check will fail instead.
|
above), or no exception at all, the check will fail instead.
|
||||||
|
|
||||||
You can also use the keyword argument ``match`` to assert that the
|
You can also use the keyword argument ``match`` to assert that the
|
||||||
|
@ -845,7 +836,7 @@ def raises( # noqa: F811
|
||||||
... raise ValueError("value must be 42")
|
... raise ValueError("value must be 42")
|
||||||
|
|
||||||
The ``match`` argument searches the formatted exception string, which includes any
|
The ``match`` argument searches the formatted exception string, which includes any
|
||||||
`PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``:
|
`PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``:
|
||||||
|
|
||||||
>>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP
|
>>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP
|
||||||
... e = ValueError("value must be 42")
|
... e = ValueError("value must be 42")
|
||||||
|
@ -860,6 +851,20 @@ def raises( # noqa: F811
|
||||||
>>> assert exc_info.type is ValueError
|
>>> assert exc_info.type is ValueError
|
||||||
>>> assert exc_info.value.args[0] == "value must be 42"
|
>>> assert exc_info.value.args[0] == "value must be 42"
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this::
|
||||||
|
|
||||||
|
with pytest.raises(Exception): # Careful, this will catch ANY exception raised.
|
||||||
|
some_function()
|
||||||
|
|
||||||
|
Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide
|
||||||
|
real bugs, where the user wrote this expecting a specific exception, but some other exception is being
|
||||||
|
raised due to a bug introduced during a refactoring.
|
||||||
|
|
||||||
|
Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch
|
||||||
|
**any** exception raised.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
When using ``pytest.raises`` as a context manager, it's worthwhile to
|
When using ``pytest.raises`` as a context manager, it's worthwhile to
|
||||||
|
@ -872,7 +877,7 @@ def raises( # noqa: F811
|
||||||
>>> with pytest.raises(ValueError) as exc_info:
|
>>> with pytest.raises(ValueError) as exc_info:
|
||||||
... if value > 10:
|
... if value > 10:
|
||||||
... raise ValueError("value must be <= 10")
|
... raise ValueError("value must be <= 10")
|
||||||
... assert exc_info.type is ValueError # this will not execute
|
... assert exc_info.type is ValueError # This will not execute.
|
||||||
|
|
||||||
Instead, the following approach must be taken (note the difference in
|
Instead, the following approach must be taken (note the difference in
|
||||||
scope)::
|
scope)::
|
||||||
|
@ -891,6 +896,10 @@ def raises( # noqa: F811
|
||||||
|
|
||||||
See :ref:`parametrizing_conditional_raising` for an example.
|
See :ref:`parametrizing_conditional_raising` for an example.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:ref:`assertraises` for more examples and detailed discussion.
|
||||||
|
|
||||||
**Legacy form**
|
**Legacy form**
|
||||||
|
|
||||||
It is possible to specify a callable by passing a to-be-called lambda::
|
It is possible to specify a callable by passing a to-be-called lambda::
|
||||||
|
|
|
@ -317,7 +317,7 @@ class CallInfo(Generic[TResult]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_call(
|
def from_call(
|
||||||
cls,
|
cls,
|
||||||
func: "Callable[[], TResult]",
|
func: Callable[[], TResult],
|
||||||
when: Literal["collect", "setup", "call", "teardown"],
|
when: Literal["collect", "setup", "call", "teardown"],
|
||||||
reraise: Optional[
|
reraise: Optional[
|
||||||
Union[Type[BaseException], Tuple[Type[BaseException], ...]]
|
Union[Type[BaseException], Tuple[Type[BaseException], ...]]
|
||||||
|
|
|
@ -8,3 +8,5 @@ import _pytest._py.path as path
|
||||||
|
|
||||||
sys.modules["py.error"] = error
|
sys.modules["py.error"] = error
|
||||||
sys.modules["py.path"] = path
|
sys.modules["py.path"] = path
|
||||||
|
|
||||||
|
__all__ = ["error", "path"]
|
||||||
|
|
|
@ -868,6 +868,9 @@ class TestLocalPath(CommonFSTests):
|
||||||
py_path.strpath, str_path
|
py_path.strpath, str_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pytest.mark.xfail(
|
||||||
|
reason="#11603", raises=(error.EEXIST, error.ENOENT), strict=False
|
||||||
|
)
|
||||||
def test_make_numbered_dir_multiprocess_safe(self, tmpdir):
|
def test_make_numbered_dir_multiprocess_safe(self, tmpdir):
|
||||||
# https://github.com/pytest-dev/py/issues/30
|
# https://github.com/pytest-dev/py/issues/30
|
||||||
with multiprocessing.Pool() as pool:
|
with multiprocessing.Pool() as pool:
|
||||||
|
|
|
@ -31,6 +31,17 @@ def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setenv("COLUMNS", "80")
|
monkeypatch.setenv("COLUMNS", "80")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_colors(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""
|
||||||
|
Reset all color-related variables to prevent them from affecting internal pytest output
|
||||||
|
in tests that depend on it.
|
||||||
|
"""
|
||||||
|
monkeypatch.delenv("PY_COLORS", raising=False)
|
||||||
|
monkeypatch.delenv("NO_COLOR", raising=False)
|
||||||
|
monkeypatch.delenv("FORCE_COLOR", raising=False)
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||||
def pytest_collection_modifyitems(items) -> Generator[None, None, None]:
|
def pytest_collection_modifyitems(items) -> Generator[None, None, None]:
|
||||||
"""Prefer faster tests.
|
"""Prefer faster tests.
|
||||||
|
|
|
@ -257,11 +257,17 @@ def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
|
||||||
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
|
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
|
||||||
mod = pytester.getmodulecol("")
|
mod = pytester.getmodulecol("")
|
||||||
|
|
||||||
|
class MyFile(pytest.File):
|
||||||
|
def collect(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
with pytest.warns(
|
with pytest.warns(
|
||||||
pytest.PytestDeprecationWarning,
|
pytest.PytestDeprecationWarning,
|
||||||
match=re.escape("The (fspath: py.path.local) argument to File is deprecated."),
|
match=re.escape(
|
||||||
|
"The (fspath: py.path.local) argument to MyFile is deprecated."
|
||||||
|
),
|
||||||
):
|
):
|
||||||
pytest.File.from_parent(
|
MyFile.from_parent(
|
||||||
parent=mod.parent,
|
parent=mod.parent,
|
||||||
fspath=legacy_path("bla"),
|
fspath=legacy_path("bla"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,4 +11,5 @@ def pytest_collect_file(file_path, parent):
|
||||||
|
|
||||||
|
|
||||||
class MyItem(pytest.Item):
|
class MyItem(pytest.Item):
|
||||||
pass
|
def runtest(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
|
@ -0,0 +1,406 @@
|
||||||
|
import textwrap
|
||||||
|
from collections import ChainMap
|
||||||
|
from collections import Counter
|
||||||
|
from collections import defaultdict
|
||||||
|
from collections import deque
|
||||||
|
from collections import OrderedDict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from types import MappingProxyType
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from _pytest._io.pprint import PrettyPrinter
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EmptyDataclass:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DataclassWithOneItem:
|
||||||
|
foo: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DataclassWithTwoItems:
|
||||||
|
foo: str
|
||||||
|
bar: str
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("data", "expected"),
|
||||||
|
(
|
||||||
|
pytest.param(
|
||||||
|
EmptyDataclass(),
|
||||||
|
"EmptyDataclass()",
|
||||||
|
id="dataclass-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
DataclassWithOneItem(foo="bar"),
|
||||||
|
"""
|
||||||
|
DataclassWithOneItem(
|
||||||
|
foo='bar',
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
id="dataclass-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
DataclassWithTwoItems(foo="foo", bar="bar"),
|
||||||
|
"""
|
||||||
|
DataclassWithTwoItems(
|
||||||
|
foo='foo',
|
||||||
|
bar='bar',
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
id="dataclass-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{},
|
||||||
|
"{}",
|
||||||
|
id="dict-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"one": 1},
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
'one': 1,
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
id="dict-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"one": 1, "two": 2},
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
'one': 1,
|
||||||
|
'two': 2,
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
id="dict-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(OrderedDict(), "OrderedDict()", id="ordereddict-empty"),
|
||||||
|
pytest.param(
|
||||||
|
OrderedDict({"one": 1}),
|
||||||
|
"""
|
||||||
|
OrderedDict({
|
||||||
|
'one': 1,
|
||||||
|
})
|
||||||
|
""",
|
||||||
|
id="ordereddict-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
OrderedDict({"one": 1, "two": 2}),
|
||||||
|
"""
|
||||||
|
OrderedDict({
|
||||||
|
'one': 1,
|
||||||
|
'two': 2,
|
||||||
|
})
|
||||||
|
""",
|
||||||
|
id="ordereddict-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
[],
|
||||||
|
"[]",
|
||||||
|
id="list-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
[1],
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
]
|
||||||
|
""",
|
||||||
|
id="list-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
[1, 2],
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
]
|
||||||
|
""",
|
||||||
|
id="list-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
tuple(),
|
||||||
|
"()",
|
||||||
|
id="tuple-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
(1,),
|
||||||
|
"""
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
id="tuple-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
(1, 2),
|
||||||
|
"""
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
id="tuple-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
set(),
|
||||||
|
"set()",
|
||||||
|
id="set-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{1},
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
1,
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
id="set-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{1, 2},
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
id="set-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
MappingProxyType({}),
|
||||||
|
"mappingproxy({})",
|
||||||
|
id="mappingproxy-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
MappingProxyType({"one": 1}),
|
||||||
|
"""
|
||||||
|
mappingproxy({
|
||||||
|
'one': 1,
|
||||||
|
})
|
||||||
|
""",
|
||||||
|
id="mappingproxy-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
MappingProxyType({"one": 1, "two": 2}),
|
||||||
|
"""
|
||||||
|
mappingproxy({
|
||||||
|
'one': 1,
|
||||||
|
'two': 2,
|
||||||
|
})
|
||||||
|
""",
|
||||||
|
id="mappingproxy-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
SimpleNamespace(),
|
||||||
|
"namespace()",
|
||||||
|
id="simplenamespace-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
SimpleNamespace(one=1),
|
||||||
|
"""
|
||||||
|
namespace(
|
||||||
|
one=1,
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
id="simplenamespace-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
SimpleNamespace(one=1, two=2),
|
||||||
|
"""
|
||||||
|
namespace(
|
||||||
|
one=1,
|
||||||
|
two=2,
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
id="simplenamespace-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
defaultdict(str), "defaultdict(<class 'str'>, {})", id="defaultdict-empty"
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
defaultdict(str, {"one": "1"}),
|
||||||
|
"""
|
||||||
|
defaultdict(<class 'str'>, {
|
||||||
|
'one': '1',
|
||||||
|
})
|
||||||
|
""",
|
||||||
|
id="defaultdict-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
defaultdict(str, {"one": "1", "two": "2"}),
|
||||||
|
"""
|
||||||
|
defaultdict(<class 'str'>, {
|
||||||
|
'one': '1',
|
||||||
|
'two': '2',
|
||||||
|
})
|
||||||
|
""",
|
||||||
|
id="defaultdict-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Counter(),
|
||||||
|
"Counter()",
|
||||||
|
id="counter-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Counter("1"),
|
||||||
|
"""
|
||||||
|
Counter({
|
||||||
|
'1': 1,
|
||||||
|
})
|
||||||
|
""",
|
||||||
|
id="counter-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Counter("121"),
|
||||||
|
"""
|
||||||
|
Counter({
|
||||||
|
'1': 2,
|
||||||
|
'2': 1,
|
||||||
|
})
|
||||||
|
""",
|
||||||
|
id="counter-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(ChainMap(), "ChainMap({})", id="chainmap-empty"),
|
||||||
|
pytest.param(
|
||||||
|
ChainMap({"one": 1, "two": 2}),
|
||||||
|
"""
|
||||||
|
ChainMap(
|
||||||
|
{
|
||||||
|
'one': 1,
|
||||||
|
'two': 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
id="chainmap-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
ChainMap({"one": 1}, {"two": 2}),
|
||||||
|
"""
|
||||||
|
ChainMap(
|
||||||
|
{
|
||||||
|
'one': 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'two': 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
id="chainmap-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
deque(),
|
||||||
|
"deque([])",
|
||||||
|
id="deque-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
deque([1]),
|
||||||
|
"""
|
||||||
|
deque([
|
||||||
|
1,
|
||||||
|
])
|
||||||
|
""",
|
||||||
|
id="deque-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
deque([1, 2]),
|
||||||
|
"""
|
||||||
|
deque([
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
])
|
||||||
|
""",
|
||||||
|
id="deque-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
deque([1, 2], maxlen=3),
|
||||||
|
"""
|
||||||
|
deque(maxlen=3, [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
])
|
||||||
|
""",
|
||||||
|
id="deque-maxlen",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"chainmap": ChainMap({"one": 1}, {"two": 2}),
|
||||||
|
"counter": Counter("122"),
|
||||||
|
"dataclass": DataclassWithTwoItems(foo="foo", bar="bar"),
|
||||||
|
"defaultdict": defaultdict(str, {"one": "1", "two": "2"}),
|
||||||
|
"deque": deque([1, 2], maxlen=3),
|
||||||
|
"dict": {"one": 1, "two": 2},
|
||||||
|
"list": [1, 2],
|
||||||
|
"mappingproxy": MappingProxyType({"one": 1, "two": 2}),
|
||||||
|
"ordereddict": OrderedDict({"one": 1, "two": 2}),
|
||||||
|
"set": {1, 2},
|
||||||
|
"simplenamespace": SimpleNamespace(one=1, two=2),
|
||||||
|
"tuple": (1, 2),
|
||||||
|
},
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
'chainmap': ChainMap(
|
||||||
|
{
|
||||||
|
'one': 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'two': 2,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'counter': Counter({
|
||||||
|
'2': 2,
|
||||||
|
'1': 1,
|
||||||
|
}),
|
||||||
|
'dataclass': DataclassWithTwoItems(
|
||||||
|
foo='foo',
|
||||||
|
bar='bar',
|
||||||
|
),
|
||||||
|
'defaultdict': defaultdict(<class 'str'>, {
|
||||||
|
'one': '1',
|
||||||
|
'two': '2',
|
||||||
|
}),
|
||||||
|
'deque': deque(maxlen=3, [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
]),
|
||||||
|
'dict': {
|
||||||
|
'one': 1,
|
||||||
|
'two': 2,
|
||||||
|
},
|
||||||
|
'list': [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
'mappingproxy': mappingproxy({
|
||||||
|
'one': 1,
|
||||||
|
'two': 2,
|
||||||
|
}),
|
||||||
|
'ordereddict': OrderedDict({
|
||||||
|
'one': 1,
|
||||||
|
'two': 2,
|
||||||
|
}),
|
||||||
|
'set': {
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
'simplenamespace': namespace(
|
||||||
|
one=1,
|
||||||
|
two=2,
|
||||||
|
),
|
||||||
|
'tuple': (
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
id="deep-example",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_consistent_pretty_printer(data: Any, expected: str) -> None:
|
||||||
|
assert PrettyPrinter().pformat(data) == textwrap.dedent(expected).strip()
|
|
@ -1,5 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest._io.saferepr import _pformat_dispatch
|
|
||||||
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
|
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest._io.saferepr import saferepr_unlimited
|
from _pytest._io.saferepr import saferepr_unlimited
|
||||||
|
@ -159,12 +158,6 @@ def test_unicode():
|
||||||
assert saferepr(val) == reprval
|
assert saferepr(val) == reprval
|
||||||
|
|
||||||
|
|
||||||
def test_pformat_dispatch():
|
|
||||||
assert _pformat_dispatch("a") == "'a'"
|
|
||||||
assert _pformat_dispatch("a" * 10, width=5) == "'aaaaaaaaaa'"
|
|
||||||
assert _pformat_dispatch("foo bar", width=5) == "('foo '\n 'bar')"
|
|
||||||
|
|
||||||
|
|
||||||
def test_broken_getattribute():
|
def test_broken_getattribute():
|
||||||
"""saferepr() can create proper representations of classes with
|
"""saferepr() can create proper representations of classes with
|
||||||
broken __getattribute__ (#7145)
|
broken __getattribute__ (#7145)
|
||||||
|
|
|
@ -254,7 +254,7 @@ class TestTerminalWriterLineWidth:
|
||||||
pytest.param(
|
pytest.param(
|
||||||
True,
|
True,
|
||||||
True,
|
True,
|
||||||
"{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n",
|
"{reset}{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n",
|
||||||
id="with markup and code_highlight",
|
id="with markup and code_highlight",
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
|
|
|
@ -144,7 +144,7 @@ def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
||||||
result.assert_outcomes(passed=3)
|
result.assert_outcomes(passed=3)
|
||||||
|
|
||||||
|
|
||||||
def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
|
def test_with_statement_at_level(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
with caplog.at_level(logging.INFO):
|
with caplog.at_level(logging.INFO):
|
||||||
logger.debug("handler DEBUG level")
|
logger.debug("handler DEBUG level")
|
||||||
logger.info("handler INFO level")
|
logger.info("handler INFO level")
|
||||||
|
@ -159,7 +159,9 @@ def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
assert "CRITICAL" in caplog.text
|
assert "CRITICAL" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_with_statement_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
|
def test_with_statement_at_level_logging_disabled(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
assert logging.root.manager.disable == logging.CRITICAL
|
assert logging.root.manager.disable == logging.CRITICAL
|
||||||
with caplog.at_level(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
|
@ -185,6 +187,22 @@ def test_with_statement_logging_disabled(caplog: pytest.LogCaptureFixture) -> No
|
||||||
assert logging.root.manager.disable == logging.CRITICAL
|
assert logging.root.manager.disable == logging.CRITICAL
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_statement_filtering(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
|
class TestFilter(logging.Filter):
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
record.msg = "filtered handler call"
|
||||||
|
return True
|
||||||
|
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
with caplog.filtering(TestFilter()):
|
||||||
|
logger.info("handler call")
|
||||||
|
logger.info("handler call")
|
||||||
|
|
||||||
|
filtered_tuple, unfiltered_tuple = caplog.record_tuples
|
||||||
|
assert filtered_tuple == ("test_fixture", 20, "filtered handler call")
|
||||||
|
assert unfiltered_tuple == ("test_fixture", 20, "handler call")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"level_str,expected_disable_level",
|
"level_str,expected_disable_level",
|
||||||
[
|
[
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
anyio[curio,trio]==4.0.0
|
anyio[curio,trio]==4.2.0
|
||||||
django==4.2.6
|
django==5.0
|
||||||
pytest-asyncio==0.21.1
|
pytest-asyncio==0.23.2
|
||||||
pytest-bdd==7.0.0
|
pytest-bdd==7.0.1
|
||||||
pytest-cov==4.1.0
|
pytest-cov==4.1.0
|
||||||
pytest-django==4.5.2
|
pytest-django==4.7.0
|
||||||
pytest-flakes==4.0.5
|
pytest-flakes==4.0.5
|
||||||
pytest-html==4.0.2
|
pytest-html==4.1.1
|
||||||
pytest-mock==3.12.0
|
pytest-mock==3.12.0
|
||||||
pytest-rerunfailures==12.0
|
pytest-rerunfailures==13.0
|
||||||
pytest-sugar==0.9.7
|
pytest-sugar==0.9.7
|
||||||
pytest-trio==0.7.0
|
pytest-trio==0.7.0
|
||||||
pytest-twisted==1.14.0
|
pytest-twisted==1.14.0
|
||||||
twisted==23.8.0
|
twisted==23.10.0
|
||||||
pytest-xvfb==3.0.0
|
pytest-xvfb==3.0.0
|
||||||
|
|
|
@ -99,6 +99,7 @@ class TestApprox:
|
||||||
2.0,
|
2.0,
|
||||||
1.0,
|
1.0,
|
||||||
[
|
[
|
||||||
|
"",
|
||||||
" comparison failed",
|
" comparison failed",
|
||||||
f" Obtained: {SOME_FLOAT}",
|
f" Obtained: {SOME_FLOAT}",
|
||||||
f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}",
|
f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}",
|
||||||
|
@ -113,6 +114,7 @@ class TestApprox:
|
||||||
"c": 3000000.0,
|
"c": 3000000.0,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 2 / 3:",
|
r" comparison failed. Mismatched elements: 2 / 3:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
rf" Max relative difference: {SOME_FLOAT}",
|
rf" Max relative difference: {SOME_FLOAT}",
|
||||||
|
@ -130,6 +132,7 @@ class TestApprox:
|
||||||
"c": None,
|
"c": None,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 2 / 3:",
|
r" comparison failed. Mismatched elements: 2 / 3:",
|
||||||
r" Max absolute difference: -inf",
|
r" Max absolute difference: -inf",
|
||||||
r" Max relative difference: -inf",
|
r" Max relative difference: -inf",
|
||||||
|
@ -143,6 +146,7 @@ class TestApprox:
|
||||||
[1.0, 2.0, 3.0, 4.0],
|
[1.0, 2.0, 3.0, 4.0],
|
||||||
[1.0, 3.0, 3.0, 5.0],
|
[1.0, 3.0, 3.0, 5.0],
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 2 / 4:",
|
r" comparison failed. Mismatched elements: 2 / 4:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
rf" Max relative difference: {SOME_FLOAT}",
|
rf" Max relative difference: {SOME_FLOAT}",
|
||||||
|
@ -156,6 +160,7 @@ class TestApprox:
|
||||||
(1, 2.2, 4),
|
(1, 2.2, 4),
|
||||||
(1, 3.2, 4),
|
(1, 3.2, 4),
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 1 / 3:",
|
r" comparison failed. Mismatched elements: 1 / 3:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
rf" Max relative difference: {SOME_FLOAT}",
|
rf" Max relative difference: {SOME_FLOAT}",
|
||||||
|
@ -169,6 +174,7 @@ class TestApprox:
|
||||||
[0.0],
|
[0.0],
|
||||||
[1.0],
|
[1.0],
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 1 / 1:",
|
r" comparison failed. Mismatched elements: 1 / 1:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
r" Max relative difference: inf",
|
r" Max relative difference: inf",
|
||||||
|
@ -187,6 +193,7 @@ class TestApprox:
|
||||||
a,
|
a,
|
||||||
b,
|
b,
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 1 / 20:",
|
r" comparison failed. Mismatched elements: 1 / 20:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
rf" Max relative difference: {SOME_FLOAT}",
|
rf" Max relative difference: {SOME_FLOAT}",
|
||||||
|
@ -209,6 +216,7 @@ class TestApprox:
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 3 / 8:",
|
r" comparison failed. Mismatched elements: 3 / 8:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
rf" Max relative difference: {SOME_FLOAT}",
|
rf" Max relative difference: {SOME_FLOAT}",
|
||||||
|
@ -224,6 +232,7 @@ class TestApprox:
|
||||||
np.array([0.0]),
|
np.array([0.0]),
|
||||||
np.array([1.0]),
|
np.array([1.0]),
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 1 / 1:",
|
r" comparison failed. Mismatched elements: 1 / 1:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
r" Max relative difference: inf",
|
r" Max relative difference: inf",
|
||||||
|
@ -241,6 +250,7 @@ class TestApprox:
|
||||||
message = "\n".join(str(e.value).split("\n")[1:])
|
message = "\n".join(str(e.value).split("\n")[1:])
|
||||||
assert message == "\n".join(
|
assert message == "\n".join(
|
||||||
[
|
[
|
||||||
|
" ",
|
||||||
" Impossible to compare arrays with different shapes.",
|
" Impossible to compare arrays with different shapes.",
|
||||||
" Shapes: (2, 1) and (2, 2)",
|
" Shapes: (2, 1) and (2, 2)",
|
||||||
]
|
]
|
||||||
|
@ -251,6 +261,7 @@ class TestApprox:
|
||||||
message = "\n".join(str(e.value).split("\n")[1:])
|
message = "\n".join(str(e.value).split("\n")[1:])
|
||||||
assert message == "\n".join(
|
assert message == "\n".join(
|
||||||
[
|
[
|
||||||
|
" ",
|
||||||
" Impossible to compare lists with different sizes.",
|
" Impossible to compare lists with different sizes.",
|
||||||
" Lengths: 2 and 3",
|
" Lengths: 2 and 3",
|
||||||
]
|
]
|
||||||
|
@ -264,6 +275,7 @@ class TestApprox:
|
||||||
2.0,
|
2.0,
|
||||||
1.0,
|
1.0,
|
||||||
[
|
[
|
||||||
|
"",
|
||||||
" comparison failed",
|
" comparison failed",
|
||||||
f" Obtained: {SOME_FLOAT}",
|
f" Obtained: {SOME_FLOAT}",
|
||||||
f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}",
|
f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}",
|
||||||
|
@ -277,15 +289,15 @@ class TestApprox:
|
||||||
a,
|
a,
|
||||||
b,
|
b,
|
||||||
[
|
[
|
||||||
r" comparison failed. Mismatched elements: 20 / 20:",
|
r"^ $",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
r"^ comparison failed. Mismatched elements: 20 / 20:$",
|
||||||
rf" Max relative difference: {SOME_FLOAT}",
|
rf"^ Max absolute difference: {SOME_FLOAT}$",
|
||||||
r" Index \| Obtained\s+\| Expected",
|
rf"^ Max relative difference: {SOME_FLOAT}$",
|
||||||
rf" \(0,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
|
r"^ Index \| Obtained\s+\| Expected\s+$",
|
||||||
rf" \(1,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
|
rf"^ \(0,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}e-{SOME_INT}$",
|
||||||
rf" \(2,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}...",
|
rf"^ \(1,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}e-{SOME_INT}\.\.\.$",
|
||||||
"",
|
"^ $",
|
||||||
rf"\s*...Full output truncated \({SOME_INT} lines hidden\), use '-vv' to show",
|
rf"^ ...Full output truncated \({SOME_INT} lines hidden\), use '-vv' to show$",
|
||||||
],
|
],
|
||||||
verbosity_level=0,
|
verbosity_level=0,
|
||||||
)
|
)
|
||||||
|
@ -294,6 +306,7 @@ class TestApprox:
|
||||||
a,
|
a,
|
||||||
b,
|
b,
|
||||||
[
|
[
|
||||||
|
r" ",
|
||||||
r" comparison failed. Mismatched elements: 20 / 20:",
|
r" comparison failed. Mismatched elements: 20 / 20:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
rf" Max relative difference: {SOME_FLOAT}",
|
rf" Max relative difference: {SOME_FLOAT}",
|
||||||
|
@ -652,6 +665,7 @@ class TestApprox:
|
||||||
{"foo": 42.0},
|
{"foo": 42.0},
|
||||||
{"foo": 0.0},
|
{"foo": 0.0},
|
||||||
[
|
[
|
||||||
|
r"",
|
||||||
r" comparison failed. Mismatched elements: 1 / 1:",
|
r" comparison failed. Mismatched elements: 1 / 1:",
|
||||||
rf" Max absolute difference: {SOME_FLOAT}",
|
rf" Max absolute difference: {SOME_FLOAT}",
|
||||||
r" Max relative difference: inf",
|
r" Max relative difference: inf",
|
||||||
|
|
|
@ -13,27 +13,68 @@ import pytest
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest.assertion import truncate
|
from _pytest.assertion import truncate
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
|
from _pytest.config import Config as _Config
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
|
|
||||||
|
|
||||||
def mock_config(verbose=0):
|
def mock_config(verbose: int = 0, assertion_override: Optional[int] = None):
|
||||||
class TerminalWriter:
|
class TerminalWriter:
|
||||||
def _highlight(self, source, lexer):
|
def _highlight(self, source, lexer="python"):
|
||||||
return source
|
return source
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
def getoption(self, name):
|
|
||||||
if name == "verbose":
|
|
||||||
return verbose
|
|
||||||
raise KeyError("Not mocked out: %s" % name)
|
|
||||||
|
|
||||||
def get_terminal_writer(self):
|
def get_terminal_writer(self):
|
||||||
return TerminalWriter()
|
return TerminalWriter()
|
||||||
|
|
||||||
|
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||||
|
if verbosity_type is None:
|
||||||
|
return verbose
|
||||||
|
if verbosity_type == _Config.VERBOSITY_ASSERTIONS:
|
||||||
|
if assertion_override is not None:
|
||||||
|
return assertion_override
|
||||||
|
return verbose
|
||||||
|
|
||||||
|
raise KeyError(f"Not mocked out: {verbosity_type}")
|
||||||
|
|
||||||
return Config()
|
return Config()
|
||||||
|
|
||||||
|
|
||||||
|
class TestMockConfig:
|
||||||
|
SOME_VERBOSITY_LEVEL = 3
|
||||||
|
SOME_OTHER_VERBOSITY_LEVEL = 10
|
||||||
|
|
||||||
|
def test_verbose_exposes_value(self):
|
||||||
|
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
|
||||||
|
|
||||||
|
assert config.get_verbosity() == TestMockConfig.SOME_VERBOSITY_LEVEL
|
||||||
|
|
||||||
|
def test_get_assertion_override_not_set_verbose_value(self):
|
||||||
|
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
|
||||||
|
== TestMockConfig.SOME_VERBOSITY_LEVEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_assertion_override_set_custom_value(self):
|
||||||
|
config = mock_config(
|
||||||
|
verbose=TestMockConfig.SOME_VERBOSITY_LEVEL,
|
||||||
|
assertion_override=TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
|
||||||
|
== TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_unsupported_type_error(self):
|
||||||
|
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
config.get_verbosity("--- NOT A VERBOSITY LEVEL ---")
|
||||||
|
|
||||||
|
|
||||||
class TestImportHookInstallation:
|
class TestImportHookInstallation:
|
||||||
@pytest.mark.parametrize("initial_conftest", [True, False])
|
@pytest.mark.parametrize("initial_conftest", [True, False])
|
||||||
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
|
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
|
||||||
|
@ -351,6 +392,7 @@ class TestAssert_reprcompare:
|
||||||
def test_text_diff(self) -> None:
|
def test_text_diff(self) -> None:
|
||||||
assert callequal("spam", "eggs") == [
|
assert callequal("spam", "eggs") == [
|
||||||
"'spam' == 'eggs'",
|
"'spam' == 'eggs'",
|
||||||
|
"",
|
||||||
"- eggs",
|
"- eggs",
|
||||||
"+ spam",
|
"+ spam",
|
||||||
]
|
]
|
||||||
|
@ -358,7 +400,7 @@ class TestAssert_reprcompare:
|
||||||
def test_text_skipping(self) -> None:
|
def test_text_skipping(self) -> None:
|
||||||
lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs")
|
lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs")
|
||||||
assert lines is not None
|
assert lines is not None
|
||||||
assert "Skipping" in lines[1]
|
assert "Skipping" in lines[2]
|
||||||
for line in lines:
|
for line in lines:
|
||||||
assert "a" * 50 not in line
|
assert "a" * 50 not in line
|
||||||
|
|
||||||
|
@ -382,6 +424,7 @@ class TestAssert_reprcompare:
|
||||||
|
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"b'spam' == b'eggs'",
|
"b'spam' == b'eggs'",
|
||||||
|
"",
|
||||||
"At index 0 diff: b's' != b'e'",
|
"At index 0 diff: b's' != b'e'",
|
||||||
"Use -v to get more diff",
|
"Use -v to get more diff",
|
||||||
]
|
]
|
||||||
|
@ -391,7 +434,9 @@ class TestAssert_reprcompare:
|
||||||
diff = callequal(b"spam", b"eggs", verbose=1)
|
diff = callequal(b"spam", b"eggs", verbose=1)
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"b'spam' == b'eggs'",
|
"b'spam' == b'eggs'",
|
||||||
|
"",
|
||||||
"At index 0 diff: b's' != b'e'",
|
"At index 0 diff: b's' != b'e'",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
"- b'eggs'",
|
"- b'eggs'",
|
||||||
"+ b'spam'",
|
"+ b'spam'",
|
||||||
|
@ -410,10 +455,13 @@ class TestAssert_reprcompare:
|
||||||
[0, 2],
|
[0, 2],
|
||||||
"""
|
"""
|
||||||
Full diff:
|
Full diff:
|
||||||
- [0, 2]
|
[
|
||||||
|
0,
|
||||||
|
- 2,
|
||||||
? ^
|
? ^
|
||||||
+ [0, 1]
|
+ 1,
|
||||||
? ^
|
? ^
|
||||||
|
]
|
||||||
""",
|
""",
|
||||||
id="lists",
|
id="lists",
|
||||||
),
|
),
|
||||||
|
@ -422,10 +470,12 @@ class TestAssert_reprcompare:
|
||||||
{0: 2},
|
{0: 2},
|
||||||
"""
|
"""
|
||||||
Full diff:
|
Full diff:
|
||||||
- {0: 2}
|
{
|
||||||
|
- 0: 2,
|
||||||
? ^
|
? ^
|
||||||
+ {0: 1}
|
+ 0: 1,
|
||||||
? ^
|
? ^
|
||||||
|
}
|
||||||
""",
|
""",
|
||||||
id="dicts",
|
id="dicts",
|
||||||
),
|
),
|
||||||
|
@ -434,10 +484,13 @@ class TestAssert_reprcompare:
|
||||||
{0, 2},
|
{0, 2},
|
||||||
"""
|
"""
|
||||||
Full diff:
|
Full diff:
|
||||||
- {0, 2}
|
{
|
||||||
|
0,
|
||||||
|
- 2,
|
||||||
? ^
|
? ^
|
||||||
+ {0, 1}
|
+ 1,
|
||||||
? ^
|
? ^
|
||||||
|
}
|
||||||
""",
|
""",
|
||||||
id="sets",
|
id="sets",
|
||||||
),
|
),
|
||||||
|
@ -460,6 +513,7 @@ class TestAssert_reprcompare:
|
||||||
expl = callequal([1, 2], [10, 2], verbose=-1)
|
expl = callequal([1, 2], [10, 2], verbose=-1)
|
||||||
assert expl == [
|
assert expl == [
|
||||||
"[1, 2] == [10, 2]",
|
"[1, 2] == [10, 2]",
|
||||||
|
"",
|
||||||
"At index 0 diff: 1 != 10",
|
"At index 0 diff: 1 != 10",
|
||||||
"Use -v to get more diff",
|
"Use -v to get more diff",
|
||||||
]
|
]
|
||||||
|
@ -498,7 +552,9 @@ class TestAssert_reprcompare:
|
||||||
diff = callequal(l1, l2, verbose=True)
|
diff = callequal(l1, l2, verbose=True)
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"['a', 'b', 'c'] == ['a', 'b', 'c...dddddddddddd']",
|
"['a', 'b', 'c'] == ['a', 'b', 'c...dddddddddddd']",
|
||||||
|
"",
|
||||||
"Right contains one more item: '" + long_d + "'",
|
"Right contains one more item: '" + long_d + "'",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
" [",
|
" [",
|
||||||
" 'a',",
|
" 'a',",
|
||||||
|
@ -511,7 +567,9 @@ class TestAssert_reprcompare:
|
||||||
diff = callequal(l2, l1, verbose=True)
|
diff = callequal(l2, l1, verbose=True)
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"['a', 'b', 'c...dddddddddddd'] == ['a', 'b', 'c']",
|
"['a', 'b', 'c...dddddddddddd'] == ['a', 'b', 'c']",
|
||||||
|
"",
|
||||||
"Left contains one more item: '" + long_d + "'",
|
"Left contains one more item: '" + long_d + "'",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
" [",
|
" [",
|
||||||
" 'a',",
|
" 'a',",
|
||||||
|
@ -530,7 +588,9 @@ class TestAssert_reprcompare:
|
||||||
diff = callequal(l1, l2, verbose=True)
|
diff = callequal(l1, l2, verbose=True)
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"['aaaaaaaaaaa...cccccccccccc'] == ['bbbbbbbbbbb...aaaaaaaaaaaa']",
|
"['aaaaaaaaaaa...cccccccccccc'] == ['bbbbbbbbbbb...aaaaaaaaaaaa']",
|
||||||
|
"",
|
||||||
"At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'",
|
"At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
" [",
|
" [",
|
||||||
"+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
|
"+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
|
||||||
|
@ -547,8 +607,10 @@ class TestAssert_reprcompare:
|
||||||
diff = callequal(l1, l2, verbose=True)
|
diff = callequal(l1, l2, verbose=True)
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"['a', 'aaaaaa...aaaaaaa', ...] == ['should not get wrapped']",
|
"['a', 'aaaaaa...aaaaaaa', ...] == ['should not get wrapped']",
|
||||||
|
"",
|
||||||
"At index 0 diff: 'a' != 'should not get wrapped'",
|
"At index 0 diff: 'a' != 'should not get wrapped'",
|
||||||
"Left contains 7 more items, first extra item: 'aaaaaaaaaa'",
|
"Left contains 7 more items, first extra item: 'aaaaaaaaaa'",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
" [",
|
" [",
|
||||||
"- 'should not get wrapped',",
|
"- 'should not get wrapped',",
|
||||||
|
@ -570,30 +632,44 @@ class TestAssert_reprcompare:
|
||||||
diff = callequal(d1, d2, verbose=True)
|
diff = callequal(d1, d2, verbose=True)
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"{'common': 1,...1, 'env2': 2}} == {'common': 1,...: {'env1': 1}}",
|
"{'common': 1,...1, 'env2': 2}} == {'common': 1,...: {'env1': 1}}",
|
||||||
|
"",
|
||||||
"Omitting 1 identical items, use -vv to show",
|
"Omitting 1 identical items, use -vv to show",
|
||||||
"Differing items:",
|
"Differing items:",
|
||||||
"{'env': {'env1': 1, 'env2': 2}} != {'env': {'env1': 1}}",
|
"{'env': {'env1': 1, 'env2': 2}} != {'env': {'env1': 1}}",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
"- {'common': 1, 'env': {'env1': 1}}",
|
" {",
|
||||||
"+ {'common': 1, 'env': {'env1': 1, 'env2': 2}}",
|
" 'common': 1,",
|
||||||
"? +++++++++++",
|
" 'env': {",
|
||||||
|
" 'env1': 1,",
|
||||||
|
"+ 'env2': 2,",
|
||||||
|
" },",
|
||||||
|
" }",
|
||||||
]
|
]
|
||||||
|
|
||||||
long_a = "a" * 80
|
long_a = "a" * 80
|
||||||
sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 2}}
|
sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}}
|
||||||
d1 = {"env": {"sub": sub}}
|
d1 = {"env": {"sub": sub}}
|
||||||
d2 = {"env": {"sub": sub}, "new": 1}
|
d2 = {"env": {"sub": sub}, "new": 1}
|
||||||
diff = callequal(d1, d2, verbose=True)
|
diff = callequal(d1, d2, verbose=True)
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}",
|
"{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}",
|
||||||
|
"",
|
||||||
"Omitting 1 identical items, use -vv to show",
|
"Omitting 1 identical items, use -vv to show",
|
||||||
"Right contains 1 more item:",
|
"Right contains 1 more item:",
|
||||||
"{'new': 1}",
|
"{'new': 1}",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
" {",
|
" {",
|
||||||
" 'env': {'sub': {'long_a': '" + long_a + "',",
|
" 'env': {",
|
||||||
" 'sub1': {'long_a': 'substring that gets wrapped substring '",
|
" 'sub': {",
|
||||||
" 'that gets wrapped '}}},",
|
f" 'long_a': '{long_a}',",
|
||||||
|
" 'sub1': {",
|
||||||
|
" 'long_a': 'substring that gets wrapped substring that gets wrapped '",
|
||||||
|
" 'substring that gets wrapped ',",
|
||||||
|
" },",
|
||||||
|
" },",
|
||||||
|
" },",
|
||||||
"- 'new': 1,",
|
"- 'new': 1,",
|
||||||
" }",
|
" }",
|
||||||
]
|
]
|
||||||
|
@ -606,7 +682,7 @@ class TestAssert_reprcompare:
|
||||||
def test_dict_omitting(self) -> None:
|
def test_dict_omitting(self) -> None:
|
||||||
lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1})
|
lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1})
|
||||||
assert lines is not None
|
assert lines is not None
|
||||||
assert lines[1].startswith("Omitting 1 identical item")
|
assert lines[2].startswith("Omitting 1 identical item")
|
||||||
assert "Common items" not in lines
|
assert "Common items" not in lines
|
||||||
for line in lines[1:]:
|
for line in lines[1:]:
|
||||||
assert "b" not in line
|
assert "b" not in line
|
||||||
|
@ -615,60 +691,109 @@ class TestAssert_reprcompare:
|
||||||
"""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)
|
lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=1)
|
||||||
assert lines is not None
|
assert lines is not None
|
||||||
assert lines[1].startswith("Omitting 1 identical item")
|
assert lines[1] == ""
|
||||||
assert lines[2].startswith("Differing items")
|
assert lines[2].startswith("Omitting 1 identical item")
|
||||||
assert lines[3] == "{'a': 0} != {'a': 1}"
|
assert lines[3].startswith("Differing items")
|
||||||
|
assert lines[4] == "{'a': 0} != {'a': 1}"
|
||||||
assert "Common items" not in lines
|
assert "Common items" not in lines
|
||||||
|
|
||||||
def test_dict_omitting_with_verbosity_2(self) -> None:
|
def test_dict_omitting_with_verbosity_2(self) -> None:
|
||||||
lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=2)
|
lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=2)
|
||||||
assert lines is not None
|
assert lines is not None
|
||||||
assert lines[1].startswith("Common items:")
|
assert lines[2].startswith("Common items:")
|
||||||
assert "Omitting" not in lines[1]
|
assert "Omitting" not in lines[2]
|
||||||
assert lines[2] == "{'b': 1}"
|
assert lines[3] == "{'b': 1}"
|
||||||
|
|
||||||
def test_dict_different_items(self) -> None:
|
def test_dict_different_items(self) -> None:
|
||||||
lines = callequal({"a": 0}, {"b": 1, "c": 2}, verbose=2)
|
lines = callequal({"a": 0}, {"b": 1, "c": 2}, verbose=2)
|
||||||
assert lines == [
|
assert lines == [
|
||||||
"{'a': 0} == {'b': 1, 'c': 2}",
|
"{'a': 0} == {'b': 1, 'c': 2}",
|
||||||
|
"",
|
||||||
"Left contains 1 more item:",
|
"Left contains 1 more item:",
|
||||||
"{'a': 0}",
|
"{'a': 0}",
|
||||||
"Right contains 2 more items:",
|
"Right contains 2 more items:",
|
||||||
"{'b': 1, 'c': 2}",
|
"{'b': 1, 'c': 2}",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
"- {'b': 1, 'c': 2}",
|
" {",
|
||||||
"+ {'a': 0}",
|
"- 'b': 1,",
|
||||||
|
"? ^ ^",
|
||||||
|
"+ 'a': 0,",
|
||||||
|
"? ^ ^",
|
||||||
|
"- 'c': 2,",
|
||||||
|
" }",
|
||||||
]
|
]
|
||||||
lines = callequal({"b": 1, "c": 2}, {"a": 0}, verbose=2)
|
lines = callequal({"b": 1, "c": 2}, {"a": 0}, verbose=2)
|
||||||
assert lines == [
|
assert lines == [
|
||||||
"{'b': 1, 'c': 2} == {'a': 0}",
|
"{'b': 1, 'c': 2} == {'a': 0}",
|
||||||
|
"",
|
||||||
"Left contains 2 more items:",
|
"Left contains 2 more items:",
|
||||||
"{'b': 1, 'c': 2}",
|
"{'b': 1, 'c': 2}",
|
||||||
"Right contains 1 more item:",
|
"Right contains 1 more item:",
|
||||||
"{'a': 0}",
|
"{'a': 0}",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
"- {'a': 0}",
|
" {",
|
||||||
"+ {'b': 1, 'c': 2}",
|
"- 'a': 0,",
|
||||||
|
"? ^ ^",
|
||||||
|
"+ 'b': 1,",
|
||||||
|
"? ^ ^",
|
||||||
|
"+ 'c': 2,",
|
||||||
|
" }",
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_sequence_different_items(self) -> None:
|
def test_sequence_different_items(self) -> None:
|
||||||
lines = callequal((1, 2), (3, 4, 5), verbose=2)
|
lines = callequal((1, 2), (3, 4, 5), verbose=2)
|
||||||
assert lines == [
|
assert lines == [
|
||||||
"(1, 2) == (3, 4, 5)",
|
"(1, 2) == (3, 4, 5)",
|
||||||
|
"",
|
||||||
"At index 0 diff: 1 != 3",
|
"At index 0 diff: 1 != 3",
|
||||||
"Right contains one more item: 5",
|
"Right contains one more item: 5",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
"- (3, 4, 5)",
|
" (",
|
||||||
"+ (1, 2)",
|
"- 3,",
|
||||||
|
"? ^",
|
||||||
|
"+ 1,",
|
||||||
|
"? ^",
|
||||||
|
"- 4,",
|
||||||
|
"? ^",
|
||||||
|
"+ 2,",
|
||||||
|
"? ^",
|
||||||
|
"- 5,",
|
||||||
|
" )",
|
||||||
]
|
]
|
||||||
lines = callequal((1, 2, 3), (4,), verbose=2)
|
lines = callequal((1, 2, 3), (4,), verbose=2)
|
||||||
assert lines == [
|
assert lines == [
|
||||||
"(1, 2, 3) == (4,)",
|
"(1, 2, 3) == (4,)",
|
||||||
|
"",
|
||||||
"At index 0 diff: 1 != 4",
|
"At index 0 diff: 1 != 4",
|
||||||
"Left contains 2 more items, first extra item: 2",
|
"Left contains 2 more items, first extra item: 2",
|
||||||
|
"",
|
||||||
"Full diff:",
|
"Full diff:",
|
||||||
"- (4,)",
|
" (",
|
||||||
"+ (1, 2, 3)",
|
"- 4,",
|
||||||
|
"? ^",
|
||||||
|
"+ 1,",
|
||||||
|
"? ^",
|
||||||
|
"+ 2,",
|
||||||
|
"+ 3,",
|
||||||
|
" )",
|
||||||
|
]
|
||||||
|
lines = callequal((1, 2, 3), (1, 20, 3), verbose=2)
|
||||||
|
assert lines == [
|
||||||
|
"(1, 2, 3) == (1, 20, 3)",
|
||||||
|
"",
|
||||||
|
"At index 1 diff: 2 != 20",
|
||||||
|
"",
|
||||||
|
"Full diff:",
|
||||||
|
" (",
|
||||||
|
" 1,",
|
||||||
|
"- 20,",
|
||||||
|
"? -",
|
||||||
|
"+ 2,",
|
||||||
|
" 3,",
|
||||||
|
" )",
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_set(self) -> None:
|
def test_set(self) -> None:
|
||||||
|
@ -726,7 +851,7 @@ class TestAssert_reprcompare:
|
||||||
assert expl is not None
|
assert expl is not None
|
||||||
assert expl[0].startswith("{} == <[ValueError")
|
assert expl[0].startswith("{} == <[ValueError")
|
||||||
assert "raised in repr" in expl[0]
|
assert "raised in repr" in expl[0]
|
||||||
assert expl[1:] == [
|
assert expl[2:] == [
|
||||||
"(pytest_assertion plugin: representation of details failed:"
|
"(pytest_assertion plugin: representation of details failed:"
|
||||||
" {}:{}: ValueError: 42.".format(
|
" {}:{}: ValueError: 42.".format(
|
||||||
__file__, A.__repr__.__code__.co_firstlineno + 1
|
__file__, A.__repr__.__code__.co_firstlineno + 1
|
||||||
|
@ -752,6 +877,7 @@ class TestAssert_reprcompare:
|
||||||
def test_unicode(self) -> None:
|
def test_unicode(self) -> None:
|
||||||
assert callequal("£€", "£") == [
|
assert callequal("£€", "£") == [
|
||||||
"'£€' == '£'",
|
"'£€' == '£'",
|
||||||
|
"",
|
||||||
"- £",
|
"- £",
|
||||||
"+ £€",
|
"+ £€",
|
||||||
]
|
]
|
||||||
|
@ -767,7 +893,7 @@ class TestAssert_reprcompare:
|
||||||
return "\xff"
|
return "\xff"
|
||||||
|
|
||||||
expl = callequal(A(), "1")
|
expl = callequal(A(), "1")
|
||||||
assert expl == ["ÿ == '1'", "- 1"]
|
assert expl == ["ÿ == '1'", "", "- 1"]
|
||||||
|
|
||||||
def test_format_nonascii_explanation(self) -> None:
|
def test_format_nonascii_explanation(self) -> None:
|
||||||
assert util.format_explanation("λ")
|
assert util.format_explanation("λ")
|
||||||
|
@ -790,6 +916,7 @@ class TestAssert_reprcompare:
|
||||||
expl = callequal(left, right)
|
expl = callequal(left, right)
|
||||||
assert expl == [
|
assert expl == [
|
||||||
r"'hyv\xe4' == 'hyva\u0308'",
|
r"'hyv\xe4' == 'hyva\u0308'",
|
||||||
|
"",
|
||||||
f"- {str(right)}",
|
f"- {str(right)}",
|
||||||
f"+ {str(left)}",
|
f"+ {str(left)}",
|
||||||
]
|
]
|
||||||
|
@ -797,6 +924,7 @@ class TestAssert_reprcompare:
|
||||||
expl = callequal(left, right, verbose=2)
|
expl = callequal(left, right, verbose=2)
|
||||||
assert expl == [
|
assert expl == [
|
||||||
r"'hyv\xe4' == 'hyva\u0308'",
|
r"'hyv\xe4' == 'hyva\u0308'",
|
||||||
|
"",
|
||||||
f"- {str(right)}",
|
f"- {str(right)}",
|
||||||
f"+ {str(left)}",
|
f"+ {str(left)}",
|
||||||
]
|
]
|
||||||
|
@ -1085,6 +1213,7 @@ class TestAssert_reprcompare_namedtuple:
|
||||||
# Because the types are different, uses the generic sequence matcher.
|
# Because the types are different, uses the generic sequence matcher.
|
||||||
assert lines == [
|
assert lines == [
|
||||||
"NT1(a=1, b='b') == NT2(a=2, b='b')",
|
"NT1(a=1, b='b') == NT2(a=2, b='b')",
|
||||||
|
"",
|
||||||
"At index 0 diff: 1 != 2",
|
"At index 0 diff: 1 != 2",
|
||||||
"Use -v to get more diff",
|
"Use -v to get more diff",
|
||||||
]
|
]
|
||||||
|
@ -1272,7 +1401,7 @@ class TestTruncateExplanation:
|
||||||
|
|
||||||
line_count = 7
|
line_count = 7
|
||||||
line_len = 100
|
line_len = 100
|
||||||
expected_truncated_lines = 1
|
expected_truncated_lines = 2
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
r"""
|
r"""
|
||||||
def test_many_lines():
|
def test_many_lines():
|
||||||
|
@ -1292,8 +1421,7 @@ class TestTruncateExplanation:
|
||||||
[
|
[
|
||||||
"*+ 1*",
|
"*+ 1*",
|
||||||
"*+ 3*",
|
"*+ 3*",
|
||||||
"*+ 5*",
|
"*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines,
|
||||||
"*truncated (%d line hidden)*use*-vv*" % expected_truncated_lines,
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1336,6 +1464,7 @@ def test_rewritten(pytester: Pytester) -> None:
|
||||||
def test_reprcompare_notin() -> None:
|
def test_reprcompare_notin() -> None:
|
||||||
assert callop("not in", "foo", "aaafoobbb") == [
|
assert callop("not in", "foo", "aaafoobbb") == [
|
||||||
"'foo' not in 'aaafoobbb'",
|
"'foo' not in 'aaafoobbb'",
|
||||||
|
"",
|
||||||
"'foo' is contained here:",
|
"'foo' is contained here:",
|
||||||
" aaafoobbb",
|
" aaafoobbb",
|
||||||
"? +++",
|
"? +++",
|
||||||
|
@ -1345,6 +1474,7 @@ def test_reprcompare_notin() -> None:
|
||||||
def test_reprcompare_whitespaces() -> None:
|
def test_reprcompare_whitespaces() -> None:
|
||||||
assert callequal("\r\n", "\n") == [
|
assert callequal("\r\n", "\n") == [
|
||||||
r"'\r\n' == '\n'",
|
r"'\r\n' == '\n'",
|
||||||
|
"",
|
||||||
r"Strings contain only whitespace, escaping them using repr()",
|
r"Strings contain only whitespace, escaping them using repr()",
|
||||||
r"- '\n'",
|
r"- '\n'",
|
||||||
r"+ '\r\n'",
|
r"+ '\r\n'",
|
||||||
|
@ -1803,8 +1933,9 @@ def test_reprcompare_verbose_long() -> None:
|
||||||
assert [0, 1] == [0, 2]
|
assert [0, 1] == [0, 2]
|
||||||
""",
|
""",
|
||||||
[
|
[
|
||||||
"{bold}{red}E {light-red}- [0, 2]{hl-reset}{endline}{reset}",
|
"{bold}{red}E At index 1 diff: {reset}{number}1{hl-reset}{endline} != {reset}{number}2*",
|
||||||
"{bold}{red}E {light-green}+ [0, 1]{hl-reset}{endline}{reset}",
|
"{bold}{red}E {light-red}- 2,{hl-reset}{endline}{reset}",
|
||||||
|
"{bold}{red}E {light-green}+ 1,{hl-reset}{endline}{reset}",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -1815,7 +1946,13 @@ def test_reprcompare_verbose_long() -> None:
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
[
|
[
|
||||||
"{bold}{red}E {light-gray} {hl-reset} {{{endline}{reset}",
|
"{bold}{red}E Common items:{reset}",
|
||||||
|
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-1{hl-reset}{str}'{hl-reset}: {number}1*",
|
||||||
|
"{bold}{red}E Left contains 1 more item:{reset}",
|
||||||
|
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-5{hl-reset}{str}'{hl-reset}: {number}5*",
|
||||||
|
"{bold}{red}E Right contains 1 more item:{reset}",
|
||||||
|
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-0{hl-reset}{str}'{hl-reset}: {number}0*",
|
||||||
|
"{bold}{red}E {reset}{light-gray} {hl-reset} {{{endline}{reset}",
|
||||||
"{bold}{red}E {light-gray} {hl-reset} 'number-is-1': 1,{endline}{reset}",
|
"{bold}{red}E {light-gray} {hl-reset} 'number-is-1': 1,{endline}{reset}",
|
||||||
"{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}",
|
"{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}",
|
||||||
],
|
],
|
||||||
|
@ -1836,3 +1973,72 @@ def test_comparisons_handle_colors(
|
||||||
)
|
)
|
||||||
|
|
||||||
result.stdout.fnmatch_lines(formatter(expected_lines), consecutive=False)
|
result.stdout.fnmatch_lines(formatter(expected_lines), consecutive=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fine_grained_assertion_verbosity(pytester: Pytester):
|
||||||
|
long_text = "Lorem ipsum dolor sit amet " * 10
|
||||||
|
p = pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
def test_ok():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_words_fail():
|
||||||
|
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
|
||||||
|
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
|
||||||
|
assert fruits1 == fruits2
|
||||||
|
|
||||||
|
|
||||||
|
def test_numbers_fail():
|
||||||
|
number_to_text1 = {{str(x): x for x in range(5)}}
|
||||||
|
number_to_text2 = {{str(x * 10): x * 10 for x in range(5)}}
|
||||||
|
assert number_to_text1 == number_to_text2
|
||||||
|
|
||||||
|
|
||||||
|
def test_long_text_fail():
|
||||||
|
long_text = "{long_text}"
|
||||||
|
assert "hello world" in long_text
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
verbosity_assertions = 2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest(p)
|
||||||
|
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
f"{p.name} .FFF [100%]",
|
||||||
|
"E At index 2 diff: 'grapes' != 'orange'",
|
||||||
|
"E Full diff:",
|
||||||
|
"E [",
|
||||||
|
"E 'banana',",
|
||||||
|
"E 'apple',",
|
||||||
|
"E - 'orange',",
|
||||||
|
"E ? ^ ^^",
|
||||||
|
"E + 'grapes',",
|
||||||
|
"E ? ^ ^ +",
|
||||||
|
"E 'melon',",
|
||||||
|
"E 'kiwi',",
|
||||||
|
"E ]",
|
||||||
|
"E Full diff:",
|
||||||
|
"E {",
|
||||||
|
"E '0': 0,",
|
||||||
|
"E - '10': 10,",
|
||||||
|
"E ? - -",
|
||||||
|
"E + '1': 1,",
|
||||||
|
"E - '20': 20,",
|
||||||
|
"E ? - -",
|
||||||
|
"E + '2': 2,",
|
||||||
|
"E - '30': 30,",
|
||||||
|
"E ? - -",
|
||||||
|
"E + '3': 3,",
|
||||||
|
"E - '40': 40,",
|
||||||
|
"E ? - -",
|
||||||
|
"E + '4': 4,",
|
||||||
|
"E }",
|
||||||
|
f"E AssertionError: assert 'hello world' in '{long_text}'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -2056,13 +2056,15 @@ class TestReprSizeVerbosity:
|
||||||
)
|
)
|
||||||
def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
|
def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
|
||||||
class FakeConfig:
|
class FakeConfig:
|
||||||
def getoption(self, name: str) -> int:
|
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||||
assert name == "verbose"
|
|
||||||
return verbose
|
return verbose
|
||||||
|
|
||||||
config = FakeConfig()
|
config = FakeConfig()
|
||||||
assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size
|
assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size
|
||||||
|
|
||||||
|
def test_get_maxsize_for_saferepr_no_config(self) -> None:
|
||||||
|
assert _get_maxsize_for_saferepr(None) == DEFAULT_REPR_MAX_SIZE
|
||||||
|
|
||||||
def create_test_file(self, pytester: Pytester, size: int) -> None:
|
def create_test_file(self, pytester: Pytester, size: int) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
f"""
|
f"""
|
||||||
|
|
|
@ -99,7 +99,8 @@ class TestCollector:
|
||||||
conftest="""
|
conftest="""
|
||||||
import pytest
|
import pytest
|
||||||
class CustomFile(pytest.File):
|
class CustomFile(pytest.File):
|
||||||
pass
|
def collect(self):
|
||||||
|
return []
|
||||||
def pytest_collect_file(file_path, parent):
|
def pytest_collect_file(file_path, parent):
|
||||||
if file_path.suffix == ".xxx":
|
if file_path.suffix == ".xxx":
|
||||||
return CustomFile.from_parent(path=file_path, parent=parent)
|
return CustomFile.from_parent(path=file_path, parent=parent)
|
||||||
|
@ -1509,6 +1510,9 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) ->
|
||||||
super().__init__(*k, **kw)
|
super().__init__(*k, **kw)
|
||||||
self.x = x
|
self.x = x
|
||||||
|
|
||||||
|
def collect(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
collector = MyCollector.from_parent(
|
collector = MyCollector.from_parent(
|
||||||
parent=request.session, path=pytester.path / "foo", x=10
|
parent=request.session, path=pytester.path / "foo", x=10
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
@ -21,6 +22,8 @@ from _pytest.config import Config
|
||||||
from _pytest.config import ConftestImportFailure
|
from _pytest.config import ConftestImportFailure
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import parse_warning_filter
|
from _pytest.config import parse_warning_filter
|
||||||
|
from _pytest.config.argparsing import get_ini_default_for_type
|
||||||
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
from _pytest.config.findpaths import determine_setup
|
from _pytest.config.findpaths import determine_setup
|
||||||
from _pytest.config.findpaths import get_common_ancestor
|
from _pytest.config.findpaths import get_common_ancestor
|
||||||
|
@ -857,6 +860,68 @@ class TestConfigAPI:
|
||||||
assert len(values) == 2
|
assert len(values) == 2
|
||||||
assert values == ["456", "123"]
|
assert values == ["456", "123"]
|
||||||
|
|
||||||
|
def test_addini_default_values(self, pytester: Pytester) -> None:
|
||||||
|
"""Tests the default values for configuration based on
|
||||||
|
config type
|
||||||
|
"""
|
||||||
|
|
||||||
|
pytester.makeconftest(
|
||||||
|
"""
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addini("linelist1", "", type="linelist")
|
||||||
|
parser.addini("paths1", "", type="paths")
|
||||||
|
parser.addini("pathlist1", "", type="pathlist")
|
||||||
|
parser.addini("args1", "", type="args")
|
||||||
|
parser.addini("bool1", "", type="bool")
|
||||||
|
parser.addini("string1", "", type="string")
|
||||||
|
parser.addini("none_1", "", type="linelist", default=None)
|
||||||
|
parser.addini("none_2", "", default=None)
|
||||||
|
parser.addini("no_type", "")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
config = pytester.parseconfig()
|
||||||
|
# default for linelist, paths, pathlist and args is []
|
||||||
|
value = config.getini("linelist1")
|
||||||
|
assert value == []
|
||||||
|
value = config.getini("paths1")
|
||||||
|
assert value == []
|
||||||
|
value = config.getini("pathlist1")
|
||||||
|
assert value == []
|
||||||
|
value = config.getini("args1")
|
||||||
|
assert value == []
|
||||||
|
# default for bool is False
|
||||||
|
value = config.getini("bool1")
|
||||||
|
assert value is False
|
||||||
|
# default for string is ""
|
||||||
|
value = config.getini("string1")
|
||||||
|
assert value == ""
|
||||||
|
# should return None if None is explicity set as default value
|
||||||
|
# irrespective of the type argument
|
||||||
|
value = config.getini("none_1")
|
||||||
|
assert value is None
|
||||||
|
value = config.getini("none_2")
|
||||||
|
assert value is None
|
||||||
|
# in case no type is provided and no default set
|
||||||
|
# treat it as string and default value will be ""
|
||||||
|
value = config.getini("no_type")
|
||||||
|
assert value == ""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"type, expected",
|
||||||
|
[
|
||||||
|
pytest.param(None, "", id="None"),
|
||||||
|
pytest.param("string", "", id="string"),
|
||||||
|
pytest.param("paths", [], id="paths"),
|
||||||
|
pytest.param("pathlist", [], id="pathlist"),
|
||||||
|
pytest.param("args", [], id="args"),
|
||||||
|
pytest.param("linelist", [], id="linelist"),
|
||||||
|
pytest.param("bool", False, id="bool"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_ini_default_for_type(self, type: Any, expected: Any) -> None:
|
||||||
|
assert get_ini_default_for_type(type) == expected
|
||||||
|
|
||||||
def test_confcutdir_check_isdir(self, pytester: Pytester) -> None:
|
def test_confcutdir_check_isdir(self, pytester: Pytester) -> None:
|
||||||
"""Give an error if --confcutdir is not a valid directory (#2078)"""
|
"""Give an error if --confcutdir is not a valid directory (#2078)"""
|
||||||
exp_match = r"^--confcutdir must be a directory, given: "
|
exp_match = r"^--confcutdir must be a directory, given: "
|
||||||
|
@ -1894,16 +1959,6 @@ def test_invocation_args(pytester: Pytester) -> None:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None:
|
def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None:
|
||||||
if plugin == "debugging":
|
|
||||||
# Fixed in xdist (after 1.27.0).
|
|
||||||
# https://github.com/pytest-dev/pytest-xdist/pull/422
|
|
||||||
try:
|
|
||||||
import xdist # noqa: F401
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
pytest.skip("does not work with xdist currently")
|
|
||||||
|
|
||||||
p = pytester.makepyfile("def test(): pass")
|
p = pytester.makepyfile("def test(): pass")
|
||||||
result = pytester.runpytest(str(p), "-pno:%s" % plugin)
|
result = pytester.runpytest(str(p), "-pno:%s" % plugin)
|
||||||
|
|
||||||
|
@ -2181,3 +2236,76 @@ class TestDebugOptions:
|
||||||
"*Default: pytestdebug.log.",
|
"*Default: pytestdebug.log.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerbosity:
|
||||||
|
SOME_OUTPUT_TYPE = Config.VERBOSITY_ASSERTIONS
|
||||||
|
SOME_OUTPUT_VERBOSITY_LEVEL = 5
|
||||||
|
|
||||||
|
class VerbosityIni:
|
||||||
|
def pytest_addoption(self, parser: Parser) -> None:
|
||||||
|
Config._add_verbosity_ini(
|
||||||
|
parser, TestVerbosity.SOME_OUTPUT_TYPE, help="some help text"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_level_matches_verbose_when_not_specified(
|
||||||
|
self, pytester: Pytester, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
tmp_path.joinpath("pytest.ini").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
[pytest]
|
||||||
|
addopts = --verbose
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
pytester.plugins = [TestVerbosity.VerbosityIni()]
|
||||||
|
|
||||||
|
config = pytester.parseconfig(tmp_path)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE)
|
||||||
|
== config.option.verbose
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_level_matches_verbose_when_not_known_type(
|
||||||
|
self, pytester: Pytester, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
tmp_path.joinpath("pytest.ini").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
[pytest]
|
||||||
|
addopts = --verbose
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
pytester.plugins = [TestVerbosity.VerbosityIni()]
|
||||||
|
|
||||||
|
config = pytester.parseconfig(tmp_path)
|
||||||
|
|
||||||
|
assert config.get_verbosity("some fake verbosity type") == config.option.verbose
|
||||||
|
|
||||||
|
def test_level_matches_specified_override(
|
||||||
|
self, pytester: Pytester, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
setting_name = f"verbosity_{TestVerbosity.SOME_OUTPUT_TYPE}"
|
||||||
|
tmp_path.joinpath("pytest.ini").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
[pytest]
|
||||||
|
addopts = --verbose
|
||||||
|
{setting_name} = {TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL}
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
pytester.plugins = [TestVerbosity.VerbosityIni()]
|
||||||
|
|
||||||
|
config = pytester.parseconfig(tmp_path)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE)
|
||||||
|
== TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL
|
||||||
|
)
|
||||||
|
|
|
@ -21,10 +21,14 @@ TESTCASES = [
|
||||||
E assert [1, 4, 3] == [1, 2, 3]
|
E assert [1, 4, 3] == [1, 2, 3]
|
||||||
E At index 1 diff: 4 != 2
|
E At index 1 diff: 4 != 2
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - [1, 2, 3]
|
E [
|
||||||
|
E 1,
|
||||||
|
E - 2,
|
||||||
E ? ^
|
E ? ^
|
||||||
E + [1, 4, 3]
|
E + 4,
|
||||||
E ? ^
|
E ? ^
|
||||||
|
E 3,
|
||||||
|
E ]
|
||||||
""",
|
""",
|
||||||
id="Compare lists, one item differs",
|
id="Compare lists, one item differs",
|
||||||
),
|
),
|
||||||
|
@ -40,9 +44,11 @@ TESTCASES = [
|
||||||
E assert [1, 2, 3] == [1, 2]
|
E assert [1, 2, 3] == [1, 2]
|
||||||
E Left contains one more item: 3
|
E Left contains one more item: 3
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - [1, 2]
|
E [
|
||||||
E + [1, 2, 3]
|
E 1,
|
||||||
E ? +++
|
E 2,
|
||||||
|
E + 3,
|
||||||
|
E ]
|
||||||
""",
|
""",
|
||||||
id="Compare lists, one extra item",
|
id="Compare lists, one extra item",
|
||||||
),
|
),
|
||||||
|
@ -59,9 +65,11 @@ TESTCASES = [
|
||||||
E At index 1 diff: 3 != 2
|
E At index 1 diff: 3 != 2
|
||||||
E Right contains one more item: 3
|
E Right contains one more item: 3
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - [1, 2, 3]
|
E [
|
||||||
E ? ---
|
E 1,
|
||||||
E + [1, 3]
|
E - 2,
|
||||||
|
E 3,
|
||||||
|
E ]
|
||||||
""",
|
""",
|
||||||
id="Compare lists, one item missing",
|
id="Compare lists, one item missing",
|
||||||
),
|
),
|
||||||
|
@ -77,10 +85,14 @@ TESTCASES = [
|
||||||
E assert (1, 4, 3) == (1, 2, 3)
|
E assert (1, 4, 3) == (1, 2, 3)
|
||||||
E At index 1 diff: 4 != 2
|
E At index 1 diff: 4 != 2
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - (1, 2, 3)
|
E (
|
||||||
|
E 1,
|
||||||
|
E - 2,
|
||||||
E ? ^
|
E ? ^
|
||||||
E + (1, 4, 3)
|
E + 4,
|
||||||
E ? ^
|
E ? ^
|
||||||
|
E 3,
|
||||||
|
E )
|
||||||
""",
|
""",
|
||||||
id="Compare tuples",
|
id="Compare tuples",
|
||||||
),
|
),
|
||||||
|
@ -99,10 +111,12 @@ TESTCASES = [
|
||||||
E Extra items in the right set:
|
E Extra items in the right set:
|
||||||
E 2
|
E 2
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - {1, 2, 3}
|
E {
|
||||||
E ? ^ ^
|
E 1,
|
||||||
E + {1, 3, 4}
|
E - 2,
|
||||||
E ? ^ ^
|
E 3,
|
||||||
|
E + 4,
|
||||||
|
E }
|
||||||
""",
|
""",
|
||||||
id="Compare sets",
|
id="Compare sets",
|
||||||
),
|
),
|
||||||
|
@ -123,10 +137,13 @@ TESTCASES = [
|
||||||
E Right contains 1 more item:
|
E Right contains 1 more item:
|
||||||
E {2: 'eggs'}
|
E {2: 'eggs'}
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - {1: 'spam', 2: 'eggs'}
|
E {
|
||||||
|
E 1: 'spam',
|
||||||
|
E - 2: 'eggs',
|
||||||
E ? ^
|
E ? ^
|
||||||
E + {1: 'spam', 3: 'eggs'}
|
E + 3: 'eggs',
|
||||||
E ? ^
|
E ? ^
|
||||||
|
E }
|
||||||
""",
|
""",
|
||||||
id="Compare dicts with differing keys",
|
id="Compare dicts with differing keys",
|
||||||
),
|
),
|
||||||
|
@ -145,10 +162,11 @@ TESTCASES = [
|
||||||
E Differing items:
|
E Differing items:
|
||||||
E {2: 'eggs'} != {2: 'bacon'}
|
E {2: 'eggs'} != {2: 'bacon'}
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - {1: 'spam', 2: 'bacon'}
|
E {
|
||||||
E ? ^^^^^
|
E 1: 'spam',
|
||||||
E + {1: 'spam', 2: 'eggs'}
|
E - 2: 'bacon',
|
||||||
E ? ^^^^
|
E + 2: 'eggs',
|
||||||
|
E }
|
||||||
""",
|
""",
|
||||||
id="Compare dicts with differing values",
|
id="Compare dicts with differing values",
|
||||||
),
|
),
|
||||||
|
@ -169,10 +187,11 @@ TESTCASES = [
|
||||||
E Right contains 1 more item:
|
E Right contains 1 more item:
|
||||||
E {3: 'bacon'}
|
E {3: 'bacon'}
|
||||||
E Full diff:
|
E Full diff:
|
||||||
E - {1: 'spam', 3: 'bacon'}
|
E {
|
||||||
E ? ^ ^^^^^
|
E 1: 'spam',
|
||||||
E + {1: 'spam', 2: 'eggs'}
|
E - 3: 'bacon',
|
||||||
E ? ^ ^^^^
|
E + 2: 'eggs',
|
||||||
|
E }
|
||||||
""",
|
""",
|
||||||
id="Compare dicts with differing items",
|
id="Compare dicts with differing items",
|
||||||
),
|
),
|
||||||
|
|
|
@ -73,6 +73,12 @@ def test_subclassing_both_item_and_collector_deprecated(
|
||||||
"""Legacy ctor with legacy call # don't wana see"""
|
"""Legacy ctor with legacy call # don't wana see"""
|
||||||
super().__init__(fspath, parent)
|
super().__init__(fspath, parent)
|
||||||
|
|
||||||
|
def collect(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def runtest(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
with pytest.warns(PytestWarning) as rec:
|
with pytest.warns(PytestWarning) as rec:
|
||||||
SoWrong.from_parent(
|
SoWrong.from_parent(
|
||||||
request.session, fspath=legacy_path(tmp_path / "broken.txt")
|
request.session, fspath=legacy_path(tmp_path / "broken.txt")
|
||||||
|
|
|
@ -290,10 +290,10 @@ class TestParser:
|
||||||
|
|
||||||
|
|
||||||
def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
||||||
try:
|
if sys.version_info >= (3, 11):
|
||||||
# New in Python 3.11, ignores utf-8 mode
|
# New in Python 3.11, ignores utf-8 mode
|
||||||
encoding = locale.getencoding() # type: ignore[attr-defined]
|
encoding = locale.getencoding()
|
||||||
except AttributeError:
|
else:
|
||||||
encoding = locale.getpreferredencoding(False)
|
encoding = locale.getpreferredencoding(False)
|
||||||
try:
|
try:
|
||||||
bash_version = subprocess.run(
|
bash_version = subprocess.run(
|
||||||
|
|
|
@ -1268,13 +1268,13 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None:
|
||||||
"=*= FAILURES =*=",
|
"=*= FAILURES =*=",
|
||||||
"{red}{bold}_*_ test_this _*_{reset}",
|
"{red}{bold}_*_ test_this _*_{reset}",
|
||||||
"",
|
"",
|
||||||
" {kw}def{hl-reset} {function}test_this{hl-reset}():{endline}",
|
" {reset}{kw}def{hl-reset} {function}test_this{hl-reset}():{endline}",
|
||||||
"> fail(){endline}",
|
"> fail(){endline}",
|
||||||
"",
|
"",
|
||||||
"{bold}{red}test_color_yes.py{reset}:5: ",
|
"{bold}{red}test_color_yes.py{reset}:5: ",
|
||||||
"_ _ * _ _*",
|
"_ _ * _ _*",
|
||||||
"",
|
"",
|
||||||
" {kw}def{hl-reset} {function}fail{hl-reset}():{endline}",
|
" {reset}{kw}def{hl-reset} {function}fail{hl-reset}():{endline}",
|
||||||
"> {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
"> {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||||
"{bold}{red}E assert 0{reset}",
|
"{bold}{red}E assert 0{reset}",
|
||||||
"",
|
"",
|
||||||
|
@ -1295,9 +1295,9 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None:
|
||||||
"=*= FAILURES =*=",
|
"=*= FAILURES =*=",
|
||||||
"{red}{bold}_*_ test_this _*_{reset}",
|
"{red}{bold}_*_ test_this _*_{reset}",
|
||||||
"{bold}{red}test_color_yes.py{reset}:5: in test_this",
|
"{bold}{red}test_color_yes.py{reset}:5: in test_this",
|
||||||
" fail(){endline}",
|
" {reset}fail(){endline}",
|
||||||
"{bold}{red}test_color_yes.py{reset}:2: in fail",
|
"{bold}{red}test_color_yes.py{reset}:2: in fail",
|
||||||
" {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
" {reset}{kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||||
"{bold}{red}E assert 0{reset}",
|
"{bold}{red}E assert 0{reset}",
|
||||||
"{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}",
|
"{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}",
|
||||||
]
|
]
|
||||||
|
@ -1802,7 +1802,7 @@ def test_terminal_no_summary_warnings_header_once(pytester: Pytester) -> None:
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def tr() -> TerminalReporter:
|
def tr() -> TerminalReporter:
|
||||||
config = _pytest.config._prepareconfig()
|
config = _pytest.config._prepareconfig([])
|
||||||
return TerminalReporter(config)
|
return TerminalReporter(config)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2507,7 +2507,7 @@ class TestCodeHighlight:
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
color_mapping.format_for_fnmatch(
|
color_mapping.format_for_fnmatch(
|
||||||
[
|
[
|
||||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
" {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
|
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
|
||||||
"{bold}{red}E assert 1 == 10{reset}",
|
"{bold}{red}E assert 1 == 10{reset}",
|
||||||
]
|
]
|
||||||
|
@ -2529,7 +2529,7 @@ class TestCodeHighlight:
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
color_mapping.format_for_fnmatch(
|
color_mapping.format_for_fnmatch(
|
||||||
[
|
[
|
||||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
" {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||||
" {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}",
|
" {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}",
|
||||||
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||||
"{bold}{red}E assert 0{reset}",
|
"{bold}{red}E assert 0{reset}",
|
||||||
|
@ -2552,7 +2552,7 @@ class TestCodeHighlight:
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
color_mapping.format_for_fnmatch(
|
color_mapping.format_for_fnmatch(
|
||||||
[
|
[
|
||||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
" {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
|
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
|
||||||
"{bold}{red}E assert 1 == 10{reset}",
|
"{bold}{red}E assert 1 == 10{reset}",
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue