Merge branch 'main' into whitespacediff

This commit is contained in:
OlegP-andrew 2023-12-11 17:49:53 -05:00 committed by GitHub
commit 9e07485254
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1203 additions and 744 deletions

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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') }}

View File

@ -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

View File

@ -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.7.0 rev: v1.7.1
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)

View File

@ -188,6 +188,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

View File

@ -1 +1,3 @@
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.

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1 @@
Fixed the selftests to pass correctly if ``FORCE_COLOR``, ``NO_COLOR`` or ``PY_COLORS`` is set in the calling environment.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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"))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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`:

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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`.

File diff suppressed because it is too large Load Diff

View File

@ -612,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
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -801,6 +821,7 @@ Node
.. autoclass:: _pytest.nodes.Node() .. autoclass:: _pytest.nodes.Node()
:members: :members:
:show-inheritance:
Collector Collector
~~~~~~~~~ ~~~~~~~~~
@ -1137,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`:

View File

@ -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",
] ]

View File

@ -1,4 +0,0 @@
from setuptools import setup
if __name__ == "__main__":
setup()

View File

@ -15,14 +15,17 @@
import collections as _collections import collections as _collections
import dataclasses as _dataclasses import dataclasses as _dataclasses
import re import re
import sys as _sys
import types as _types import types as _types
from io import StringIO as _StringIO from io import StringIO as _StringIO
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import IO from typing import IO
from typing import Iterator
from typing import List from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
class _safe_key: class _safe_key:
@ -58,15 +61,13 @@ def _safe_tuple(t):
class PrettyPrinter: class PrettyPrinter:
def __init__( def __init__(
self, self,
indent=1, indent: int = 4,
width=80, width: int = 80,
depth=None, depth: Optional[int] = None,
stream=None,
*, *,
compact=False, sort_dicts: bool = True,
sort_dicts=True, underscore_numbers: bool = False,
underscore_numbers=False, ) -> None:
):
"""Handle pretty printing operations onto a stream using a set of """Handle pretty printing operations onto a stream using a set of
configured parameters. configured parameters.
@ -79,13 +80,6 @@ class PrettyPrinter:
depth depth
The maximum depth to print out nested structures. The maximum depth to print out nested structures.
stream
The desired output stream. If omitted (or false), the standard
output stream available at construction will be used.
compact
If true, several items will be combined in one line.
sort_dicts sort_dicts
If true, dict keys are sorted. If true, dict keys are sorted.
@ -101,32 +95,33 @@ class PrettyPrinter:
self._depth = depth self._depth = depth
self._indent_per_level = indent self._indent_per_level = indent
self._width = width self._width = width
if stream is not None:
self._stream = stream
else:
self._stream = _sys.stdout
self._compact = bool(compact)
self._sort_dicts = sort_dicts self._sort_dicts = sort_dicts
self._underscore_numbers = underscore_numbers self._underscore_numbers = underscore_numbers
def pformat(self, object: Any) -> str: def pformat(self, object: Any) -> str:
sio = _StringIO() sio = _StringIO()
self._format(object, sio, 0, 0, {}, 0) self._format(object, sio, 0, 0, set(), 0)
return sio.getvalue() return sio.getvalue()
def _format(self, object, stream, indent, allowance, context, level): def _format(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
objid = id(object) objid = id(object)
if objid in context: if objid in context:
stream.write(_recursion(object)) stream.write(_recursion(object))
self._recursive = True
self._readable = False
return return
p = self._dispatch.get(type(object).__repr__, None) p = self._dispatch.get(type(object).__repr__, None)
if p is not None: if p is not None:
context[objid] = 1 context.add(objid)
p(self, object, stream, indent, allowance, context, level + 1) p(self, object, stream, indent, allowance, context, level + 1)
del context[objid] context.remove(objid)
elif ( elif (
_dataclasses.is_dataclass(object) _dataclasses.is_dataclass(object)
and not isinstance(object, type) and not isinstance(object, type)
@ -136,17 +131,24 @@ class PrettyPrinter:
hasattr(object.__repr__, "__wrapped__") hasattr(object.__repr__, "__wrapped__")
and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
): ):
context[objid] = 1 context.add(objid)
self._pprint_dataclass( self._pprint_dataclass(
object, stream, indent, allowance, context, level + 1 object, stream, indent, allowance, context, level + 1
) )
del context[objid] context.remove(objid)
else: else:
stream.write(self._repr(object, context, level)) stream.write(self._repr(object, context, level))
def _pprint_dataclass(self, object, stream, indent, allowance, 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__ cls_name = object.__class__.__name__
indent += len(cls_name) + 1
items = [ items = [
(f.name, getattr(object, f.name)) (f.name, getattr(object, f.name))
for f in _dataclasses.fields(object) for f in _dataclasses.fields(object)
@ -158,63 +160,87 @@ class PrettyPrinter:
_dispatch: Dict[ _dispatch: Dict[
Callable[..., str], Callable[..., str],
Callable[["PrettyPrinter", Any, IO[str], int, int, Dict[int, int], int], str], Callable[["PrettyPrinter", Any, IO[str], int, int, Set[int], int], None],
] = {} ] = {}
def _pprint_dict(self, object, stream, indent, allowance, context, level): def _pprint_dict(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
write = stream.write write = stream.write
write("{") write("{")
if self._indent_per_level > 1: if self._sort_dicts:
write((self._indent_per_level - 1) * " ") items = sorted(object.items(), key=_safe_tuple)
length = len(object) else:
if length: items = object.items()
if self._sort_dicts: self._format_dict_items(items, stream, indent, allowance, context, level)
items = sorted(object.items(), key=_safe_tuple)
else:
items = object.items()
self._format_dict_items(
items, stream, indent, allowance + 1, context, level
)
write("}") write("}")
_dispatch[dict.__repr__] = _pprint_dict _dispatch[dict.__repr__] = _pprint_dict
def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): def _pprint_ordered_dict(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if not len(object): if not len(object):
stream.write(repr(object)) stream.write(repr(object))
return return
cls = object.__class__ cls = object.__class__
stream.write(cls.__name__ + "(") stream.write(cls.__name__ + "(")
self._format( self._pprint_dict(object, stream, indent, allowance, context, level)
list(object.items()),
stream,
indent + len(cls.__name__) + 1,
allowance + 1,
context,
level,
)
stream.write(")") stream.write(")")
_dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
def _pprint_list(self, object, stream, indent, allowance, context, level): def _pprint_list(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
stream.write("[") stream.write("[")
self._format_items(object, stream, indent, allowance + 1, context, level) self._format_items(object, stream, indent, allowance, context, level)
stream.write("]") stream.write("]")
_dispatch[list.__repr__] = _pprint_list _dispatch[list.__repr__] = _pprint_list
def _pprint_tuple(self, object, stream, indent, allowance, context, level): def _pprint_tuple(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
stream.write("(") stream.write("(")
endchar = ",)" if len(object) == 1 else ")" self._format_items(object, stream, indent, allowance, context, level)
self._format_items( stream.write(")")
object, stream, indent, allowance + len(endchar), context, level
)
stream.write(endchar)
_dispatch[tuple.__repr__] = _pprint_tuple _dispatch[tuple.__repr__] = _pprint_tuple
def _pprint_set(self, object, stream, indent, allowance, context, level): def _pprint_set(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if not len(object): if not len(object):
stream.write(repr(object)) stream.write(repr(object))
return return
@ -225,17 +251,22 @@ class PrettyPrinter:
else: else:
stream.write(typ.__name__ + "({") stream.write(typ.__name__ + "({")
endchar = "})" endchar = "})"
indent += len(typ.__name__) + 1
object = sorted(object, key=_safe_key) object = sorted(object, key=_safe_key)
self._format_items( self._format_items(object, stream, indent, allowance, context, level)
object, stream, indent, allowance + len(endchar), context, level
)
stream.write(endchar) stream.write(endchar)
_dispatch[set.__repr__] = _pprint_set _dispatch[set.__repr__] = _pprint_set
_dispatch[frozenset.__repr__] = _pprint_set _dispatch[frozenset.__repr__] = _pprint_set
def _pprint_str(self, object, stream, indent, allowance, context, level): def _pprint_str(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
write = stream.write write = stream.write
if not len(object): if not len(object):
write(repr(object)) write(repr(object))
@ -286,7 +317,15 @@ class PrettyPrinter:
_dispatch[str.__repr__] = _pprint_str _dispatch[str.__repr__] = _pprint_str
def _pprint_bytes(self, object, stream, indent, allowance, context, level): def _pprint_bytes(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
write = stream.write write = stream.write
if len(object) <= 4: if len(object) <= 4:
write(repr(object)) write(repr(object))
@ -307,7 +346,15 @@ class PrettyPrinter:
_dispatch[bytes.__repr__] = _pprint_bytes _dispatch[bytes.__repr__] = _pprint_bytes
def _pprint_bytearray(self, object, stream, indent, allowance, context, level): def _pprint_bytearray(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
write = stream.write write = stream.write
write("bytearray(") write("bytearray(")
self._pprint_bytes( self._pprint_bytes(
@ -317,23 +364,36 @@ class PrettyPrinter:
_dispatch[bytearray.__repr__] = _pprint_bytearray _dispatch[bytearray.__repr__] = _pprint_bytearray
def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level): def _pprint_mappingproxy(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
stream.write("mappingproxy(") stream.write("mappingproxy(")
self._format(object.copy(), stream, indent + 13, allowance + 1, context, level) self._format(object.copy(), stream, indent, allowance, context, level)
stream.write(")") stream.write(")")
_dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
def _pprint_simplenamespace( def _pprint_simplenamespace(
self, object, stream, indent, allowance, context, level self,
): object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if type(object) is _types.SimpleNamespace: if type(object) is _types.SimpleNamespace:
# The SimpleNamespace repr is "namespace" instead of the class # The SimpleNamespace repr is "namespace" instead of the class
# name, so we do the same here. For subclasses; use the class name. # name, so we do the same here. For subclasses; use the class name.
cls_name = "namespace" cls_name = "namespace"
else: else:
cls_name = object.__class__.__name__ cls_name = object.__class__.__name__
indent += len(cls_name) + 1
items = object.__dict__.items() items = object.__dict__.items()
stream.write(cls_name + "(") stream.write(cls_name + "(")
self._format_namespace_items(items, stream, indent, allowance, context, level) self._format_namespace_items(items, stream, indent, allowance, context, level)
@ -341,33 +401,47 @@ class PrettyPrinter:
_dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
def _format_dict_items(self, items, stream, indent, allowance, context, level): def _format_dict_items(
write = stream.write self,
indent += self._indent_per_level items: List[Tuple[Any, Any]],
delimnl = ",\n" + " " * indent stream: IO[str],
last_index = len(items) - 1 indent: int,
for i, (key, ent) in enumerate(items): allowance: int,
last = i == last_index context: Set[int],
rep = self._repr(key, context, level) level: int,
write(rep) ) -> None:
write(": ") if not items:
self._format( return
ent,
stream,
indent + len(rep) + 2,
allowance if last else 1,
context,
level,
)
if not last:
write(delimnl)
def _format_namespace_items(self, items, stream, indent, allowance, context, level):
write = stream.write write = stream.write
delimnl = ",\n" + " " * indent item_indent = indent + self._indent_per_level
last_index = len(items) - 1 delimnl = "\n" + " " * item_indent
for i, (key, ent) in enumerate(items): for key, ent in items:
last = i == last_index 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(key)
write("=") write("=")
if id(ent) in context: if id(ent) in context:
@ -378,174 +452,185 @@ class PrettyPrinter:
self._format( self._format(
ent, ent,
stream, stream,
indent + len(key) + 1, item_indent + len(key) + 1,
allowance if last else 1, 1,
context, context,
level, level,
) )
if not last:
write(delimnl)
def _format_items(self, items, stream, indent, allowance, context, level): write(",")
write = stream.write
indent += self._indent_per_level write("\n" + " " * indent)
if self._indent_per_level > 1:
write((self._indent_per_level - 1) * " ") def _format_items(
delimnl = ",\n" + " " * indent self,
delim = "" items: List[Any],
width = max_width = self._width - indent + 1 stream: IO[str],
it = iter(items) indent: int,
try: allowance: int,
next_ent = next(it) context: Set[int],
except StopIteration: level: int,
) -> None:
if not items:
return return
last = False
while not last:
ent = next_ent
try:
next_ent = next(it)
except StopIteration:
last = True
max_width -= allowance
width -= allowance
if self._compact:
rep = self._repr(ent, context, level)
w = len(rep) + 2
if width < w:
width = max_width
if delim:
delim = delimnl
if width >= w:
width -= w
write(delim)
delim = ", "
write(rep)
continue
write(delim)
delim = delimnl
self._format(ent, stream, indent, allowance if last else 1, context, level)
def _repr(self, object, context, level): write = stream.write
repr, readable, recursive = self.format( item_indent = indent + self._indent_per_level
object, context.copy(), self._depth, level delimnl = "\n" + " " * item_indent
)
if not readable:
self._readable = False
if recursive:
self._recursive = True
return repr
def format(self, object, context, maxlevels, level): for item in items:
"""Format object for a specific context, returning a string write(delimnl)
and flags indicating whether the representation is 'readable' self._format(item, stream, item_indent, 1, context, level)
and whether the object represents a recursive construct. write(",")
"""
write("\n" + " " * indent)
def _repr(self, object: Any, context: Set[int], level: int) -> str:
return self.format(object, context.copy(), self._depth, level)
def format(
self, object: Any, context: Set[int], maxlevels: Optional[int], level: int
) -> str:
return self._safe_repr(object, context, maxlevels, level) return self._safe_repr(object, context, maxlevels, level)
def _pprint_default_dict(self, object, stream, indent, allowance, context, level): def _pprint_default_dict(
if not len(object): self,
stream.write(repr(object)) object: Any,
return stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
rdf = self._repr(object.default_factory, context, level) rdf = self._repr(object.default_factory, context, level)
cls = object.__class__ stream.write(f"{object.__class__.__name__}({rdf}, ")
indent += len(cls.__name__) + 1 self._pprint_dict(object, stream, indent, allowance, context, level)
stream.write(f"{cls.__name__}({rdf},\n{' ' * indent}")
self._pprint_dict(object, stream, indent, allowance + 1, context, level)
stream.write(")") stream.write(")")
_dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
def _pprint_counter(self, object, stream, indent, allowance, context, level): def _pprint_counter(
if not len(object): self,
stream.write(repr(object)) object: Any,
return stream: IO[str],
cls = object.__class__ indent: int,
stream.write(cls.__name__ + "({") allowance: int,
if self._indent_per_level > 1: context: Set[int],
stream.write((self._indent_per_level - 1) * " ") level: int,
items = object.most_common() ) -> None:
self._format_dict_items( stream.write(object.__class__.__name__ + "(")
items, stream, indent + len(cls.__name__) + 1, allowance + 2, context, level
) if object:
stream.write("})") 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 _dispatch[_collections.Counter.__repr__] = _pprint_counter
def _pprint_chain_map(self, object, stream, indent, allowance, context, level): def _pprint_chain_map(
if not len(object.maps): 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)) stream.write(repr(object))
return return
cls = object.__class__
stream.write(cls.__name__ + "(") stream.write(object.__class__.__name__ + "(")
indent += len(cls.__name__) + 1 self._format_items(object.maps, stream, indent, allowance, context, level)
for i, m in enumerate(object.maps): stream.write(")")
if i == len(object.maps) - 1:
self._format(m, stream, indent, allowance + 1, context, level)
stream.write(")")
else:
self._format(m, stream, indent, 1, context, level)
stream.write(",\n" + " " * indent)
_dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
def _pprint_deque(self, object, stream, indent, allowance, context, level): def _pprint_deque(
if not len(object): self,
stream.write(repr(object)) object: Any,
return stream: IO[str],
cls = object.__class__ indent: int,
stream.write(cls.__name__ + "(") allowance: int,
indent += len(cls.__name__) + 1 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("[") stream.write("[")
if object.maxlen is None:
self._format_items(object, stream, indent, allowance + 2, context, level) self._format_items(object, stream, indent, allowance + 1, context, level)
stream.write("])") stream.write("])")
else:
self._format_items(object, stream, indent, 2, context, level)
rml = self._repr(object.maxlen, context, level)
stream.write(f"],\n{' ' * indent}maxlen={rml})")
_dispatch[_collections.deque.__repr__] = _pprint_deque _dispatch[_collections.deque.__repr__] = _pprint_deque
def _pprint_user_dict(self, object, stream, indent, allowance, context, level): 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) self._format(object.data, stream, indent, allowance, context, level - 1)
_dispatch[_collections.UserDict.__repr__] = _pprint_user_dict _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
def _pprint_user_list(self, object, stream, indent, allowance, context, level): 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) self._format(object.data, stream, indent, allowance, context, level - 1)
_dispatch[_collections.UserList.__repr__] = _pprint_user_list _dispatch[_collections.UserList.__repr__] = _pprint_user_list
def _pprint_user_string(self, object, stream, indent, allowance, context, level): 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) self._format(object.data, stream, indent, allowance, context, level - 1)
_dispatch[_collections.UserString.__repr__] = _pprint_user_string _dispatch[_collections.UserString.__repr__] = _pprint_user_string
def _safe_repr(self, object, context, maxlevels, level): def _safe_repr(
# Return triple (repr_string, isreadable, isrecursive). self, object: Any, context: Set[int], maxlevels: Optional[int], level: int
) -> str:
typ = type(object) typ = type(object)
if typ in _builtin_scalars: if typ in _builtin_scalars:
return repr(object), True, False return repr(object)
r = getattr(typ, "__repr__", None) r = getattr(typ, "__repr__", None)
if issubclass(typ, int) and r is int.__repr__: if issubclass(typ, int) and r is int.__repr__:
if self._underscore_numbers: if self._underscore_numbers:
return f"{object:_d}", True, False return f"{object:_d}"
else: else:
return repr(object), True, False return repr(object)
if issubclass(typ, dict) and r is dict.__repr__: if issubclass(typ, dict) and r is dict.__repr__:
if not object: if not object:
return "{}", True, False return "{}"
objid = id(object) objid = id(object)
if maxlevels and level >= maxlevels: if maxlevels and level >= maxlevels:
return "{...}", False, objid in context return "{...}"
if objid in context: if objid in context:
return _recursion(object), False, True return _recursion(object)
context[objid] = 1 context.add(objid)
readable = True
recursive = False
components: List[str] = [] components: List[str] = []
append = components.append append = components.append
level += 1 level += 1
@ -554,61 +639,51 @@ class PrettyPrinter:
else: else:
items = object.items() items = object.items()
for k, v in items: for k, v in items:
krepr, kreadable, krecur = self.format(k, context, maxlevels, level) krepr = self.format(k, context, maxlevels, level)
vrepr, vreadable, vrecur = self.format(v, context, maxlevels, level) vrepr = self.format(v, context, maxlevels, level)
append(f"{krepr}: {vrepr}") append(f"{krepr}: {vrepr}")
readable = readable and kreadable and vreadable context.remove(objid)
if krecur or vrecur: return "{%s}" % ", ".join(components)
recursive = True
del context[objid]
return "{%s}" % ", ".join(components), readable, recursive
if (issubclass(typ, list) and r is list.__repr__) or ( if (issubclass(typ, list) and r is list.__repr__) or (
issubclass(typ, tuple) and r is tuple.__repr__ issubclass(typ, tuple) and r is tuple.__repr__
): ):
if issubclass(typ, list): if issubclass(typ, list):
if not object: if not object:
return "[]", True, False return "[]"
format = "[%s]" format = "[%s]"
elif len(object) == 1: elif len(object) == 1:
format = "(%s,)" format = "(%s,)"
else: else:
if not object: if not object:
return "()", True, False return "()"
format = "(%s)" format = "(%s)"
objid = id(object) objid = id(object)
if maxlevels and level >= maxlevels: if maxlevels and level >= maxlevels:
return format % "...", False, objid in context return format % "..."
if objid in context: if objid in context:
return _recursion(object), False, True return _recursion(object)
context[objid] = 1 context.add(objid)
readable = True
recursive = False
components = [] components = []
append = components.append append = components.append
level += 1 level += 1
for o in object: for o in object:
orepr, oreadable, orecur = self.format(o, context, maxlevels, level) orepr = self.format(o, context, maxlevels, level)
append(orepr) append(orepr)
if not oreadable: context.remove(objid)
readable = False return format % ", ".join(components)
if orecur:
recursive = True
del context[objid]
return format % ", ".join(components), readable, recursive
rep = repr(object) return repr(object)
return rep, (rep and not rep.startswith("<")), False
_builtin_scalars = frozenset({str, bytes, bytearray, float, complex, bool, type(None)}) _builtin_scalars = frozenset({str, bytes, bytearray, float, complex, bool, type(None)})
def _recursion(object): def _recursion(object: Any) -> str:
return f"<Recursion on {type(object).__name__} with id={id(object)}>" return f"<Recursion on {type(object).__name__} with id={id(object)}>"
def _wrap_bytes_repr(object, width, allowance): def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]:
current = b"" current = b""
last = len(object) // 4 * 4 last = len(object) // 4 * 4
for i in range(0, len(object), 4): for i in range(0, len(object), 4):

View File

@ -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
@ -330,18 +332,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],
@ -353,22 +343,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:
printer = PrettyPrinter()
left_formatting = printer.pformat(left).splitlines()
right_formatting = printer.pformat(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(

View File

@ -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__ = ()

View File

@ -449,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).
@ -472,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)

View File

@ -98,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
@ -400,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(

View File

@ -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. if fault_handler_original_stderr_fd_key in config.stash:
faulthandler.enable(file=get_stderr_fileno()) 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:

View File

@ -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:

View File

@ -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``.

View File

@ -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())

View File

@ -522,7 +522,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 +546,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 +564,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]:

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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]],
@ -806,27 +797,25 @@ def raises( # noqa: F811
) -> 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 type, or one of its subclasses. 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. Note that subclasses of the passed exceptions exception types are expected. Note that subclasses of the passed exceptions
will also match. will also match.
:kwparam str | typing.Pattern[str] | None 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, or any of its subclasses:: type, or any of its subclasses::

View File

@ -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.

View File

@ -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"),
) )

View File

@ -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()

View File

@ -40,15 +40,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
DataclassWithOneItem(foo="bar"), DataclassWithOneItem(foo="bar"),
""" """
DataclassWithOneItem(foo='bar') DataclassWithOneItem(
foo='bar',
)
""", """,
id="dataclass-one-item", id="dataclass-one-item",
), ),
pytest.param( pytest.param(
DataclassWithTwoItems(foo="foo", bar="bar"), DataclassWithTwoItems(foo="foo", bar="bar"),
""" """
DataclassWithTwoItems(foo='foo', DataclassWithTwoItems(
bar='bar') foo='foo',
bar='bar',
)
""", """,
id="dataclass-two-items", id="dataclass-two-items",
), ),
@ -60,15 +64,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
{"one": 1}, {"one": 1},
""" """
{'one': 1} {
'one': 1,
}
""", """,
id="dict-one-item", id="dict-one-item",
), ),
pytest.param( pytest.param(
{"one": 1, "two": 2}, {"one": 1, "two": 2},
""" """
{'one': 1, {
'two': 2} 'one': 1,
'two': 2,
}
""", """,
id="dict-two-items", id="dict-two-items",
), ),
@ -76,18 +84,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
OrderedDict({"one": 1}), OrderedDict({"one": 1}),
""" """
OrderedDict([('one', OrderedDict({
1)]) 'one': 1,
})
""", """,
id="ordereddict-one-item", id="ordereddict-one-item",
), ),
pytest.param( pytest.param(
OrderedDict({"one": 1, "two": 2}), OrderedDict({"one": 1, "two": 2}),
""" """
OrderedDict([('one', OrderedDict({
1), 'one': 1,
('two', 'two': 2,
2)]) })
""", """,
id="ordereddict-two-items", id="ordereddict-two-items",
), ),
@ -99,15 +108,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
[1], [1],
""" """
[1] [
1,
]
""", """,
id="list-one-item", id="list-one-item",
), ),
pytest.param( pytest.param(
[1, 2], [1, 2],
""" """
[1, [
2] 1,
2,
]
""", """,
id="list-two-items", id="list-two-items",
), ),
@ -119,15 +132,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
(1,), (1,),
""" """
(1,) (
1,
)
""", """,
id="tuple-one-item", id="tuple-one-item",
), ),
pytest.param( pytest.param(
(1, 2), (1, 2),
""" """
(1, (
2) 1,
2,
)
""", """,
id="tuple-two-items", id="tuple-two-items",
), ),
@ -139,15 +156,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
{1}, {1},
""" """
{1} {
1,
}
""", """,
id="set-one-item", id="set-one-item",
), ),
pytest.param( pytest.param(
{1, 2}, {1, 2},
""" """
{1, {
2} 1,
2,
}
""", """,
id="set-two-items", id="set-two-items",
), ),
@ -159,15 +180,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
MappingProxyType({"one": 1}), MappingProxyType({"one": 1}),
""" """
mappingproxy({'one': 1}) mappingproxy({
'one': 1,
})
""", """,
id="mappingproxy-one-item", id="mappingproxy-one-item",
), ),
pytest.param( pytest.param(
MappingProxyType({"one": 1, "two": 2}), MappingProxyType({"one": 1, "two": 2}),
""" """
mappingproxy({'one': 1, mappingproxy({
'two': 2}) 'one': 1,
'two': 2,
})
""", """,
id="mappingproxy-two-items", id="mappingproxy-two-items",
), ),
@ -179,15 +204,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
SimpleNamespace(one=1), SimpleNamespace(one=1),
""" """
namespace(one=1) namespace(
one=1,
)
""", """,
id="simplenamespace-one-item", id="simplenamespace-one-item",
), ),
pytest.param( pytest.param(
SimpleNamespace(one=1, two=2), SimpleNamespace(one=1, two=2),
""" """
namespace(one=1, namespace(
two=2) one=1,
two=2,
)
""", """,
id="simplenamespace-two-items", id="simplenamespace-two-items",
), ),
@ -197,17 +226,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
defaultdict(str, {"one": "1"}), defaultdict(str, {"one": "1"}),
""" """
defaultdict(<class 'str'>, defaultdict(<class 'str'>, {
{'one': '1'}) 'one': '1',
})
""", """,
id="defaultdict-one-item", id="defaultdict-one-item",
), ),
pytest.param( pytest.param(
defaultdict(str, {"one": "1", "two": "2"}), defaultdict(str, {"one": "1", "two": "2"}),
""" """
defaultdict(<class 'str'>, defaultdict(<class 'str'>, {
{'one': '1', 'one': '1',
'two': '2'}) 'two': '2',
})
""", """,
id="defaultdict-two-items", id="defaultdict-two-items",
), ),
@ -219,15 +250,19 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
Counter("1"), Counter("1"),
""" """
Counter({'1': 1}) Counter({
'1': 1,
})
""", """,
id="counter-one-item", id="counter-one-item",
), ),
pytest.param( pytest.param(
Counter("121"), Counter("121"),
""" """
Counter({'1': 2, Counter({
'2': 1}) '1': 2,
'2': 1,
})
""", """,
id="counter-two-items", id="counter-two-items",
), ),
@ -235,16 +270,26 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
ChainMap({"one": 1, "two": 2}), ChainMap({"one": 1, "two": 2}),
""" """
ChainMap({'one': 1, ChainMap(
'two': 2}) {
'one': 1,
'two': 2,
},
)
""", """,
id="chainmap-one-item", id="chainmap-one-item",
), ),
pytest.param( pytest.param(
ChainMap({"one": 1}, {"two": 2}), ChainMap({"one": 1}, {"two": 2}),
""" """
ChainMap({'one': 1}, ChainMap(
{'two': 2}) {
'one': 1,
},
{
'two': 2,
},
)
""", """,
id="chainmap-two-items", id="chainmap-two-items",
), ),
@ -256,24 +301,29 @@ class DataclassWithTwoItems:
pytest.param( pytest.param(
deque([1]), deque([1]),
""" """
deque([1]) deque([
1,
])
""", """,
id="deque-one-item", id="deque-one-item",
), ),
pytest.param( pytest.param(
deque([1, 2]), deque([1, 2]),
""" """
deque([1, deque([
2]) 1,
2,
])
""", """,
id="deque-two-items", id="deque-two-items",
), ),
pytest.param( pytest.param(
deque([1, 2], maxlen=3), deque([1, 2], maxlen=3),
""" """
deque([1, deque(maxlen=3, [
2], 1,
maxlen=3) 2,
])
""", """,
id="deque-maxlen", id="deque-maxlen",
), ),
@ -293,34 +343,60 @@ class DataclassWithTwoItems:
"tuple": (1, 2), "tuple": (1, 2),
}, },
""" """
{'chainmap': ChainMap({'one': 1}, {
{'two': 2}), 'chainmap': ChainMap(
'counter': Counter({'2': 2, {
'1': 1}), 'one': 1,
'dataclass': DataclassWithTwoItems(foo='foo', },
bar='bar'), {
'defaultdict': defaultdict(<class 'str'>, 'two': 2,
{'one': '1', },
'two': '2'}), ),
'deque': deque([1, 'counter': Counter({
2], '2': 2,
maxlen=3), '1': 1,
'dict': {'one': 1, }),
'two': 2}, 'dataclass': DataclassWithTwoItems(
'list': [1, foo='foo',
2], bar='bar',
'mappingproxy': mappingproxy({'one': 1, ),
'two': 2}), 'defaultdict': defaultdict(<class 'str'>, {
'ordereddict': OrderedDict([('one', 'one': '1',
1), 'two': '2',
('two', }),
2)]), 'deque': deque(maxlen=3, [
'set': {1, 1,
2}, 2,
'simplenamespace': namespace(one=1, ]),
two=2), 'dict': {
'tuple': (1, 'one': 1,
2)} '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", id="deep-example",
), ),

View File

@ -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",
[ [

View File

@ -1,13 +1,13 @@
anyio[curio,trio]==4.0.0 anyio[curio,trio]==4.1.0
django==4.2.7 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.7.0 pytest-django==4.7.0
pytest-flakes==4.0.5 pytest-flakes==4.0.5
pytest-html==4.1.1 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

View File

@ -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",

View File

@ -392,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",
] ]
@ -399,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
@ -423,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",
] ]
@ -432,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'",
@ -451,11 +455,14 @@ class TestAssert_reprcompare:
[0, 2], [0, 2],
""" """
Full diff: Full diff:
- [0, 2] [
0,
- 2,
? ^ ? ^
+ [0, 1] + 1,
? ^ ? ^
""", ]
""",
id="lists", id="lists",
), ),
pytest.param( pytest.param(
@ -463,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",
), ),
@ -475,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",
), ),
@ -501,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",
] ]
@ -539,26 +552,30 @@ 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',",
" 'b',", " 'b',",
" 'c',", " 'c',",
"- '" + long_d + "',", "- '" + long_d + "',",
" ]", " ]",
] ]
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',",
" 'b',", " 'b',",
" 'c',", " 'c',",
"+ '" + long_d + "',", "+ '" + long_d + "',",
" ]", " ]",
] ]
@ -571,13 +588,15 @@ 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',",
" 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',", " 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',",
" 'cccccccccccccccccccccccccccccc',", " 'cccccccccccccccccccccccccccccc',",
"- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", "- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
" ]", " ]",
] ]
@ -588,19 +607,21 @@ 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',",
"+ 'a',", "+ 'a',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
"+ 'aaaaaaaaaa',", "+ 'aaaaaaaaaa',",
" ]", " ]",
] ]
@ -611,31 +632,45 @@ 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}',",
"- 'new': 1,", " 'sub1': {",
" 'long_a': 'substring that gets wrapped substring that gets wrapped '",
" 'substring that gets wrapped ',",
" },",
" },",
" },",
"- 'new': 1,",
" }", " }",
] ]
@ -647,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
@ -656,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:
@ -767,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
@ -793,6 +877,7 @@ class TestAssert_reprcompare:
def test_unicode(self) -> None: def test_unicode(self) -> None:
assert callequal("£€", "£") == [ assert callequal("£€", "£") == [
"'£€' == '£'", "'£€' == '£'",
"",
"- £", "- £",
"+ £€", "+ £€",
] ]
@ -808,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("λ")
@ -831,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)}",
] ]
@ -838,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)}",
] ]
@ -1126,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",
] ]
@ -1313,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():
@ -1333,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,
] ]
) )
@ -1377,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",
"? +++", "? +++",
@ -1386,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'",
@ -1844,8 +1933,8 @@ 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 {light-red}- 2,{hl-reset}{endline}{reset}",
"{bold}{red}E {light-green}+ [0, 1]{hl-reset}{endline}{reset}", "{bold}{red}E {light-green}+ 1,{hl-reset}{endline}{reset}",
], ],
), ),
( (
@ -1857,8 +1946,8 @@ def test_reprcompare_verbose_long() -> None:
""", """,
[ [
"{bold}{red}E {light-gray} {hl-reset} {{{endline}{reset}", "{bold}{red}E {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}",
], ],
), ),
), ),
@ -1917,14 +2006,32 @@ def test_fine_grained_assertion_verbosity(pytester: Pytester):
f"{p.name} .FFF [100%]", f"{p.name} .FFF [100%]",
"E At index 2 diff: 'grapes' != 'orange'", "E At index 2 diff: 'grapes' != 'orange'",
"E Full diff:", "E Full diff:",
"E - ['banana', 'apple', 'orange', 'melon', 'kiwi']", "E [",
"E ? ^ ^^", "E 'banana',",
"E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']", "E 'apple',",
"E ? ^ ^ +", "E - 'orange',",
"E ? ^ ^^",
"E + 'grapes',",
"E ? ^ ^ +",
"E 'melon',",
"E 'kiwi',",
"E ]",
"E Full diff:", "E Full diff:",
"E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}", "E {",
"E ? - - - - - - - -", "E '0': 0,",
"E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}", "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}'", f"E AssertionError: assert 'hello world' in '{long_text}'",
] ]
) )

View File

@ -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
) )

View File

@ -1959,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)

View File

@ -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 ? ^ E 1: 'spam',
E + {1: 'spam', 3: 'eggs'} E - 2: 'eggs',
E ? ^ E ? ^
E + 3: 'eggs',
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",
), ),

View File

@ -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")

View File

@ -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(

View File

@ -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)