Merge branch 'main' of https://github.com/pytest-dev/pytest into downstream_testing_2

This commit is contained in:
sommersoft 2022-03-23 12:07:29 -05:00
commit 495c5abf31
84 changed files with 1492 additions and 783 deletions

56
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: deploy
on:
push:
tags:
# These tags are protected, see:
# https://github.com/pytest-dev/pytest/settings/tag_protection
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
# Set permissions at the job level.
permissions: {}
jobs:
deploy:
if: github.repository == 'pytest-dev/pytest'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.7"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade build tox
- name: Build package
run: |
python -m build
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_token }}
- name: Publish GitHub release notes
env:
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}
run: |
sudo apt-get install pandoc
tox -e publish-gh-release-notes

View File

@ -1,4 +1,4 @@
name: main name: test
on: on:
push: push:
@ -37,6 +37,7 @@ jobs:
"windows-py38", "windows-py38",
"windows-py39", "windows-py39",
"windows-py310", "windows-py310",
"windows-py311",
"ubuntu-py37", "ubuntu-py37",
"ubuntu-py37-pluggy", "ubuntu-py37-pluggy",
@ -44,6 +45,7 @@ jobs:
"ubuntu-py38", "ubuntu-py38",
"ubuntu-py39", "ubuntu-py39",
"ubuntu-py310", "ubuntu-py310",
"ubuntu-py311",
"ubuntu-pypy3", "ubuntu-pypy3",
"macos-py37", "macos-py37",
@ -75,9 +77,13 @@ jobs:
os: windows-latest os: windows-latest
tox_env: "py39-xdist" tox_env: "py39-xdist"
- name: "windows-py310" - name: "windows-py310"
python: "3.10.1" python: "3.10"
os: windows-latest os: windows-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "windows-py311"
python: "3.11-dev"
os: windows-latest
tox_env: "py311"
- name: "ubuntu-py37" - name: "ubuntu-py37"
python: "3.7" python: "3.7"
@ -101,9 +107,13 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
tox_env: "py39-xdist" tox_env: "py39-xdist"
- name: "ubuntu-py310" - name: "ubuntu-py310"
python: "3.10.1" python: "3.10"
os: ubuntu-latest os: ubuntu-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "ubuntu-py311"
python: "3.11-dev"
os: ubuntu-latest
tox_env: "py311"
- name: "ubuntu-pypy3" - name: "ubuntu-pypy3"
python: "pypy-3.7" python: "pypy-3.7"
os: ubuntu-latest os: ubuntu-latest
@ -177,46 +187,3 @@ jobs:
fail_ci_if_error: true fail_ci_if_error: true
files: ./coverage.xml files: ./coverage.xml
verbose: true verbose: true
deploy:
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
needs: [build]
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.7"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade build tox
- name: Build package
run: |
python -m build
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_token }}
- name: Publish GitHub release notes
env:
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}
run: |
sudo apt-get install pandoc
tox -e publish-gh-release-notes

View File

@ -12,6 +12,7 @@ permissions: {}
jobs: jobs:
createPullRequest: createPullRequest:
if: github.repository_owner == 'pytest-dev'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write

View File

@ -20,6 +20,14 @@ repos:
- id: debug-statements - id: debug-statements
exclude: _pytest/(debugging|hookspec).py exclude: _pytest/(debugging|hookspec).py
language_version: python3 language_version: python3
- repo: https://github.com/myint/autoflake
rev: v1.4
hooks:
- id: autoflake
name: autoflake
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
language: python
files: \.py$
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 4.0.1 rev: 4.0.1
hooks: hooks:
@ -29,12 +37,12 @@ repos:
- flake8-typing-imports==1.12.0 - flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0 - flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder_python_imports
rev: v2.7.1 rev: v3.0.1
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus] args: ['--application-directories=.:src', --py37-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.31.0 rev: v2.31.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
@ -48,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: v0.931 rev: v0.940
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)

View File

@ -185,8 +185,10 @@ Katerina Koukiou
Keri Volans Keri Volans
Kevin Cox Kevin Cox
Kevin J. Foley Kevin J. Foley
Kian Eliasi
Kian-Meng Ang Kian-Meng Ang
Kodi B. Arfer Kodi B. Arfer
Kojo Idrissa
Kostis Anagnostopoulos Kostis Anagnostopoulos
Kristoffer Nordström Kristoffer Nordström
Kyle Altendorf Kyle Altendorf
@ -288,6 +290,7 @@ Ruaridh Williamson
Russel Winder Russel Winder
Ryan Wooden Ryan Wooden
Saiprasad Kale Saiprasad Kale
Samuel Colvin
Samuel Dion-Girardeau Samuel Dion-Girardeau
Samuel Searles-Bryant Samuel Searles-Bryant
Samuele Pedroni Samuele Pedroni

View File

@ -20,8 +20,8 @@
:target: https://codecov.io/gh/pytest-dev/pytest :target: https://codecov.io/gh/pytest-dev/pytest
:alt: Code coverage Status :alt: Code coverage Status
.. image:: https://github.com/pytest-dev/pytest/workflows/main/badge.svg .. image:: https://github.com/pytest-dev/pytest/workflows/test/badge.svg
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Amain :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg .. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main :target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main

View File

@ -142,7 +142,7 @@ Both automatic and manual processes described above follow the same steps from t
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_. Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
#. Merge the PR. #. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
#. Cherry-pick the CHANGELOG / announce files to the ``main`` branch:: #. Cherry-pick the CHANGELOG / announce files to the ``main`` branch::

View File

@ -0,0 +1,2 @@
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`.

View File

@ -1,15 +0,0 @@
As per our policy, the following features have been deprecated in the 6.X series and are now
removed:
* ``pytest._fillfuncargs`` function.
* ``pytest_warning_captured`` hook - use ``pytest_warning_recorded`` instead.
* ``-k -foobar`` syntax - use ``-k 'not foobar'`` instead.
* ``-k foobar:`` syntax.
* ``pytest.collect`` module - import from ``pytest`` directly.
For more information consult
`Deprecations and Removals <https://docs.pytest.org/en/latest/deprecations.html>`__ in the docs.

View File

@ -1 +0,0 @@
Pytest will now avoid specialized assert formatting when it is detected that the default __eq__ is overridden

View File

@ -1 +0,0 @@
Dropped support for Python 3.6, which reached `end-of-life <https://devguide.python.org/#status-of-python-branches>`__ at 2021-12-23.

View File

@ -1,10 +0,0 @@
Symbolic link components are no longer resolved in conftest paths.
This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice.
For example, given
tests/real/conftest.py
tests/real/test_it.py
tests/link -> tests/real
running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``.
This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details).

View File

@ -1 +0,0 @@
When ``-vv`` is given on command line, show skipping and xfail reasons in full instead of truncating them to fit the terminal width.

View File

@ -0,0 +1 @@
An unnecessary ``numpy`` import inside :func:`pytest.approx` was removed.

View File

@ -0,0 +1 @@
Display assertion message without escaped newline characters with ``-vv``.

View File

@ -6,6 +6,9 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-7.1.1
release-7.1.0
release-7.0.1
release-7.0.0 release-7.0.0
release-7.0.0rc1 release-7.0.0rc1
release-6.2.5 release-6.2.5

View File

@ -0,0 +1,20 @@
pytest-7.0.1
=======================================
pytest 7.0.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Anthony Sottile
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,48 @@
pytest-7.1.0
=======================================
The pytest team is proud to announce the 7.1.0 release!
This release contains new features, improvements, and bug fixes,
the full list of changes is available in the changelog:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Akuli
* Andrew Svetlov
* Anthony Sottile
* Brett Holman
* Bruno Oliveira
* Chris NeJame
* Dan Alvizu
* Elijah DeLee
* Emmanuel Arias
* Fabian Egli
* Florian Bruhin
* Gabor Szabo
* Hasan Ramezani
* Hugo van Kemenade
* Kian Meng, Ang
* Kojo Idrissa
* Masaru Tsuchiyama
* Olga Matoula
* P. L. Lim
* Ran Benita
* Tobias Deiminger
* Yuval Shimon
* eduardo naufel schettino
* Éric
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,18 @@
pytest-7.1.1
=======================================
pytest 7.1.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Ran Benita
Happy testing,
The pytest Development Team

View File

@ -65,7 +65,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Fixture that returns a :py:class:`dict` that will be injected into the Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests. namespace of doctests.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1365 pytestconfig [session scope] -- .../_pytest/fixtures.py:1334
Session-scoped fixture that returns the session's :class:`pytest.Config` Session-scoped fixture that returns the session's :class:`pytest.Config`
object. object.
@ -134,7 +134,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:483 caplog -- .../_pytest/logging.py:487
Access and control log capturing. Access and control log capturing.
Captured logs are available through the following properties/methods:: Captured logs are available through the following properties/methods::

View File

@ -28,6 +28,131 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 7.1.1 (2022-03-17)
=========================
Bug Fixes
---------
- `#9767 <https://github.com/pytest-dev/pytest/issues/9767>`_: Fixed a regression in pytest 7.1.0 where some conftest.py files outside of the source tree (e.g. in the `site-packages` directory) were not picked up.
pytest 7.1.0 (2022-03-13)
=========================
Breaking Changes
----------------
- `#8838 <https://github.com/pytest-dev/pytest/issues/8838>`_: As per our policy, the following features have been deprecated in the 6.X series and are now
removed:
* ``pytest._fillfuncargs`` function.
* ``pytest_warning_captured`` hook - use ``pytest_warning_recorded`` instead.
* ``-k -foobar`` syntax - use ``-k 'not foobar'`` instead.
* ``-k foobar:`` syntax.
* ``pytest.collect`` module - import from ``pytest`` directly.
For more information consult
`Deprecations and Removals <https://docs.pytest.org/en/latest/deprecations.html>`__ in the docs.
- `#9437 <https://github.com/pytest-dev/pytest/issues/9437>`_: Dropped support for Python 3.6, which reached `end-of-life <https://devguide.python.org/#status-of-python-branches>`__ at 2021-12-23.
Improvements
------------
- `#5192 <https://github.com/pytest-dev/pytest/issues/5192>`_: Fixed test output for some data types where ``-v`` would show less information.
Also, when showing diffs for sequences, ``-q`` would produce full diffs instead of the expected diff.
- `#9362 <https://github.com/pytest-dev/pytest/issues/9362>`_: pytest now avoids specialized assert formatting when it is detected that the default ``__eq__`` is overridden in ``attrs`` or ``dataclasses``.
- `#9536 <https://github.com/pytest-dev/pytest/issues/9536>`_: When ``-vv`` is given on command line, show skipping and xfail reasons in full instead of truncating them to fit the terminal width.
- `#9644 <https://github.com/pytest-dev/pytest/issues/9644>`_: More information about the location of resources that led Python to raise :class:`ResourceWarning` can now
be obtained by enabling :mod:`tracemalloc`.
See :ref:`resource-warnings` for more information.
- `#9678 <https://github.com/pytest-dev/pytest/issues/9678>`_: More types are now accepted in the ``ids`` argument to ``@pytest.mark.parametrize``.
Previously only `str`, `float`, `int` and `bool` were accepted;
now `bytes`, `complex`, `re.Pattern`, `Enum` and anything with a `__name__` are also accepted.
- `#9692 <https://github.com/pytest-dev/pytest/issues/9692>`_: :func:`pytest.approx` now raises a :class:`TypeError` when given an unordered sequence (such as :class:`set`).
Note that this implies that custom classes which only implement ``__iter__`` and ``__len__`` are no longer supported as they don't guarantee order.
Bug Fixes
---------
- `#8242 <https://github.com/pytest-dev/pytest/issues/8242>`_: The deprecation of raising :class:`unittest.SkipTest` to skip collection of
tests during the pytest collection phase is reverted - this is now a supported
feature again.
- `#9493 <https://github.com/pytest-dev/pytest/issues/9493>`_: Symbolic link components are no longer resolved in conftest paths.
This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice.
For example, given
tests/real/conftest.py
tests/real/test_it.py
tests/link -> tests/real
running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``.
This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details).
- `#9626 <https://github.com/pytest-dev/pytest/issues/9626>`_: Fixed count of selected tests on terminal collection summary when there were errors or skipped modules.
If there were errors or skipped modules on collection, pytest would mistakenly subtract those from the selected count.
- `#9645 <https://github.com/pytest-dev/pytest/issues/9645>`_: Fixed regression where ``--import-mode=importlib`` used together with :envvar:`PYTHONPATH` or :confval:`pythonpath` would cause import errors in test suites.
- `#9708 <https://github.com/pytest-dev/pytest/issues/9708>`_: :fixture:`pytester` now requests a :fixture:`monkeypatch` fixture instead of creating one internally. This solves some issues with tests that involve pytest environment variables.
- `#9730 <https://github.com/pytest-dev/pytest/issues/9730>`_: Malformed ``pyproject.toml`` files now produce a clearer error message.
pytest 7.0.1 (2022-02-11)
=========================
Bug Fixes
---------
- `#9608 <https://github.com/pytest-dev/pytest/issues/9608>`_: Fix invalid importing of ``importlib.readers`` in Python 3.9.
- `#9610 <https://github.com/pytest-dev/pytest/issues/9610>`_: Restore `UnitTestFunction.obj` to return unbound rather than bound method.
Fixes a crash during a failed teardown in unittest TestCases with non-default `__init__`.
Regressed in pytest 7.0.0.
- `#9636 <https://github.com/pytest-dev/pytest/issues/9636>`_: The ``pythonpath`` plugin was renamed to ``python_path``. This avoids a conflict with the ``pytest-pythonpath`` plugin.
- `#9642 <https://github.com/pytest-dev/pytest/issues/9642>`_: Fix running tests by id with ``::`` in the parametrize portion.
- `#9643 <https://github.com/pytest-dev/pytest/issues/9643>`_: Delay issuing a :class:`~pytest.PytestWarning` about diamond inheritance involving :class:`~pytest.Item` and
:class:`~pytest.Collector` so it can be filtered using :ref:`standard warning filters <warnings>`.
pytest 7.0.0 (2022-02-03) pytest 7.0.0 (2022-02-03)
========================= =========================
@ -187,6 +312,8 @@ Deprecations
:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` / :class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` /
:func:`unittest.skip` in unittest test cases is fully supported. :func:`unittest.skip` in unittest test cases is fully supported.
.. note:: This deprecation has been reverted in pytest 7.1.0.
- `#8315 <https://github.com/pytest-dev/pytest/issues/8315>`_: Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now - `#8315 <https://github.com/pytest-dev/pytest/issues/8315>`_: Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
scheduled for removal in pytest 8 (deprecated since pytest 2.4.0): scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):

View File

@ -382,7 +382,6 @@ texinfo_documents = [
] ]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = { intersphinx_mapping = {
"pluggy": ("https://pluggy.readthedocs.io/en/stable", None), "pluggy": ("https://pluggy.readthedocs.io/en/stable", None),
"python": ("https://docs.python.org/3", None), "python": ("https://docs.python.org/3", None),
@ -390,10 +389,6 @@ intersphinx_mapping = {
"pip": ("https://pip.pypa.io/en/stable", None), "pip": ("https://pip.pypa.io/en/stable", None),
"tox": ("https://tox.wiki/en/stable", None), "tox": ("https://tox.wiki/en/stable", None),
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None), "virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
"django": (
"http://docs.djangoproject.com/en/stable",
"http://docs.djangoproject.com/en/stable/_objects",
),
"setuptools": ("https://setuptools.pypa.io/en/stable", None), "setuptools": ("https://setuptools.pypa.io/en/stable", None),
} }

View File

@ -16,7 +16,7 @@ Deprecated Features
------------------- -------------------
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`. :class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
.. _instance-collector-deprecation: .. _instance-collector-deprecation:
@ -241,19 +241,6 @@ scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead. - ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
Raising ``unittest.SkipTest`` during collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Raising :class:`unittest.SkipTest` to skip collection of tests during the
pytest collection phase is deprecated. Use :func:`pytest.skip` instead.
Note: This deprecation only relates to using `unittest.SkipTest` during test
collection. You are probably not doing that. Ordinary usage of
:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` /
:func:`unittest.skip` in unittest test cases is fully supported.
Using ``pytest.warns(None)`` Using ``pytest.warns(None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -314,7 +301,7 @@ The ``pytest_warning_captured`` hook
This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``. This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``.
Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter Use the ``pytest_warning_recorded`` hook instead, which replaces the ``item`` parameter
by a ``nodeid`` parameter. by a ``nodeid`` parameter.

View File

@ -155,7 +155,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert [0, 1, 2] == [0, 1, 3] > assert [0, 1, 2] == [0, 1, 3]
E assert [0, 1, 2] == [0, 1, 3] E assert [0, 1, 2] == [0, 1, 3]
E At index 2 diff: 2 != 3 E At index 2 diff: 2 != 3
E Use -v to get the full diff E Use -v to get more diff
failure_demo.py:63: AssertionError failure_demo.py:63: AssertionError
______________ TestSpecialisedExplanations.test_eq_list_long _______________ ______________ TestSpecialisedExplanations.test_eq_list_long _______________
@ -168,7 +168,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert a == b > assert a == b
E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...] E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...]
E At index 100 diff: 1 != 2 E At index 100 diff: 1 != 2
E Use -v to get the full diff E Use -v to get more diff
failure_demo.py:68: AssertionError failure_demo.py:68: AssertionError
_________________ TestSpecialisedExplanations.test_eq_dict _________________ _________________ TestSpecialisedExplanations.test_eq_dict _________________
@ -215,7 +215,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert [1, 2] == [1, 2, 3] > assert [1, 2] == [1, 2, 3]
E assert [1, 2] == [1, 2, 3] E assert [1, 2] == [1, 2, 3]
E Right contains one more item: 3 E Right contains one more item: 3
E Use -v to get the full diff E Use -v to get more diff
failure_demo.py:77: AssertionError failure_demo.py:77: AssertionError
_________________ TestSpecialisedExplanations.test_in_list _________________ _________________ TestSpecialisedExplanations.test_in_list _________________

View File

@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash .. code-block:: bash
$ pytest --version $ pytest --version
pytest 7.0.0 pytest 7.1.1
.. _`simpletest`: .. _`simpletest`:

View File

@ -201,7 +201,7 @@ if you run this module:
E '1' E '1'
E Extra items in the right set: E Extra items in the right set:
E '5' E '5'
E Use -v to get the full diff E Use -v to get more diff
test_assert2.py:4: AssertionError test_assert2.py:4: AssertionError
========================= short test summary info ========================== ========================= short test summary info ==========================

View File

@ -5,7 +5,7 @@ How to set up bash completion
============================= =============================
When using bash as your shell, ``pytest`` can use argcomplete When using bash as your shell, ``pytest`` can use argcomplete
(https://argcomplete.readthedocs.io/) for auto-completion. (https://kislyuk.github.io/argcomplete/) for auto-completion.
For this ``argcomplete`` needs to be installed **and** enabled. For this ``argcomplete`` needs to be installed **and** enabled.
Install argcomplete using: Install argcomplete using:

View File

@ -358,7 +358,7 @@ Additional use cases of warnings in tests
Here are some use cases involving warnings that often come up in tests, and suggestions on how to deal with them: Here are some use cases involving warnings that often come up in tests, and suggestions on how to deal with them:
- To ensure that **any** warning is emitted, use: - To ensure that **at least one** warning is emitted, use:
.. code-block:: python .. code-block:: python
@ -441,3 +441,18 @@ Please read our :ref:`backwards-compatibility` to learn how we proceed about dep
features. features.
The full list of warnings is listed in :ref:`the reference documentation <warnings ref>`. The full list of warnings is listed in :ref:`the reference documentation <warnings ref>`.
.. _`resource-warnings`:
Resource Warnings
-----------------
Additional information of the source of a :class:`ResourceWarning` can be obtained when captured by pytest if
:mod:`tracemalloc` module is enabled.
One convenient way to enable :mod:`tracemalloc` when running tests is to set the :envvar:`PYTHONTRACEMALLOC` to a large
enough number of frames (say ``20``, but that number is application dependent).
For more information, consult the `Python Development Mode <https://docs.python.org/3/library/devmode.html>`__
section in the Python documentation.

View File

@ -84,7 +84,7 @@ Executing pytest normally gives us this output (we are skipping the header to fo
> assert fruits1 == fruits2 > assert fruits1 == fruits2
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi'] E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
E At index 2 diff: 'grapes' != 'orange' E At index 2 diff: 'grapes' != 'orange'
E Use -v to get the full diff E Use -v to get more diff
test_verbosity_example.py:8: AssertionError test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail _____________________________ ____________________________ test_numbers_fail _____________________________
@ -99,7 +99,7 @@ Executing pytest normally gives us this output (we are skipping the header to fo
E {'1': 1, '2': 2, '3': 3, '4': 4} E {'1': 1, '2': 2, '3': 3, '4': 4}
E Right contains 4 more items: E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40} E {'10': 10, '20': 20, '30': 30, '40': 40}
E Use -v to get the full diff E Use -v to get more diff
test_verbosity_example.py:14: AssertionError test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________ ___________________________ test_long_text_fail ____________________________

View File

@ -21,7 +21,7 @@ there is no need to activate it.
Here is a little annotated list for some popular plugins: Here is a little annotated list for some popular plugins:
* :pypi:`pytest-django`: write tests * :pypi:`pytest-django`: write tests
for :std:doc:`django <django:index>` apps, using pytest integration. for `django <https://docs.djangoproject.com/>`_ apps, using pytest integration.
* :pypi:`pytest-twisted`: write tests * :pypi:`pytest-twisted`: write tests
for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and

View File

@ -1,11 +1,16 @@
:orphan: :orphan:
..
.. sidebar:: Next Open Trainings .. sidebar:: Next Open Trainings
- `Professional Testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, February 1st to 3rd, 2022, Leipzig (Germany) and remote. - `PyConDE <https://2022.pycon.de/program/W93DBJ/>`__, April 11th 2022 (3h), Berlin, Germany
- `PyConIT <https://pycon.it/en/talk/pytest-simple-rapid-and-fun-testing-with-python>`__, June 3rd 2022 (4h), Florence, Italy
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
Also see `previous talks and blogposts <talks.html>`_. Also see :doc:`previous talks and blogposts <talks>`.
..
- `Europython <https://ep2022.europython.eu/>`__, July 11th to 17th (3h), Dublin, Ireland
- `CH Open Workshoptage <https://workshoptage.ch/>`__ (German), September 6th to 8th (1 day), Bern, Switzerland
.. _features: .. _features:

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,14 @@ Books
- `Python Testing with pytest, by Brian Okken (2017) - `Python Testing with pytest, by Brian Okken (2017)
<https://pragprog.com/book/bopytest/python-testing-with-pytest>`_. <https://pragprog.com/book/bopytest/python-testing-with-pytest>`_.
- `Python Testing with pytest, Second Edition, by Brian Okken (2022)
<https://pragprog.com/titles/bopytest2/python-testing-with-pytest-second-edition>`_.
Talks and blog postings Talks and blog postings
--------------------------------------------- ---------------------------------------------
- `pytest: Simple, rapid and fun testing with Python, <https://youtu.be/cSJ-X3TbQ1c?t=15752>`_ (@ 4:22:32), Florian Bruhin, WeAreDevelopers World Congress 2021
- Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020 - Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020
- Webinar: `Simplify Your Tests with Fixtures <https://blog.jetbrains.com/pycharm/2020/08/webinar-recording-simplify-your-tests-with-fixtures-with-oliver-bestwalter/>`_, Oliver Bestwalter, via JetBrains, 2020 - Webinar: `Simplify Your Tests with Fixtures <https://blog.jetbrains.com/pycharm/2020/08/webinar-recording-simplify-your-tests-with-fixtures-with-oliver-bestwalter/>`_, Oliver Bestwalter, via JetBrains, 2020

View File

@ -1,4 +1,5 @@
import sys import sys
from distutils.core import setup from distutils.core import setup
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -672,10 +672,11 @@ class ExceptionInfo(Generic[E]):
If it matches `True` is returned, otherwise an `AssertionError` is raised. If it matches `True` is returned, otherwise an `AssertionError` is raised.
""" """
__tracebackhide__ = True __tracebackhide__ = True
msg = "Regex pattern {!r} does not match {!r}." value = str(self.value)
if regexp == str(self.value): msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
msg += " Did you mean to `re.escape()` the regex?" if regexp == value:
assert re.search(regexp, str(self.value)), msg.format(regexp, str(self.value)) msg += "\n Did you mean to `re.escape()` the regex?"
assert re.search(regexp, value), msg
# Return True to allow for "assert excinfo.match()". # Return True to allow for "assert excinfo.match()".
return True return True

View File

@ -107,6 +107,23 @@ def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str
return SafeRepr(maxsize).repr(obj) return SafeRepr(maxsize).repr(obj)
def saferepr_unlimited(obj: object) -> str:
"""Return an unlimited-size safe repr-string for the given object.
As with saferepr, failing __repr__ functions of user instances
will be represented with a short exception info.
This function is a wrapper around simple repr.
Note: a cleaner solution would be to alter ``saferepr``this way
when maxsize=None, but that might affect some other code.
"""
try:
return repr(obj)
except Exception as exc:
return _format_repr_exception(exc, obj)
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
"""PrettyPrinter that always dispatches (regardless of width).""" """PrettyPrinter that always dispatches (regardless of width)."""

View File

@ -273,13 +273,15 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
with open(pathname, "rb") as f: with open(pathname, "rb") as f:
return f.read() return f.read()
if sys.version_info >= (3, 9): if sys.version_info >= (3, 10):
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
from types import SimpleNamespace if sys.version_info < (3, 11):
from importlib.readers import FileReader from importlib.readers import FileReader
else:
from importlib.resources.readers import FileReader
return FileReader(SimpleNamespace(path=self._rewritten_names[name])) return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
def _write_pyc_fp( def _write_pyc_fp(

View File

@ -14,8 +14,8 @@ from typing import Sequence
import _pytest._code import _pytest._code
from _pytest import outcomes from _pytest import outcomes
from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
from _pytest.config import Config from _pytest.config import Config
# The _reprcompare attribute on the util module is used by the new assertion # The _reprcompare attribute on the util module is used by the new assertion
@ -160,8 +160,8 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[
"""Return specialised explanations for some operators/operands.""" """Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose") verbose = config.getoption("verbose")
if verbose > 1: if verbose > 1:
left_repr = safeformat(left) left_repr = saferepr_unlimited(left)
right_repr = safeformat(right) right_repr = saferepr_unlimited(right)
else: else:
# XXX: "15 chars indentation" is wrong # XXX: "15 chars indentation" is wrong
# ("E AssertionError: assert "); should use term width. # ("E AssertionError: assert "); should use term width.
@ -223,8 +223,6 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
explanation = _compare_eq_set(left, right, verbose) explanation = _compare_eq_set(left, right, verbose)
elif isdict(left) and isdict(right): elif isdict(left) and isdict(right):
explanation = _compare_eq_dict(left, right, verbose) explanation = _compare_eq_dict(left, right, verbose)
elif verbose > 0:
explanation = _compare_eq_verbose(left, right)
if isiterable(left) and isiterable(right): if isiterable(left) and isiterable(right):
expl = _compare_eq_iterable(left, right, verbose) expl = _compare_eq_iterable(left, right, verbose)
@ -281,18 +279,6 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
return explanation return explanation
def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
keepends = True
left_lines = repr(left).splitlines(keepends)
right_lines = repr(right).splitlines(keepends)
explanation: List[str] = []
explanation += ["+" + line for line in left_lines]
explanation += ["-" + line for line in right_lines]
return explanation
def _surrounding_parens_on_own_lines(lines: List[str]) -> None: def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
"""Move opening/closing parenthesis/bracket to own lines.""" """Move opening/closing parenthesis/bracket to own lines."""
opening = lines[0][:1] opening = lines[0][:1]
@ -308,8 +294,8 @@ def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
def _compare_eq_iterable( def _compare_eq_iterable(
left: Iterable[Any], right: Iterable[Any], verbose: int = 0 left: Iterable[Any], right: Iterable[Any], verbose: int = 0
) -> List[str]: ) -> List[str]:
if not verbose and not running_on_ci(): if verbose <= 0 and not running_on_ci():
return ["Use -v to get the full diff"] return ["Use -v to get more diff"]
# dynamic import to speedup pytest # dynamic import to speedup pytest
import difflib import difflib

View File

@ -254,7 +254,7 @@ default_plugins = essential_plugins + (
"warnings", "warnings",
"logging", "logging",
"reports", "reports",
"pythonpath", "python_path",
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
"faulthandler", "faulthandler",
) )
@ -309,7 +309,9 @@ def _prepareconfig(
elif isinstance(args, os.PathLike): elif isinstance(args, os.PathLike):
args = [os.fspath(args)] args = [os.fspath(args)]
elif not isinstance(args, list): elif not isinstance(args, list):
msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})" msg = ( # type:ignore[unreachable]
"`args` parameter expected to be a list of strings, got: {!r} (type: {})"
)
raise TypeError(msg.format(args, type(args))) raise TypeError(msg.format(args, type(args)))
config = get_config(args, plugins) config = get_config(args, plugins)
@ -538,11 +540,7 @@ class PytestPluginManager(PluginManager):
""" """
if self._confcutdir is None: if self._confcutdir is None:
return True return True
try: return path not in self._confcutdir.parents
path.relative_to(self._confcutdir)
except ValueError:
return False
return True
def _try_load_conftest( def _try_load_conftest(
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path

View File

@ -70,7 +70,7 @@ def load_config_dict_from_file(
try: try:
config = tomli.loads(toml_text) config = tomli.loads(toml_text)
except tomli.TOMLDecodeError as exc: except tomli.TOMLDecodeError as exc:
raise UsageError(str(exc)) from exc raise UsageError(f"{filepath}: {exc}") from exc
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
if result is not None: if result is not None:

View File

@ -47,11 +47,6 @@ STRICT_OPTION = PytestRemovedIn8Warning(
# This deprecation is never really meant to be removed. # This deprecation is never really meant to be removed.
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
UNITTEST_SKIP_DURING_COLLECTION = PytestRemovedIn8Warning(
"Raising unittest.SkipTest to skip tests during collection is deprecated. "
"Use pytest.skip() instead."
)
ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning( ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
'pytest now uses argparse. "%default" should be changed to "%(default)s"', 'pytest now uses argparse. "%default" should be changed to "%(default)s"',
) )

View File

@ -597,8 +597,17 @@ class FixtureRequest:
funcitem = self._pyfuncitem funcitem = self._pyfuncitem
scope = fixturedef._scope scope = fixturedef._scope
try: try:
param = funcitem.callspec.getparam(argname) callspec = funcitem.callspec
except (AttributeError, ValueError): except AttributeError:
callspec = None
if callspec is not None and argname in callspec.params:
param = callspec.params[argname]
param_index = callspec.indices[argname]
# If a parametrize invocation set a scope it will override
# the static scope defined with the fixture function.
with suppress(KeyError):
scope = callspec._arg2scope[argname]
else:
param = NOTSET param = NOTSET
param_index = 0 param_index = 0
has_params = fixturedef.params is not None has_params = fixturedef.params is not None
@ -638,12 +647,6 @@ class FixtureRequest:
) )
) )
fail(msg, pytrace=False) fail(msg, pytrace=False)
else:
param_index = funcitem.callspec.indices[argname]
# If a parametrize invocation set a scope it will override
# the static scope defined with the fixture function.
with suppress(KeyError):
scope = funcitem.callspec._arg2scope[argname]
subrequest = SubRequest( subrequest = SubRequest(
self, scope, param, param_index, fixturedef, _ispytest=True self, scope, param, param_index, fixturedef, _ispytest=True
@ -927,7 +930,7 @@ def _eval_scope_callable(
@final @final
class FixtureDef(Generic[FixtureValue]): class FixtureDef(Generic[FixtureValue]):
"""A container for a factory definition.""" """A container for a fixture definition."""
def __init__( def __init__(
self, self,
@ -939,33 +942,56 @@ class FixtureDef(Generic[FixtureValue]):
params: Optional[Sequence[object]], params: Optional[Sequence[object]],
unittest: bool = False, unittest: bool = False,
ids: Optional[ ids: Optional[
Union[ Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
Tuple[Union[None, str, float, int, bool], ...],
Callable[[Any], Optional[object]],
]
] = None, ] = None,
) -> None: ) -> None:
self._fixturemanager = fixturemanager self._fixturemanager = fixturemanager
# The "base" node ID for the fixture.
#
# This is a node ID prefix. A fixture is only available to a node (e.g.
# a `Function` item) if the fixture's baseid is a parent of the node's
# nodeid (see the `iterparentnodeids` function for what constitutes a
# "parent" and a "prefix" in this context).
#
# For a fixture found in a Collector's object (e.g. a `Module`s module,
# a `Class`'s class), the baseid is the Collector's nodeid.
#
# For a fixture found in a conftest plugin, the baseid is the conftest's
# directory path relative to the rootdir.
#
# For other plugins, the baseid is the empty string (always matches).
self.baseid = baseid or "" self.baseid = baseid or ""
# Whether the fixture was found from a node or a conftest in the
# collection tree. Will be false for fixtures defined in non-conftest
# plugins.
self.has_location = baseid is not None self.has_location = baseid is not None
# The fixture factory function.
self.func = func self.func = func
# The name by which the fixture may be requested.
self.argname = argname self.argname = argname
if scope is None: if scope is None:
scope = Scope.Function scope = Scope.Function
elif callable(scope): elif callable(scope):
scope = _eval_scope_callable(scope, argname, fixturemanager.config) scope = _eval_scope_callable(scope, argname, fixturemanager.config)
if isinstance(scope, str): if isinstance(scope, str):
scope = Scope.from_user( scope = Scope.from_user(
scope, descr=f"Fixture '{func.__name__}'", where=baseid scope, descr=f"Fixture '{func.__name__}'", where=baseid
) )
self._scope = scope self._scope = scope
# If the fixture is directly parametrized, the parameter values.
self.params: Optional[Sequence[object]] = params self.params: Optional[Sequence[object]] = params
self.argnames: Tuple[str, ...] = getfuncargnames( # If the fixture is directly parametrized, a tuple of explicit IDs to
func, name=argname, is_method=unittest # assign to the parameter values, or a callable to generate an ID given
) # a parameter value.
self.unittest = unittest
self.ids = ids self.ids = ids
# The names requested by the fixtures.
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
# Whether the fixture was collected from a unittest TestCase class.
# Note that it really only makes sense to define autouse fixtures in
# unittest TestCases.
self.unittest = unittest
# If the fixture was executed, the current value of the fixture.
# Can change if the fixture is executed with different parameters.
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
self._finalizers: List[Callable[[], object]] = [] self._finalizers: List[Callable[[], object]] = []
@ -1093,18 +1119,8 @@ def pytest_fixture_setup(
def _ensure_immutable_ids( def _ensure_immutable_ids(
ids: Optional[ ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
Union[ ) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
],
) -> Optional[
Union[
Tuple[Union[None, str, float, int, bool], ...],
Callable[[Any], Optional[object]],
]
]:
if ids is None: if ids is None:
return None return None
if callable(ids): if callable(ids):
@ -1148,9 +1164,8 @@ class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter) params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
autouse: bool = False autouse: bool = False
ids: Union[ ids: Optional[
Tuple[Union[None, str, float, int, bool], ...], Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
Callable[[Any], Optional[object]],
] = attr.ib( ] = attr.ib(
default=None, default=None,
converter=_ensure_immutable_ids, converter=_ensure_immutable_ids,
@ -1191,10 +1206,7 @@ def fixture(
params: Optional[Iterable[object]] = ..., params: Optional[Iterable[object]] = ...,
autouse: bool = ..., autouse: bool = ...,
ids: Optional[ ids: Optional[
Union[ Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = ..., ] = ...,
name: Optional[str] = ..., name: Optional[str] = ...,
) -> FixtureFunction: ) -> FixtureFunction:
@ -1209,10 +1221,7 @@ def fixture(
params: Optional[Iterable[object]] = ..., params: Optional[Iterable[object]] = ...,
autouse: bool = ..., autouse: bool = ...,
ids: Optional[ ids: Optional[
Union[ Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = ..., ] = ...,
name: Optional[str] = None, name: Optional[str] = None,
) -> FixtureFunctionMarker: ) -> FixtureFunctionMarker:
@ -1226,10 +1235,7 @@ def fixture(
params: Optional[Iterable[object]] = None, params: Optional[Iterable[object]] = None,
autouse: bool = False, autouse: bool = False,
ids: Optional[ ids: Optional[
Union[ Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = None, ] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, FixtureFunction]: ) -> Union[FixtureFunctionMarker, FixtureFunction]:
@ -1271,7 +1277,7 @@ def fixture(
the fixture. the fixture.
:param ids: :param ids:
List of string ids each corresponding to the params so that they are Sequence of ids each corresponding to the params so that they are
part of the test id. If no ids are provided they will be generated part of the test id. If no ids are provided they will be generated
automatically from the params. automatically from the params.

View File

@ -870,7 +870,10 @@ def resolve_collection_argument(
If the path doesn't exist, raise UsageError. If the path doesn't exist, raise UsageError.
If the path is a directory and selection parts are present, raise UsageError. If the path is a directory and selection parts are present, raise UsageError.
""" """
strpath, *parts = str(arg).split("::") base, squacket, rest = str(arg).partition("[")
strpath, *parts = base.split("::")
if parts:
parts[-1] = f"{parts[-1]}{squacket}{rest}"
if as_pypath: if as_pypath:
strpath = search_pypath(strpath) strpath = search_pypath(strpath)
fspath = invocation_path / strpath fspath = invocation_path / strpath

View File

@ -397,7 +397,7 @@ if TYPE_CHECKING:
from _pytest.scope import _ScopeName from _pytest.scope import _ScopeName
class _SkipMarkDecorator(MarkDecorator): class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc] @overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable: def __call__(self, arg: Markable) -> Markable:
... ...
@ -415,7 +415,7 @@ if TYPE_CHECKING:
... ...
class _XfailMarkDecorator(MarkDecorator): class _XfailMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc] @overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable: def __call__(self, arg: Markable) -> Markable:
... ...

View File

@ -656,20 +656,6 @@ class Item(Node):
nextitem = None nextitem = None
def __init_subclass__(cls) -> None:
problems = ", ".join(
base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
)
if problems:
warnings.warn(
f"{cls.__name__} is an Item subclass and should not be a collector, "
f"however its bases {problems} are collectors.\n"
"Please split the Collectors and the Item into separate node types.\n"
"Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
"example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
PytestWarning,
)
def __init__( def __init__(
self, self,
name, name,
@ -697,6 +683,37 @@ class Item(Node):
#: for this test. #: for this test.
self.user_properties: List[Tuple[str, object]] = [] self.user_properties: List[Tuple[str, object]] = []
self._check_item_and_collector_diamond_inheritance()
def _check_item_and_collector_diamond_inheritance(self) -> None:
"""
Check if the current type inherits from both File and Collector
at the same time, emitting a warning accordingly (#8447).
"""
cls = type(self)
# We inject an attribute in the type to avoid issuing this warning
# for the same class more than once, which is not helpful.
# It is a hack, but was deemed acceptable in order to avoid
# flooding the user in the common case.
attr_name = "_pytest_diamond_inheritance_warning_shown"
if getattr(cls, attr_name, False):
return
setattr(cls, attr_name, True)
problems = ", ".join(
base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
)
if problems:
warnings.warn(
f"{cls.__name__} is an Item subclass and should not be a collector, "
f"however its bases {problems} are collectors.\n"
"Please split the Collectors and the Item into separate node types.\n"
"Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
"example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
PytestWarning,
)
def runtest(self) -> None: def runtest(self) -> None:
"""Run the test case for this item. """Run the test case for this item.

View File

@ -603,6 +603,15 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
module_parts = module_name.split(".") module_parts = module_name.split(".")
while module_name: while module_name:
if module_name not in modules: if module_name not in modules:
try:
# If sys.meta_path is empty, calling import_module will issue
# a warning and raise ModuleNotFoundError. To avoid the
# warning, we check sys.meta_path explicitly and raise the error
# ourselves to fall back to creating a dummy module.
if not sys.meta_path:
raise ModuleNotFoundError
importlib.import_module(module_name)
except ModuleNotFoundError:
module = ModuleType( module = ModuleType(
module_name, module_name,
doc="Empty module created by pytest's importmode=importlib.", doc="Empty module created by pytest's importmode=importlib.",

View File

@ -477,7 +477,9 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
@fixture @fixture
def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester": def pytester(
request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
) -> "Pytester":
""" """
Facilities to write tests/configuration files, execute pytest in isolation, and match Facilities to write tests/configuration files, execute pytest in isolation, and match
against expected output, perfect for black-box testing of pytest plugins. against expected output, perfect for black-box testing of pytest plugins.
@ -488,7 +490,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt
It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
fixture but provides methods which aid in testing pytest itself. fixture but provides methods which aid in testing pytest itself.
""" """
return Pytester(request, tmp_path_factory, _ispytest=True) return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True)
@fixture @fixture
@ -683,6 +685,7 @@ class Pytester:
self, self,
request: FixtureRequest, request: FixtureRequest,
tmp_path_factory: TempPathFactory, tmp_path_factory: TempPathFactory,
monkeypatch: MonkeyPatch,
*, *,
_ispytest: bool = False, _ispytest: bool = False,
) -> None: ) -> None:
@ -706,7 +709,7 @@ class Pytester:
self._method = self._request.config.getoption("--runpytest") self._method = self._request.config.getoption("--runpytest")
self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
self._monkeypatch = mp = MonkeyPatch() self._monkeypatch = mp = monkeypatch
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot)) mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
# Ensure no unexpected caching via tox. # Ensure no unexpected caching via tox.
mp.delenv("TOX_ENV_DIR", raising=False) mp.delenv("TOX_ENV_DIR", raising=False)
@ -738,7 +741,6 @@ class Pytester:
self._sys_modules_snapshot.restore() self._sys_modules_snapshot.restore()
self._sys_path_snapshot.restore() self._sys_path_snapshot.restore()
self._cwd_snapshot.restore() self._cwd_snapshot.restore()
self._monkeypatch.undo()
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
# Some zope modules used by twisted-related tests keep internal state # Some zope modules used by twisted-related tests keep internal state
@ -830,7 +832,7 @@ class Pytester:
return self._makefile(ext, args, kwargs) return self._makefile(ext, args, kwargs)
def makeconftest(self, source: str) -> Path: def makeconftest(self, source: str) -> Path:
"""Write a contest.py file with 'source' as contents.""" """Write a conftest.py file with 'source' as contents."""
return self.makepyfile(conftest=source) return self.makepyfile(conftest=source)
def makeini(self, source: str) -> Path: def makeini(self, source: str) -> Path:

View File

@ -905,8 +905,6 @@ class InstanceDummy:
only to ignore it; this dummy class keeps them working. This will be removed only to ignore it; this dummy class keeps them working. This will be removed
in pytest 8.""" in pytest 8."""
pass
def __getattr__(name: str) -> object: def __getattr__(name: str) -> object:
if name == "Instance": if name == "Instance":
@ -942,7 +940,7 @@ class IdMaker:
# ParameterSet. # ParameterSet.
idfn: Optional[Callable[[Any], Optional[object]]] idfn: Optional[Callable[[Any], Optional[object]]]
# Optionally, explicit IDs for ParameterSets by index. # Optionally, explicit IDs for ParameterSets by index.
ids: Optional[Sequence[Union[None, str]]] ids: Optional[Sequence[Optional[object]]]
# Optionally, the pytest config. # Optionally, the pytest config.
# Used for controlling ASCII escaping, and for calling the # Used for controlling ASCII escaping, and for calling the
# :hook:`pytest_make_parametrize_id` hook. # :hook:`pytest_make_parametrize_id` hook.
@ -950,6 +948,9 @@ class IdMaker:
# Optionally, the ID of the node being parametrized. # Optionally, the ID of the node being parametrized.
# Used only for clearer error messages. # Used only for clearer error messages.
nodeid: Optional[str] nodeid: Optional[str]
# Optionally, the ID of the function being parametrized.
# Used only for clearer error messages.
func_name: Optional[str]
def make_unique_parameterset_ids(self) -> List[str]: def make_unique_parameterset_ids(self) -> List[str]:
"""Make a unique identifier for each ParameterSet, that may be used to """Make a unique identifier for each ParameterSet, that may be used to
@ -984,9 +985,7 @@ class IdMaker:
yield parameterset.id yield parameterset.id
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None: elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
# ID provided in the IDs list - parametrize(..., ids=[...]). # ID provided in the IDs list - parametrize(..., ids=[...]).
id = self.ids[idx] yield self._idval_from_value_required(self.ids[idx], idx)
assert id is not None
yield _ascii_escaped_by_config(id, self.config)
else: else:
# ID not provided - generate it. # ID not provided - generate it.
yield "-".join( yield "-".join(
@ -1055,6 +1054,25 @@ class IdMaker:
return name return name
return None return None
def _idval_from_value_required(self, val: object, idx: int) -> str:
"""Like _idval_from_value(), but fails if the type is not supported."""
id = self._idval_from_value(val)
if id is not None:
return id
# Fail.
if self.func_name is not None:
prefix = f"In {self.func_name}: "
elif self.nodeid is not None:
prefix = f"In {self.nodeid}: "
else:
prefix = ""
msg = (
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
)
fail(msg, pytrace=False)
@staticmethod @staticmethod
def _idval_from_argname(argname: str, idx: int) -> str: def _idval_from_argname(argname: str, idx: int) -> str:
"""Make an ID for a parameter in a ParameterSet from the argument name """Make an ID for a parameter in a ParameterSet from the argument name
@ -1184,10 +1202,7 @@ class Metafunc:
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]], argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
indirect: Union[bool, Sequence[str]] = False, indirect: Union[bool, Sequence[str]] = False,
ids: Optional[ ids: Optional[
Union[ Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = None, ] = None,
scope: "Optional[_ScopeName]" = None, scope: "Optional[_ScopeName]" = None,
*, *,
@ -1318,10 +1333,7 @@ class Metafunc:
self, self,
argnames: Sequence[str], argnames: Sequence[str],
ids: Optional[ ids: Optional[
Union[ Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
], ],
parametersets: Sequence[ParameterSet], parametersets: Sequence[ParameterSet],
nodeid: str, nodeid: str,
@ -1351,16 +1363,22 @@ class Metafunc:
idfn = None idfn = None
ids_ = self._validate_ids(ids, parametersets, self.function.__name__) ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
id_maker = IdMaker( id_maker = IdMaker(
argnames, parametersets, idfn, ids_, self.config, nodeid=nodeid argnames,
parametersets,
idfn,
ids_,
self.config,
nodeid=nodeid,
func_name=self.function.__name__,
) )
return id_maker.make_unique_parameterset_ids() return id_maker.make_unique_parameterset_ids()
def _validate_ids( def _validate_ids(
self, self,
ids: Iterable[Union[None, str, float, int, bool]], ids: Iterable[Optional[object]],
parametersets: Sequence[ParameterSet], parametersets: Sequence[ParameterSet],
func_name: str, func_name: str,
) -> List[Union[None, str]]: ) -> List[Optional[object]]:
try: try:
num_ids = len(ids) # type: ignore[arg-type] num_ids = len(ids) # type: ignore[arg-type]
except TypeError: except TypeError:
@ -1375,22 +1393,7 @@ class Metafunc:
msg = "In {}: {} parameter sets specified, with different number of ids: {}" msg = "In {}: {} parameter sets specified, with different number of ids: {}"
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False) fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
new_ids = [] return list(itertools.islice(ids, num_ids))
for idx, id_value in enumerate(itertools.islice(ids, num_ids)):
if id_value is None or isinstance(id_value, str):
new_ids.append(id_value)
elif isinstance(id_value, (float, int, bool)):
new_ids.append(str(id_value))
else:
msg = ( # type: ignore[unreachable]
"In {}: ids must be list of string/float/int/bool, "
"found: {} (type: {!r}) at index {}"
)
fail(
msg.format(func_name, saferepr(id_value), type(id_value), idx),
pytrace=False,
)
return new_ids
def _resolve_arg_value_types( def _resolve_arg_value_types(
self, self,

View File

@ -1,5 +1,6 @@
import math import math
import pprint import pprint
from collections.abc import Collection
from collections.abc import Sized from collections.abc import Sized
from decimal import Decimal from decimal import Decimal
from numbers import Complex from numbers import Complex
@ -8,7 +9,6 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Generic from typing import Generic
from typing import Iterable
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
@ -131,7 +131,6 @@ class ApproxBase:
# a numeric type. For this reason, the default is to do nothing. The # a numeric type. For this reason, the default is to do nothing. The
# classes that deal with sequences should reimplement this method to # classes that deal with sequences should reimplement this method to
# raise if there are any non-numeric elements in the sequence. # raise if there are any non-numeric elements in the sequence.
pass
def _recursive_list_map(f, x): def _recursive_list_map(f, x):
@ -307,12 +306,12 @@ class ApproxMapping(ApproxBase):
raise TypeError(msg.format(key, value, pprint.pformat(self.expected))) raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
class ApproxSequencelike(ApproxBase): class ApproxSequenceLike(ApproxBase):
"""Perform approximate comparisons where the expected value is a sequence of numbers.""" """Perform approximate comparisons where the expected value is a sequence of numbers."""
def __repr__(self) -> str: def __repr__(self) -> str:
seq_type = type(self.expected) seq_type = type(self.expected)
if seq_type not in (tuple, list, set): if seq_type not in (tuple, list):
seq_type = list seq_type = list
return "approx({!r})".format( return "approx({!r})".format(
seq_type(self._approx_scalar(x) for x in self.expected) seq_type(self._approx_scalar(x) for x in self.expected)
@ -320,7 +319,6 @@ class ApproxSequencelike(ApproxBase):
def _repr_compare(self, other_side: Sequence[float]) -> List[str]: def _repr_compare(self, other_side: Sequence[float]) -> List[str]:
import math import math
import numpy as np
if len(self.expected) != len(other_side): if len(self.expected) != len(other_side):
return [ return [
@ -341,7 +339,7 @@ class ApproxSequencelike(ApproxBase):
abs_diff = abs(approx_value.expected - other_value) abs_diff = abs(approx_value.expected - other_value)
max_abs_diff = max(max_abs_diff, abs_diff) max_abs_diff = max(max_abs_diff, abs_diff)
if other_value == 0.0: if other_value == 0.0:
max_rel_diff = np.inf max_rel_diff = math.inf
else: else:
max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value)) max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
different_ids.append(i) different_ids.append(i)
@ -516,7 +514,7 @@ class ApproxDecimal(ApproxScalar):
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
"""Assert that two numbers (or two sets of numbers) are equal to each other """Assert that two numbers (or two ordered sequences of numbers) are equal to each other
within some tolerance. within some tolerance.
Due to the :std:doc:`tutorial/floatingpoint`, numbers that we Due to the :std:doc:`tutorial/floatingpoint`, numbers that we
@ -548,16 +546,11 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
>>> 0.1 + 0.2 == approx(0.3) >>> 0.1 + 0.2 == approx(0.3)
True True
The same syntax also works for sequences of numbers:: The same syntax also works for ordered sequences of numbers::
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6)) >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
True True
Dictionary *values*::
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
True
``numpy`` arrays:: ``numpy`` arrays::
>>> import numpy as np # doctest: +SKIP >>> import numpy as np # doctest: +SKIP
@ -570,6 +563,20 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
True True
Only ordered sequences are supported, because ``approx`` needs
to infer the relative position of the sequences without ambiguity. This means
``sets`` and other unordered sequences are not supported.
Finally, dictionary *values* can also be compared::
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
True
The comparison will be true if both mappings have the same keys and their
respective values match the expected tolerances.
**Tolerances**
By default, ``approx`` considers numbers within a relative tolerance of By default, ``approx`` considers numbers within a relative tolerance of
``1e-6`` (i.e. one part in a million) of its expected value to be equal. ``1e-6`` (i.e. one part in a million) of its expected value to be equal.
This treatment would lead to surprising results if the expected value was This treatment would lead to surprising results if the expected value was
@ -709,12 +716,19 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
expected = _as_numpy_array(expected) expected = _as_numpy_array(expected)
cls = ApproxNumpy cls = ApproxNumpy
elif ( elif (
isinstance(expected, Iterable) hasattr(expected, "__getitem__")
and isinstance(expected, Sized) and isinstance(expected, Sized)
# Type ignored because the error is wrong -- not unreachable. # Type ignored because the error is wrong -- not unreachable.
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable] and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
): ):
cls = ApproxSequencelike cls = ApproxSequenceLike
elif (
isinstance(expected, Collection)
# Type ignored because the error is wrong -- not unreachable.
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
):
msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
raise TypeError(msg)
else: else:
cls = ApproxScalar cls = ApproxScalar

View File

@ -1,6 +1,7 @@
"""Record warnings during test function execution.""" """Record warnings during test function execution."""
import re import re
import warnings import warnings
from pprint import pformat
from types import TracebackType from types import TracebackType
from typing import Any from typing import Any
from typing import Callable from typing import Callable
@ -110,7 +111,7 @@ def warns(
r"""Assert that code raises a particular class of warning. r"""Assert that code raises a particular class of warning.
Specifically, the parameter ``expected_warning`` can be a warning class or Specifically, the parameter ``expected_warning`` can be a warning class or
sequence of warning classes, and the inside the ``with`` block must issue a warning of that class or sequence of warning classes, and the code inside the ``with`` block must issue a warning of that class or
classes. classes.
This helper produces a list of :class:`warnings.WarningMessage` objects, This helper produces a list of :class:`warnings.WarningMessage` objects,
@ -142,10 +143,11 @@ def warns(
__tracebackhide__ = True __tracebackhide__ = True
if not args: if not args:
if kwargs: if kwargs:
msg = "Unexpected keyword arguments passed to pytest.warns: " argnames = ", ".join(sorted(kwargs))
msg += ", ".join(sorted(kwargs)) raise TypeError(
msg += "\nUse context-manager form instead?" f"Unexpected keyword arguments passed to pytest.warns: {argnames}"
raise TypeError(msg) "\nUse context-manager form instead?"
)
return WarningsChecker(expected_warning, match_expr=match, _ispytest=True) return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
else: else:
func = args[0] func = args[0]
@ -191,7 +193,7 @@ class WarningsRecorder(warnings.catch_warnings):
if issubclass(w.category, cls): if issubclass(w.category, cls):
return self._list.pop(i) return self._list.pop(i)
__tracebackhide__ = True __tracebackhide__ = True
raise AssertionError("%r not found in warning list" % cls) raise AssertionError(f"{cls!r} not found in warning list")
def clear(self) -> None: def clear(self) -> None:
"""Clear the list of recorded warnings.""" """Clear the list of recorded warnings."""
@ -202,7 +204,7 @@ class WarningsRecorder(warnings.catch_warnings):
def __enter__(self) -> "WarningsRecorder": # type: ignore def __enter__(self) -> "WarningsRecorder": # type: ignore
if self._entered: if self._entered:
__tracebackhide__ = True __tracebackhide__ = True
raise RuntimeError("Cannot enter %r twice" % self) raise RuntimeError(f"Cannot enter {self!r} twice")
_list = super().__enter__() _list = super().__enter__()
# record=True means it's None. # record=True means it's None.
assert _list is not None assert _list is not None
@ -218,7 +220,7 @@ class WarningsRecorder(warnings.catch_warnings):
) -> None: ) -> None:
if not self._entered: if not self._entered:
__tracebackhide__ = True __tracebackhide__ = True
raise RuntimeError("Cannot exit %r without entering first" % self) raise RuntimeError(f"Cannot exit {self!r} without entering first")
super().__exit__(exc_type, exc_val, exc_tb) super().__exit__(exc_type, exc_val, exc_tb)
@ -268,16 +270,17 @@ class WarningsChecker(WarningsRecorder):
__tracebackhide__ = True __tracebackhide__ = True
def found_str():
return pformat([record.message for record in self], indent=2)
# only check if we're not currently handling an exception # only check if we're not currently handling an exception
if exc_type is None and exc_val is None and exc_tb is None: if exc_type is None and exc_val is None and exc_tb is None:
if self.expected_warning is not None: if self.expected_warning is not None:
if not any(issubclass(r.category, self.expected_warning) for r in self): if not any(issubclass(r.category, self.expected_warning) for r in self):
__tracebackhide__ = True __tracebackhide__ = True
fail( fail(
"DID NOT WARN. No warnings of type {} were emitted. " f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
"The list of emitted warnings is: {}.".format( f"The list of emitted warnings is: {found_str()}."
self.expected_warning, [each.message for each in self]
)
) )
elif self.match_expr is not None: elif self.match_expr is not None:
for r in self: for r in self:
@ -286,11 +289,8 @@ class WarningsChecker(WarningsRecorder):
break break
else: else:
fail( fail(
"DID NOT WARN. No warnings of type {} matching" f"""\
" ('{}') were emitted. The list of emitted warnings" DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.
" is: {}.".format( Regex: {self.match_expr}
self.expected_warning, Emitted warnings: {found_str()}"""
self.match_expr,
[each.message for each in self],
)
) )

View File

@ -2,7 +2,6 @@
import bdb import bdb
import os import os
import sys import sys
import warnings
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
@ -28,7 +27,6 @@ from _pytest._code.code import TerminalRepr
from _pytest.compat import final from _pytest.compat import final
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import UNITTEST_SKIP_DURING_COLLECTION
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.nodes import Node from _pytest.nodes import Node
@ -379,11 +377,6 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
# Type ignored because unittest is loaded dynamically. # Type ignored because unittest is loaded dynamically.
skip_exceptions.append(unittest.SkipTest) # type: ignore skip_exceptions.append(unittest.SkipTest) # type: ignore
if isinstance(call.excinfo.value, tuple(skip_exceptions)): if isinstance(call.excinfo.value, tuple(skip_exceptions)):
if unittest is not None and isinstance(
call.excinfo.value, unittest.SkipTest # type: ignore[attr-defined]
):
warnings.warn(UNITTEST_SKIP_DURING_COLLECTION, stacklevel=2)
outcome = "skipped" outcome = "skipped"
r_ = collector._repr_failure_py(call.excinfo, "line") r_ = collector._repr_failure_py(call.excinfo, "line")
assert isinstance(r_, ExceptionChainRepr), repr(r_) assert isinstance(r_, ExceptionChainRepr), repr(r_)

View File

@ -663,7 +663,7 @@ class TerminalReporter:
errors = len(self.stats.get("error", [])) errors = len(self.stats.get("error", []))
skipped = len(self.stats.get("skipped", [])) skipped = len(self.stats.get("skipped", []))
deselected = len(self.stats.get("deselected", [])) deselected = len(self.stats.get("deselected", []))
selected = self._numcollected - errors - skipped - deselected selected = self._numcollected - deselected
line = "collected " if final else "collecting " line = "collected " if final else "collecting "
line += ( line += (
str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
@ -674,7 +674,7 @@ class TerminalReporter:
line += " / %d deselected" % deselected line += " / %d deselected" % deselected
if skipped: if skipped:
line += " / %d skipped" % skipped line += " / %d skipped" % skipped
if self._numcollected > selected > 0: if self._numcollected > selected:
line += " / %d selected" % selected line += " / %d selected" % selected
if self.isatty: if self.isatty:
self.rewrite(line, bold=True, erase=True) self.rewrite(line, bold=True, erase=True)

View File

@ -185,6 +185,15 @@ class TestCaseFunction(Function):
_excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
_testcase: Optional["unittest.TestCase"] = None _testcase: Optional["unittest.TestCase"] = None
def _getobj(self):
assert self.parent is not None
# Unlike a regular Function in a Class, where `item.obj` returns
# a *bound* method (attached to an instance), TestCaseFunction's
# `obj` returns an *unbound* method (not attached to an instance).
# This inconsistency is probably not desirable, but needs some
# consideration before changing.
return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
def setup(self) -> None: def setup(self) -> None:
# A bound method to be called during teardown() if set (see 'runtest()'). # A bound method to be called during teardown() if set (see 'runtest()').
self._explicit_tearDown: Optional[Callable[[], None]] = None self._explicit_tearDown: Optional[Callable[[], None]] = None

View File

@ -81,6 +81,23 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
warning_message.lineno, warning_message.lineno,
warning_message.line, warning_message.line,
) )
if warning_message.source is not None:
try:
import tracemalloc
except ImportError:
pass
else:
tb = tracemalloc.get_object_traceback(warning_message.source)
if tb is not None:
formatted_tb = "\n".join(tb.format())
# Use a leading new line to better separate the (large) output
# from the traceback to the previous warning text.
msg += f"\nObject allocated at:\n{formatted_tb}"
else:
# No need for a leading new line.
url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
msg += "Enable tracemalloc to get traceback where the object was allocated.\n"
msg += f"See {url} for more info."
return msg return msg

View File

@ -1238,8 +1238,6 @@ def test_pdb_can_be_rewritten(pytester: Pytester) -> None:
" def check():", " def check():",
"> assert 1 == 2", "> assert 1 == 2",
"E assert 1 == 2", "E assert 1 == 2",
"E +1",
"E -2",
"", "",
"pdb.py:2: AssertionError", "pdb.py:2: AssertionError",
"*= 1 failed in *", "*= 1 failed in *",

View File

@ -420,18 +420,20 @@ def test_match_raises_error(pytester: Pytester) -> None:
excinfo.match(r'[123]+') excinfo.match(r'[123]+')
""" """
) )
result = pytester.runpytest() result = pytester.runpytest("--tb=short")
assert result.ret != 0 assert result.ret != 0
exc_msg = "Regex pattern '[[]123[]]+' does not match 'division by zero'." match = [
result.stdout.fnmatch_lines([f"E * AssertionError: {exc_msg}"]) r"E .* AssertionError: Regex pattern did not match.",
r"E .* Regex: '\[123\]\+'",
r"E .* Input: 'division by zero'",
]
result.stdout.re_match_lines(match)
result.stdout.no_fnmatch_line("*__tracebackhide__ = True*") result.stdout.no_fnmatch_line("*__tracebackhide__ = True*")
result = pytester.runpytest("--fulltrace") result = pytester.runpytest("--fulltrace")
assert result.ret != 0 assert result.ret != 0
result.stdout.fnmatch_lines( result.stdout.re_match_lines([r".*__tracebackhide__ = True.*", *match])
["*__tracebackhide__ = True*", f"E * AssertionError: {exc_msg}"]
)
class TestFormattedExcinfo: class TestFormattedExcinfo:

View File

@ -1,16 +1,13 @@
# flake8: noqa # flake8: noqa
# disable flake check on this file because some constructs are strange # disable flake check on this file because some constructs are strange
# or redundant on purpose and can't be disable on a line-by-line basis # or redundant on purpose and can't be disable on a line-by-line basis
import ast
import inspect import inspect
import linecache import linecache
import sys import sys
import textwrap import textwrap
from pathlib import Path from pathlib import Path
from types import CodeType
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import Optional
import pytest import pytest
from _pytest._code import Code from _pytest._code import Code

View File

@ -86,23 +86,6 @@ def test_private_is_deprecated() -> None:
PrivateInit(10, _ispytest=True) PrivateInit(10, _ispytest=True)
def test_raising_unittest_skiptest_during_collection_is_deprecated(
pytester: Pytester,
) -> None:
pytester.makepyfile(
"""
import unittest
raise unittest.SkipTest()
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(
[
"*PytestRemovedIn8Warning: Raising unittest.SkipTest*",
]
)
@pytest.mark.parametrize("hooktype", ["hook", "ihook"]) @pytest.mark.parametrize("hooktype", ["hook", "ihook"])
def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request): def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request):
path = legacy_path(tmp_path) path = legacy_path(tmp_path)

View File

@ -2,6 +2,7 @@ import pytest
from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
def test_simple_repr(): def test_simple_repr():
@ -179,3 +180,23 @@ def test_broken_getattribute():
assert saferepr(SomeClass()).startswith( assert saferepr(SomeClass()).startswith(
"<[RuntimeError() raised in repr()] SomeClass object at 0x" "<[RuntimeError() raised in repr()] SomeClass object at 0x"
) )
def test_saferepr_unlimited():
dict5 = {f"v{i}": i for i in range(5)}
assert saferepr_unlimited(dict5) == "{'v0': 0, 'v1': 1, 'v2': 2, 'v3': 3, 'v4': 4}"
dict_long = {f"v{i}": i for i in range(1_000)}
r = saferepr_unlimited(dict_long)
assert "..." not in r
assert "\n" not in r
def test_saferepr_unlimited_exc():
class A:
def __repr__(self):
raise ValueError(42)
assert saferepr_unlimited(A()).startswith(
"<[ValueError(42) raised in repr()] A object at 0x"
)

View File

@ -1,6 +1,6 @@
anyio[curio,trio]==3.5.0 anyio[curio,trio]==3.5.0
django==4.0.1 django==4.0.3
pytest-asyncio==0.17.2 pytest-asyncio==0.18.2
pytest-bdd==5.0.0 pytest-bdd==5.0.0
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-django==4.5.2 pytest-django==4.5.2
@ -11,5 +11,5 @@ pytest-rerunfailures==10.2
pytest-sugar==0.9.4 pytest-sugar==0.9.4
pytest-trio==0.7.0 pytest-trio==0.7.0
pytest-twisted==1.13.4 pytest-twisted==1.13.4
twisted==21.7.0 twisted==22.2.0
pytest-xvfb==2.0.0 pytest-xvfb==2.0.0

View File

@ -92,9 +92,7 @@ SOME_INT = r"[0-9]+\s*"
class TestApprox: class TestApprox:
def test_error_messages(self, assert_approx_raises_regex): def test_error_messages_native_dtypes(self, assert_approx_raises_regex):
np = pytest.importorskip("numpy")
assert_approx_raises_regex( assert_approx_raises_regex(
2.0, 2.0,
1.0, 1.0,
@ -135,6 +133,22 @@ class TestApprox:
], ],
) )
# Specific test for comparison with 0.0 (relative diff will be 'inf')
assert_approx_raises_regex(
[0.0],
[1.0],
[
r" comparison failed. Mismatched elements: 1 / 1:",
rf" Max absolute difference: {SOME_FLOAT}",
r" Max relative difference: inf",
r" Index \| Obtained\s+\| Expected ",
rf"\s*0\s*\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
],
)
def test_error_messages_numpy_dtypes(self, assert_approx_raises_regex):
np = pytest.importorskip("numpy")
a = np.linspace(0, 100, 20) a = np.linspace(0, 100, 20)
b = np.linspace(0, 100, 20) b = np.linspace(0, 100, 20)
a[10] += 0.5 a[10] += 0.5
@ -175,18 +189,6 @@ class TestApprox:
) )
# Specific test for comparison with 0.0 (relative diff will be 'inf') # Specific test for comparison with 0.0 (relative diff will be 'inf')
assert_approx_raises_regex(
[0.0],
[1.0],
[
r" comparison failed. Mismatched elements: 1 / 1:",
rf" Max absolute difference: {SOME_FLOAT}",
r" Max relative difference: inf",
r" Index \| Obtained\s+\| Expected ",
rf"\s*0\s*\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
],
)
assert_approx_raises_regex( assert_approx_raises_regex(
np.array([0.0]), np.array([0.0]),
np.array([1.0]), np.array([1.0]),
@ -858,13 +860,21 @@ class TestApprox:
assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-7, abs=0) == actual
assert approx(expected, rel=5e-8, abs=0) != actual assert approx(expected, rel=5e-8, abs=0) != actual
def test_generic_sized_iterable_object(self): def test_generic_ordered_sequence(self):
class MySizedIterable: class MySequence:
def __iter__(self): def __getitem__(self, i):
return iter([1, 2, 3, 4]) return [1, 2, 3, 4][i]
def __len__(self): def __len__(self):
return 4 return 4
expected = MySizedIterable() expected = MySequence()
assert [1, 2, 3, 4] == approx(expected) assert [1, 2, 3, 4] == approx(expected, abs=1e-4)
expected_repr = "approx([1 ± 1.0e-06, 2 ± 2.0e-06, 3 ± 3.0e-06, 4 ± 4.0e-06])"
assert repr(approx(expected)) == expected_repr
def test_allow_ordered_sequences_only(self) -> None:
"""pytest.approx() should raise an error on unordered sequences (#9692)."""
with pytest.raises(TypeError, match="only supports ordered sequences"):
assert {1, 2, 3} == approx({1, 2, 3})

View File

@ -106,8 +106,8 @@ class TestMetafunc:
with pytest.raises( with pytest.raises(
fail.Exception, fail.Exception,
match=( match=(
r"In func: ids must be list of string/float/int/bool, found:" r"In func: ids contains unsupported value Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2. "
r" Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2" r"Supported types are: .*"
), ),
): ):
metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type] metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type]
@ -285,7 +285,7 @@ class TestMetafunc:
deadline=400.0 deadline=400.0
) # very close to std deadline and CI boxes are not reliable in CPU power ) # very close to std deadline and CI boxes are not reliable in CPU power
def test_idval_hypothesis(self, value) -> None: def test_idval_hypothesis(self, value) -> None:
escaped = IdMaker([], [], None, None, None, None)._idval(value, "a", 6) escaped = IdMaker([], [], None, None, None, None, None)._idval(value, "a", 6)
assert isinstance(escaped, str) assert isinstance(escaped, str)
escaped.encode("ascii") escaped.encode("ascii")
@ -308,7 +308,8 @@ class TestMetafunc:
] ]
for val, expected in values: for val, expected in values:
assert ( assert (
IdMaker([], [], None, None, None, None)._idval(val, "a", 6) == expected IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
) )
def test_unicode_idval_with_config(self) -> None: def test_unicode_idval_with_config(self) -> None:
@ -337,7 +338,7 @@ class TestMetafunc:
("ação", MockConfig({option: False}), "a\\xe7\\xe3o"), ("ação", MockConfig({option: False}), "a\\xe7\\xe3o"),
] ]
for val, config, expected in values: for val, config, expected in values:
actual = IdMaker([], [], None, None, config, None)._idval(val, "a", 6) actual = IdMaker([], [], None, None, config, None, None)._idval(val, "a", 6)
assert actual == expected assert actual == expected
def test_bytes_idval(self) -> None: def test_bytes_idval(self) -> None:
@ -351,7 +352,8 @@ class TestMetafunc:
] ]
for val, expected in values: for val, expected in values:
assert ( assert (
IdMaker([], [], None, None, None, None)._idval(val, "a", 6) == expected IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
) )
def test_class_or_function_idval(self) -> None: def test_class_or_function_idval(self) -> None:
@ -367,7 +369,8 @@ class TestMetafunc:
values = [(TestClass, "TestClass"), (test_function, "test_function")] values = [(TestClass, "TestClass"), (test_function, "test_function")]
for val, expected in values: for val, expected in values:
assert ( assert (
IdMaker([], [], None, None, None, None)._idval(val, "a", 6) == expected IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
) )
def test_notset_idval(self) -> None: def test_notset_idval(self) -> None:
@ -376,7 +379,9 @@ class TestMetafunc:
Regression test for #7686. Regression test for #7686.
""" """
assert IdMaker([], [], None, None, None, None)._idval(NOTSET, "a", 0) == "a0" assert (
IdMaker([], [], None, None, None, None, None)._idval(NOTSET, "a", 0) == "a0"
)
def test_idmaker_autoname(self) -> None: def test_idmaker_autoname(self) -> None:
"""#250""" """#250"""
@ -387,6 +392,7 @@ class TestMetafunc:
None, None,
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["string-1.0", "st-ring-2.0"] assert result == ["string-1.0", "st-ring-2.0"]
@ -397,17 +403,18 @@ class TestMetafunc:
None, None,
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["a0-1.0", "a1-b1"] assert result == ["a0-1.0", "a1-b1"]
# unicode mixing, issue250 # unicode mixing, issue250
result = IdMaker( result = IdMaker(
("a", "b"), [pytest.param({}, b"\xc3\xb4")], None, None, None, None ("a", "b"), [pytest.param({}, b"\xc3\xb4")], None, None, None, None, None
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["a0-\\xc3\\xb4"] assert result == ["a0-\\xc3\\xb4"]
def test_idmaker_with_bytes_regex(self) -> None: def test_idmaker_with_bytes_regex(self) -> None:
result = IdMaker( result = IdMaker(
("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None ("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None, None
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["foo"] assert result == ["foo"]
@ -433,6 +440,7 @@ class TestMetafunc:
None, None,
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == [ assert result == [
"1.0--1.1", "1.0--1.1",
@ -465,6 +473,7 @@ class TestMetafunc:
None, None,
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"] assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"]
@ -479,6 +488,7 @@ class TestMetafunc:
None, None,
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["hello \\x00", "hello \\x05"] assert result == ["hello \\x00", "hello \\x05"]
@ -486,7 +496,7 @@ class TestMetafunc:
enum = pytest.importorskip("enum") enum = pytest.importorskip("enum")
e = enum.Enum("Foo", "one, two") e = enum.Enum("Foo", "one, two")
result = IdMaker( result = IdMaker(
("a", "b"), [pytest.param(e.one, e.two)], None, None, None, None ("a", "b"), [pytest.param(e.one, e.two)], None, None, None, None, None
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["Foo.one-Foo.two"] assert result == ["Foo.one-Foo.two"]
@ -509,6 +519,7 @@ class TestMetafunc:
None, None,
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"] assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"]
@ -529,6 +540,7 @@ class TestMetafunc:
None, None,
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["a-a0", "a-a1", "a-a2"] assert result == ["a-a0", "a-a1", "a-a2"]
@ -560,7 +572,13 @@ class TestMetafunc:
] ]
for config, expected in values: for config, expected in values:
result = IdMaker( result = IdMaker(
("a",), [pytest.param("string")], lambda _: "ação", None, config, None ("a",),
[pytest.param("string")],
lambda _: "ação",
None,
config,
None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == [expected] assert result == [expected]
@ -592,7 +610,7 @@ class TestMetafunc:
] ]
for config, expected in values: for config, expected in values:
result = IdMaker( result = IdMaker(
("a",), [pytest.param("string")], None, ["ação"], config, None ("a",), [pytest.param("string")], None, ["ação"], config, None, None
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == [expected] assert result == [expected]
@ -657,6 +675,7 @@ class TestMetafunc:
["a", None], ["a", None],
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["a", "3-4"] assert result == ["a", "3-4"]
@ -668,6 +687,7 @@ class TestMetafunc:
["a", None], ["a", None],
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["me", "you"] assert result == ["me", "you"]
@ -679,6 +699,7 @@ class TestMetafunc:
["a", "a", "b", "c", "b"], ["a", "a", "b", "c", "b"],
None, None,
None, None,
None,
).make_unique_parameterset_ids() ).make_unique_parameterset_ids()
assert result == ["a0", "a1", "b0", "c", "b1"] assert result == ["a0", "a1", "b0", "c", "b1"]
@ -1318,7 +1339,7 @@ class TestMetafuncFunctional:
""" """
import pytest import pytest
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type)) @pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, OSError()))
def test_ids_numbers(x,expected): def test_ids_numbers(x,expected):
assert x * 2 == expected assert x * 2 == expected
""" """
@ -1326,8 +1347,8 @@ class TestMetafuncFunctional:
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"In test_ids_numbers: ids must be list of string/float/int/bool," "In test_ids_numbers: ids contains unsupported value OSError() (type: <class 'OSError'>) at index 2. "
" found: <class 'type'> (type: <class 'type'>) at index 2" "Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
] ]
) )

View File

@ -191,10 +191,12 @@ class TestRaises:
int("asdf") int("asdf")
msg = "with base 16" msg = "with base 16"
expr = "Regex pattern {!r} does not match \"invalid literal for int() with base 10: 'asdf'\".".format( expr = (
msg "Regex pattern did not match.\n"
f" Regex: {msg!r}\n"
" Input: \"invalid literal for int() with base 10: 'asdf'\""
) )
with pytest.raises(AssertionError, match=re.escape(expr)): with pytest.raises(AssertionError, match="(?m)" + re.escape(expr)):
with pytest.raises(ValueError, match=msg): with pytest.raises(ValueError, match=msg):
int("asdf", base=10) int("asdf", base=10)
@ -217,7 +219,7 @@ class TestRaises:
with pytest.raises(AssertionError, match="'foo"): with pytest.raises(AssertionError, match="'foo"):
raise AssertionError("'bar") raise AssertionError("'bar")
(msg,) = excinfo.value.args (msg,) = excinfo.value.args
assert msg == 'Regex pattern "\'foo" does not match "\'bar".' assert msg == '''Regex pattern did not match.\n Regex: "'foo"\n Input: "'bar"'''
def test_match_failure_exact_string_message(self): def test_match_failure_exact_string_message(self):
message = "Oh here is a message with (42) numbers in parameters" message = "Oh here is a message with (42) numbers in parameters"
@ -226,9 +228,10 @@ class TestRaises:
raise AssertionError(message) raise AssertionError(message)
(msg,) = excinfo.value.args (msg,) = excinfo.value.args
assert msg == ( assert msg == (
"Regex pattern 'Oh here is a message with (42) numbers in " "Regex pattern did not match.\n"
"parameters' does not match 'Oh here is a message with (42) " " Regex: 'Oh here is a message with (42) numbers in parameters'\n"
"numbers in parameters'. Did you mean to `re.escape()` the regex?" " Input: 'Oh here is a message with (42) numbers in parameters'\n"
" Did you mean to `re.escape()` the regex?"
) )
def test_raises_match_wrong_type(self): def test_raises_match_wrong_type(self):

View File

@ -83,7 +83,7 @@ class TestImportHookInstallation:
"E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}", "E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}",
"E Omitting 1 identical items, use -vv to show", "E Omitting 1 identical items, use -vv to show",
"E Differing items:", "E Differing items:",
"E Use -v to get the full diff", "E Use -v to get more diff",
] ]
) )
# XXX: unstable output. # XXX: unstable output.
@ -376,7 +376,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 the full diff", "Use -v to get more diff",
] ]
def test_bytes_diff_verbose(self) -> None: def test_bytes_diff_verbose(self) -> None:
@ -444,11 +444,19 @@ class TestAssert_reprcompare:
""" """
expl = callequal(left, right, verbose=0) expl = callequal(left, right, verbose=0)
assert expl is not None assert expl is not None
assert expl[-1] == "Use -v to get the full diff" assert expl[-1] == "Use -v to get more diff"
verbose_expl = callequal(left, right, verbose=1) verbose_expl = callequal(left, right, verbose=1)
assert verbose_expl is not None assert verbose_expl is not None
assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip())
def test_iterable_quiet(self) -> None:
expl = callequal([1, 2], [10, 2], verbose=-1)
assert expl == [
"[1, 2] == [10, 2]",
"At index 0 diff: 1 != 10",
"Use -v to get more diff",
]
def test_iterable_full_diff_ci( def test_iterable_full_diff_ci(
self, monkeypatch: MonkeyPatch, pytester: Pytester self, monkeypatch: MonkeyPatch, pytester: Pytester
) -> None: ) -> None:
@ -466,7 +474,7 @@ class TestAssert_reprcompare:
monkeypatch.delenv("CI", raising=False) monkeypatch.delenv("CI", raising=False)
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["E Use -v to get the full diff"]) result.stdout.fnmatch_lines(["E Use -v to get more diff"])
def test_list_different_lengths(self) -> None: def test_list_different_lengths(self) -> None:
expl = callequal([0, 1], [0, 1, 2]) expl = callequal([0, 1], [0, 1, 2])
@ -699,32 +707,6 @@ class TestAssert_reprcompare:
assert expl is not None assert expl is not None
assert len(expl) > 1 assert len(expl) > 1
def test_repr_verbose(self) -> None:
class Nums:
def __init__(self, nums):
self.nums = nums
def __repr__(self):
return str(self.nums)
list_x = list(range(5000))
list_y = list(range(5000))
list_y[len(list_y) // 2] = 3
nums_x = Nums(list_x)
nums_y = Nums(list_y)
assert callequal(nums_x, nums_y) is None
expl = callequal(nums_x, nums_y, verbose=1)
assert expl is not None
assert "+" + repr(nums_x) in expl
assert "-" + repr(nums_y) in expl
expl = callequal(nums_x, nums_y, verbose=2)
assert expl is not None
assert "+" + repr(nums_x) in expl
assert "-" + repr(nums_y) in expl
def test_list_bad_repr(self) -> None: def test_list_bad_repr(self) -> None:
class A: class A:
def __repr__(self): def __repr__(self):
@ -851,8 +833,6 @@ class TestAssert_reprcompare_dataclass:
"E ", "E ",
"E Drill down into differing attribute a:", "E Drill down into differing attribute a:",
"E a: 10 != 20", "E a: 10 != 20",
"E +10",
"E -20",
"E ", "E ",
"E Drill down into differing attribute b:", "E Drill down into differing attribute b:",
"E b: 'ten' != 'xxx'", "E b: 'ten' != 'xxx'",
@ -1026,7 +1006,7 @@ class TestAssert_reprcompare_attrsclass:
assert lines is None assert lines is None
def test_attrs_with_custom_eq(self) -> None: def test_attrs_with_custom_eq(self) -> None:
@attr.define @attr.define(slots=False)
class SimpleDataObject: class SimpleDataObject:
field_a = attr.ib() field_a = attr.ib()
@ -1059,7 +1039,7 @@ class TestAssert_reprcompare_namedtuple:
" b: 'b' != 'c'", " b: 'b' != 'c'",
" - c", " - c",
" + b", " + b",
"Use -v to get the full diff", "Use -v to get more diff",
] ]
def test_comparing_two_different_namedtuple(self) -> None: def test_comparing_two_different_namedtuple(self) -> None:
@ -1074,7 +1054,7 @@ class TestAssert_reprcompare_namedtuple:
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 the full diff", "Use -v to get more diff",
] ]
@ -1648,7 +1628,7 @@ def test_raise_unprintable_assertion_error(pytester: Pytester) -> None:
) )
def test_raise_assertion_error_raisin_repr(pytester: Pytester) -> None: def test_raise_assertion_error_raising_repr(pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(
""" """
class RaisingRepr(object): class RaisingRepr(object):
@ -1659,6 +1639,12 @@ def test_raise_assertion_error_raisin_repr(pytester: Pytester) -> None:
""" """
) )
result = pytester.runpytest() result = pytester.runpytest()
if sys.version_info >= (3, 11):
# python 3.11 has native support for un-str-able exceptions
result.stdout.fnmatch_lines(
["E AssertionError: <exception str() failed>"]
)
else:
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
["E AssertionError: <unprintable AssertionError object>"] ["E AssertionError: <unprintable AssertionError object>"]
) )
@ -1709,3 +1695,18 @@ def test_assertion_location_with_coverage(pytester: Pytester) -> None:
"*= 1 failed in*", "*= 1 failed in*",
] ]
) )
def test_reprcompare_verbose_long() -> None:
a = {f"v{i}": i for i in range(11)}
b = a.copy()
b["v2"] += 10
lines = callop("==", a, b, verbose=2)
assert lines is not None
assert lines[0] == (
"{'v0': 0, 'v1': 1, 'v2': 2, 'v3': 3, 'v4': 4, 'v5': 5, "
"'v6': 6, 'v7': 7, 'v8': 8, 'v9': 9, 'v10': 10}"
" == "
"{'v0': 0, 'v1': 1, 'v2': 12, 'v3': 3, 'v4': 4, 'v5': 5, "
"'v6': 6, 'v7': 7, 'v8': 8, 'v9': 9, 'v10': 10}"
)

View File

@ -13,10 +13,12 @@ from functools import partial
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import Generator
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import Set from typing import Set
from unittest import mock
import _pytest._code import _pytest._code
import pytest import pytest
@ -202,15 +204,7 @@ class TestAssertionRewrite:
def f4() -> None: def f4() -> None:
assert sys == 42 # type: ignore[comparison-overlap] assert sys == 42 # type: ignore[comparison-overlap]
verbose = request.config.getoption("verbose")
msg = getmsg(f4, {"sys": sys}) msg = getmsg(f4, {"sys": sys})
if verbose > 0:
assert msg == (
"assert <module 'sys' (built-in)> == 42\n"
" +<module 'sys' (built-in)>\n"
" -42"
)
else:
assert msg == "assert sys == 42" assert msg == "assert sys == 42"
def f5() -> None: def f5() -> None:
@ -222,19 +216,6 @@ class TestAssertionRewrite:
msg = getmsg(f5, {"cls": X}) msg = getmsg(f5, {"cls": X})
assert msg is not None assert msg is not None
lines = msg.splitlines() lines = msg.splitlines()
if verbose > 1:
assert lines == [
f"assert {X!r} == 42",
f" +{X!r}",
" -42",
]
elif verbose > 0:
assert lines == [
"assert <class 'test_...e.<locals>.X'> == 42",
f" +{X!r}",
" -42",
]
else:
assert lines == ["assert cls == 42"] assert lines == ["assert cls == 42"]
def test_assertrepr_compare_same_width(self, request) -> None: def test_assertrepr_compare_same_width(self, request) -> None:
@ -277,9 +258,6 @@ class TestAssertionRewrite:
msg = getmsg(f, {"cls": Y}) msg = getmsg(f, {"cls": Y})
assert msg is not None assert msg is not None
lines = msg.splitlines() lines = msg.splitlines()
if request.config.getoption("verbose") > 0:
assert lines == ["assert 3 == 2", " +3", " -2"]
else:
assert lines == [ assert lines == [
"assert 3 == 2", "assert 3 == 2",
" + where 3 = Y.foo", " + where 3 = Y.foo",
@ -661,9 +639,6 @@ class TestAssertionRewrite:
assert len(values) == 11 assert len(values) == 11
msg = getmsg(f) msg = getmsg(f)
if request.config.getoption("verbose") > 0:
assert msg == "assert 10 == 11\n +10\n -11"
else:
assert msg == "assert 10 == 11\n + where 10 = len([0, 1, 2, 3, 4, 5, ...])" assert msg == "assert 10 == 11\n + where 10 = len([0, 1, 2, 3, 4, 5, ...])"
def test_custom_reprcompare(self, monkeypatch) -> None: def test_custom_reprcompare(self, monkeypatch) -> None:
@ -730,9 +705,6 @@ class TestAssertionRewrite:
msg = getmsg(f) msg = getmsg(f)
assert msg is not None assert msg is not None
lines = util._format_lines([msg]) lines = util._format_lines([msg])
if request.config.getoption("verbose") > 0:
assert lines == ["assert 0 == 1\n +0\n -1"]
else:
assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"]
def test_custom_repr_non_ascii(self) -> None: def test_custom_repr_non_ascii(self) -> None:
@ -1057,7 +1029,7 @@ class TestAssertionRewriteHookDetails:
e = OSError() e = OSError()
e.errno = 10 e.errno = 10
raise e raise e
yield yield # type:ignore[unreachable]
monkeypatch.setattr( monkeypatch.setattr(
_pytest.assertion.rewrite, "atomic_write", atomic_write_failed _pytest.assertion.rewrite, "atomic_write", atomic_write_failed
@ -1376,7 +1348,7 @@ class TestEarlyRewriteBailout:
@pytest.fixture @pytest.fixture
def hook( def hook(
self, pytestconfig, monkeypatch, pytester: Pytester self, pytestconfig, monkeypatch, pytester: Pytester
) -> AssertionRewritingHook: ) -> Generator[AssertionRewritingHook, None, None]:
"""Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track
if PathFinder.find_spec has been called. if PathFinder.find_spec has been called.
""" """
@ -1397,11 +1369,11 @@ class TestEarlyRewriteBailout:
hook = AssertionRewritingHook(pytestconfig) hook = AssertionRewritingHook(pytestconfig)
# use default patterns, otherwise we inherit pytest's testing config # use default patterns, otherwise we inherit pytest's testing config
hook.fnpats[:] = ["test_*.py", "*_test.py"] with mock.patch.object(hook, "fnpats", ["test_*.py", "*_test.py"]):
monkeypatch.setattr(hook, "_find_spec", spy_find_spec) monkeypatch.setattr(hook, "_find_spec", spy_find_spec)
hook.set_session(StubSession()) # type: ignore[arg-type] hook.set_session(StubSession()) # type: ignore[arg-type]
pytester.syspathinsert() pytester.syspathinsert()
return hook yield hook
def test_basic(self, pytester: Pytester, hook: AssertionRewritingHook) -> None: def test_basic(self, pytester: Pytester, hook: AssertionRewritingHook) -> None:
""" """
@ -1451,7 +1423,7 @@ class TestEarlyRewriteBailout:
} }
) )
pytester.syspathinsert("tests") pytester.syspathinsert("tests")
hook.fnpats[:] = ["tests/**.py"] with mock.patch.object(hook, "fnpats", ["tests/**.py"]):
assert hook.find_spec("file") is not None assert hook.find_spec("file") is not None
assert self.find_spec_calls == ["file"] assert self.find_spec_calls == ["file"]

View File

@ -773,7 +773,7 @@ class TestLastFailed:
result = pytester.runpytest("--lf", "--lfnf", "none") result = pytester.runpytest("--lf", "--lfnf", "none")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"collected 2 items / 2 deselected", "collected 2 items / 2 deselected / 0 selected",
"run-last-failure: no previously failed tests, deselecting all items.", "run-last-failure: no previously failed tests, deselecting all items.",
"deselected=2", "deselected=2",
"* 2 deselected in *", "* 2 deselected in *",

View File

@ -651,7 +651,7 @@ class Test_getinitialnodes:
for parent in col.listchain(): for parent in col.listchain():
assert parent.config is config assert parent.config is config
def test_pkgfile(self, pytester: Pytester) -> None: def test_pkgfile(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
"""Verify nesting when a module is within a package. """Verify nesting when a module is within a package.
The parent chain should match: Module<x.py> -> Package<subdir> -> Session. The parent chain should match: Module<x.py> -> Package<subdir> -> Session.
Session's parent should always be None. Session's parent should always be None.
@ -660,7 +660,8 @@ class Test_getinitialnodes:
subdir = tmp_path.joinpath("subdir") subdir = tmp_path.joinpath("subdir")
x = ensure_file(subdir / "x.py") x = ensure_file(subdir / "x.py")
ensure_file(subdir / "__init__.py") ensure_file(subdir / "__init__.py")
with subdir.cwd(): with monkeypatch.context() as mp:
mp.chdir(subdir)
config = pytester.parseconfigure(x) config = pytester.parseconfigure(x)
col = pytester.getnode(config, x) col = pytester.getnode(config, x)
assert col is not None assert col is not None
@ -1188,7 +1189,6 @@ def test_collect_with_chdir_during_import(pytester: Pytester) -> None:
""" """
% (str(subdir),) % (str(subdir),)
) )
with pytester.path.cwd():
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 passed in*"]) result.stdout.fnmatch_lines(["*1 passed in*"])
assert result.ret == 0 assert result.ret == 0
@ -1200,7 +1200,6 @@ def test_collect_with_chdir_during_import(pytester: Pytester) -> None:
testpaths = . testpaths = .
""" """
) )
with pytester.path.cwd():
result = pytester.runpytest("--collect-only") result = pytester.runpytest("--collect-only")
result.stdout.fnmatch_lines(["collected 1 item"]) result.stdout.fnmatch_lines(["collected 1 item"])
@ -1224,7 +1223,8 @@ def test_collect_pyargs_with_testpaths(
) )
) )
monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep) monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep)
with root.cwd(): with monkeypatch.context() as mp:
mp.chdir(root)
result = pytester.runpytest_subprocess() result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 passed in*"]) result.stdout.fnmatch_lines(["*1 passed in*"])
@ -1507,6 +1507,35 @@ class TestImportModeImportlib:
] ]
) )
def test_using_python_path(self, pytester: Pytester) -> None:
"""
Dummy modules created by insert_missing_modules should not get in
the way of modules that could be imported via python path (#9645).
"""
pytester.makeini(
"""
[pytest]
pythonpath = .
addopts = --import-mode importlib
"""
)
pytester.makepyfile(
**{
"tests/__init__.py": "",
"tests/conftest.py": "",
"tests/subpath/__init__.py": "",
"tests/subpath/helper.py": "",
"tests/subpath/test_something.py": """
import tests.subpath.helper
def test_something():
assert True
""",
}
)
result = pytester.runpytest()
result.stdout.fnmatch_lines("*1 passed in*")
def test_does_not_crash_on_error_from_decorated_function(pytester: Pytester) -> None: def test_does_not_crash_on_error_from_decorated_function(pytester: Pytester) -> None:
"""Regression test for an issue around bad exception formatting due to """Regression test for an issue around bad exception formatting due to

View File

@ -1,4 +1,5 @@
import enum import enum
import sys
from functools import partial from functools import partial
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -91,6 +92,7 @@ def test_get_real_func_partial() -> None:
assert get_real_func(partial(foo)) is foo assert get_real_func(partial(foo)) is foo
@pytest.mark.skipif(sys.version_info >= (3, 11), reason="couroutine removed")
def test_is_generator_asyncio(pytester: Pytester) -> None: def test_is_generator_asyncio(pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(
""" """

View File

@ -163,7 +163,17 @@ class TestParseIni:
pytester.path.joinpath("pytest.ini").write_text("addopts = -x") pytester.path.joinpath("pytest.ini").write_text("addopts = -x")
result = pytester.runpytest() result = pytester.runpytest()
assert result.ret != 0 assert result.ret != 0
result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"]) result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined")
def test_toml_parse_error(self, pytester: Pytester) -> None:
pytester.makepyprojecttoml(
"""
\\"
"""
)
result = pytester.runpytest()
assert result.ret != 0
result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*")
@pytest.mark.xfail(reason="probably not needed") @pytest.mark.xfail(reason="probably not needed")
def test_confcutdir(self, pytester: Pytester) -> None: def test_confcutdir(self, pytester: Pytester) -> None:
@ -1275,7 +1285,7 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
("_pytest.config", "nonwrapper"), ("_pytest.config", "nonwrapper"),
(m.__module__, "nonwrapper"), (m.__module__, "nonwrapper"),
("_pytest.legacypath", "nonwrapper"), ("_pytest.legacypath", "nonwrapper"),
("_pytest.pythonpath", "nonwrapper"), ("_pytest.python_path", "nonwrapper"),
("_pytest.capture", "wrapper"), ("_pytest.capture", "wrapper"),
("_pytest.warnings", "wrapper"), ("_pytest.warnings", "wrapper"),
] ]

View File

@ -252,6 +252,34 @@ def test_conftest_confcutdir(pytester: Pytester) -> None:
result.stdout.no_fnmatch_line("*warning: could not load initial*") result.stdout.no_fnmatch_line("*warning: could not load initial*")
def test_installed_conftest_is_picked_up(pytester: Pytester, tmp_path: Path) -> None:
"""When using `--pyargs` to run tests in an installed packages (located e.g.
in a site-packages in the PYTHONPATH), conftest files in there are picked
up.
Regression test for #9767.
"""
# pytester dir - the source tree.
# tmp_path - the simulated site-packages dir (not in source tree).
pytester.syspathinsert(tmp_path)
pytester.makepyprojecttoml("[tool.pytest.ini_options]")
tmp_path.joinpath("foo").mkdir()
tmp_path.joinpath("foo", "__init__.py").touch()
tmp_path.joinpath("foo", "conftest.py").write_text(
textwrap.dedent(
"""\
import pytest
@pytest.fixture
def fix(): return None
"""
)
)
tmp_path.joinpath("foo", "test_it.py").write_text("def test_it(fix): pass")
result = pytester.runpytest("--pyargs", "foo")
assert result.ret == 0
def test_conftest_symlink(pytester: Pytester) -> None: def test_conftest_symlink(pytester: Pytester) -> None:
"""`conftest.py` discovery follows normal path resolution and does not resolve symlinks.""" """`conftest.py` discovery follows normal path resolution and does not resolve symlinks."""
# Structure: # Structure:

View File

@ -1,4 +1,5 @@
import inspect import inspect
import sys
import textwrap import textwrap
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
@ -200,6 +201,7 @@ class TestDoctests:
"Traceback (most recent call last):", "Traceback (most recent call last):",
' File "*/doctest.py", line *, in __run', ' File "*/doctest.py", line *, in __run',
" *", " *",
*((" *^^^^*",) if sys.version_info >= (3, 11) else ()),
' File "<doctest test_doctest_unexpected_exception.txt[1]>", line 1, in <module>', ' File "<doctest test_doctest_unexpected_exception.txt[1]>", line 1, in <module>',
"ZeroDivisionError: division by zero", "ZeroDivisionError: division by zero",
"*/test_doctest_unexpected_exception.txt:2: UnexpectedException", "*/test_doctest_unexpected_exception.txt:2: UnexpectedException",
@ -801,8 +803,8 @@ class TestDoctests:
""" """
p = pytester.makepyfile( p = pytester.makepyfile(
setup=""" setup="""
from setuptools import setup, find_packages
if __name__ == '__main__': if __name__ == '__main__':
from setuptools import setup, find_packages
setup(name='sample', setup(name='sample',
version='0.0', version='0.0',
description='description', description='description',

View File

@ -231,8 +231,6 @@ TESTCASES = [
E ['a'] E ['a']
E Drill down into differing attribute a: E Drill down into differing attribute a:
E a: 1 != 2 E a: 1 != 2
E +1
E -2
""", """,
id="Compare data classes", id="Compare data classes",
), ),

View File

@ -1,6 +1,7 @@
import argparse import argparse
import os import os
import re import re
import sys
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@ -44,16 +45,32 @@ def test_wrap_session_notify_exception(ret_exc, pytester: Pytester) -> None:
assert result.ret == ExitCode.INTERNAL_ERROR assert result.ret == ExitCode.INTERNAL_ERROR
assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):" assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):"
end_lines = (
result.stdout.lines[-4:]
if sys.version_info >= (3, 11)
else result.stdout.lines[-3:]
)
if exc == SystemExit: if exc == SystemExit:
assert result.stdout.lines[-3:] == [ assert end_lines == [
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart', f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
'INTERNALERROR> raise SystemExit("boom")', 'INTERNALERROR> raise SystemExit("boom")',
*(
("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",)
if sys.version_info >= (3, 11)
else ()
),
"INTERNALERROR> SystemExit: boom", "INTERNALERROR> SystemExit: boom",
] ]
else: else:
assert result.stdout.lines[-3:] == [ assert end_lines == [
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart', f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
'INTERNALERROR> raise ValueError("boom")', 'INTERNALERROR> raise ValueError("boom")',
*(
("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",)
if sys.version_info >= (3, 11)
else ()
),
"INTERNALERROR> ValueError: boom", "INTERNALERROR> ValueError: boom",
] ]
if returncode is False: if returncode is False:
@ -171,6 +188,12 @@ class TestResolveCollectionArgument:
invocation_path, "pkg::foo::bar", as_pypath=True invocation_path, "pkg::foo::bar", as_pypath=True
) )
def test_parametrized_name_with_colons(self, invocation_path: Path) -> None:
ret = resolve_collection_argument(
invocation_path, "src/pkg/test.py::test[a::b]"
)
assert ret == (invocation_path / "src/pkg/test.py", ["test[a::b]"])
def test_does_not_exist(self, invocation_path: Path) -> None: def test_does_not_exist(self, invocation_path: Path) -> None:
"""Given a file/module that does not exist raises UsageError.""" """Given a file/module that does not exist raises UsageError."""
with pytest.raises( with pytest.raises(

View File

@ -1,3 +1,5 @@
import re
import warnings
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from typing import List from typing import List
@ -58,30 +60,31 @@ def test_subclassing_both_item_and_collector_deprecated(
request, tmp_path: Path request, tmp_path: Path
) -> None: ) -> None:
""" """
Verifies we warn on diamond inheritance Verifies we warn on diamond inheritance as well as correctly managing legacy
as well as correctly managing legacy inheritance ctors with missing args inheritance constructors with missing args as found in plugins.
as found in plugins
""" """
with pytest.warns( # We do not expect any warnings messages to issued during class definition.
PytestWarning, with warnings.catch_warnings():
match=( warnings.simplefilter("error")
"(?m)SoWrong is an Item subclass and should not be a collector, however its bases File are collectors.\n"
"Please split the Collectors and the Item into separate node types.\n.*"
),
):
class SoWrong(nodes.Item, nodes.File): class SoWrong(nodes.Item, nodes.File):
def __init__(self, fspath, parent): def __init__(self, fspath, parent):
"""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)
with pytest.warns( with pytest.warns(PytestWarning) as rec:
PytestWarning, match=".*SoWrong.* not using a cooperative constructor.*"
):
SoWrong.from_parent( SoWrong.from_parent(
request.session, fspath=legacy_path(tmp_path / "broken.txt") request.session, fspath=legacy_path(tmp_path / "broken.txt")
) )
messages = [str(x.message) for x in rec]
assert any(
re.search(".*SoWrong.* not using a cooperative constructor.*", x)
for x in messages
)
assert any(
re.search("(?m)SoWrong .* should not be a collector", x) for x in messages
)
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -345,7 +345,7 @@ def test_SkipTest_during_collection(pytester: Pytester) -> None:
""" """
) )
result = pytester.runpytest(p) result = pytester.runpytest(p)
result.assert_outcomes(skipped=1, warnings=1) result.assert_outcomes(skipped=1, warnings=0)
def test_SkipTest_in_test(pytester: Pytester) -> None: def test_SkipTest_in_test(pytester: Pytester) -> None:

View File

@ -562,15 +562,20 @@ class TestImportLibMode:
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar")) result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
assert result == "home.foo.test_foo" assert result == "home.foo.test_foo"
def test_insert_missing_modules(self) -> None: def test_insert_missing_modules(
modules = {"src.tests.foo": ModuleType("src.tests.foo")} self, monkeypatch: MonkeyPatch, tmp_path: Path
insert_missing_modules(modules, "src.tests.foo") ) -> None:
assert sorted(modules) == ["src", "src.tests", "src.tests.foo"] monkeypatch.chdir(tmp_path)
# Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and
# don't end up being imported.
modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")}
insert_missing_modules(modules, "xxx.tests.foo")
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
mod = ModuleType("mod", doc="My Module") mod = ModuleType("mod", doc="My Module")
modules = {"src": mod} modules = {"xxy": mod}
insert_missing_modules(modules, "src") insert_missing_modules(modules, "xxy")
assert modules == {"src": mod} assert modules == {"xxy": mod}
modules = {} modules = {}
insert_missing_modules(modules, "") insert_missing_modules(modules, "")

View File

@ -618,14 +618,9 @@ def test_linematcher_string_api() -> None:
def test_pytest_addopts_before_pytester(request, monkeypatch: MonkeyPatch) -> None: def test_pytest_addopts_before_pytester(request, monkeypatch: MonkeyPatch) -> None:
orig = os.environ.get("PYTEST_ADDOPTS", None)
monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused")
pytester: Pytester = request.getfixturevalue("pytester") _: Pytester = request.getfixturevalue("pytester")
assert "PYTEST_ADDOPTS" not in os.environ assert "PYTEST_ADDOPTS" not in os.environ
pytester._finalize()
assert os.environ.get("PYTEST_ADDOPTS") == "--orig-unused"
monkeypatch.undo()
assert os.environ.get("PYTEST_ADDOPTS") == orig
def test_run_stdin(pytester: Pytester) -> None: def test_run_stdin(pytester: Pytester) -> None:
@ -743,8 +738,8 @@ def test_run_result_repr() -> None:
# known exit code # known exit code
r = pytester_mod.RunResult(1, outlines, errlines, duration=0.5) r = pytester_mod.RunResult(1, outlines, errlines, duration=0.5)
assert ( assert repr(r) == (
repr(r) == "<RunResult ret=ExitCode.TESTS_FAILED len(stdout.lines)=3" f"<RunResult ret={str(pytest.ExitCode.TESTS_FAILED)} len(stdout.lines)=3"
" len(stderr.lines)=4 duration=0.50s>" " len(stderr.lines)=4 duration=0.50s>"
) )

View File

@ -81,7 +81,7 @@ def test_no_ini(pytester: Pytester, file_structure) -> None:
def test_clean_up(pytester: Pytester) -> None: def test_clean_up(pytester: Pytester) -> None:
"""Test that the pythonpath plugin cleans up after itself.""" """Test that the plugin cleans up after itself."""
# This is tough to test behaviorly because the cleanup really runs last. # This is tough to test behaviorly because the cleanup really runs last.
# So the test make several implementation assumptions: # So the test make several implementation assumptions:
# - Cleanup is done in pytest_unconfigure(). # - Cleanup is done in pytest_unconfigure().

View File

@ -1,4 +1,3 @@
import re
import warnings import warnings
from typing import Optional from typing import Optional
@ -263,7 +262,7 @@ class TestWarns:
with pytest.warns(RuntimeWarning): with pytest.warns(RuntimeWarning):
warnings.warn("user", UserWarning) warnings.warn("user", UserWarning)
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted. " r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted.\n"
r"The list of emitted warnings is: \[UserWarning\('user',?\)\]." r"The list of emitted warnings is: \[UserWarning\('user',?\)\]."
) )
@ -271,15 +270,15 @@ class TestWarns:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
warnings.warn("runtime", RuntimeWarning) warnings.warn("runtime", RuntimeWarning)
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted. " r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n"
r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)\]." r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)]."
) )
with pytest.raises(pytest.fail.Exception) as excinfo: with pytest.raises(pytest.fail.Exception) as excinfo:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
pass pass
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted. " r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n"
r"The list of emitted warnings is: \[\]." r"The list of emitted warnings is: \[\]."
) )
@ -289,18 +288,14 @@ class TestWarns:
warnings.warn("runtime", RuntimeWarning) warnings.warn("runtime", RuntimeWarning)
warnings.warn("import", ImportWarning) warnings.warn("import", ImportWarning)
message_template = ( messages = [each.message for each in warninfo]
"DID NOT WARN. No warnings of type {0} were emitted. " expected_str = (
"The list of emitted warnings is: {1}." f"DID NOT WARN. No warnings of type {warning_classes} were emitted.\n"
) f"The list of emitted warnings is: {messages}."
excinfo.match(
re.escape(
message_template.format(
warning_classes, [each.message for each in warninfo]
)
)
) )
assert str(excinfo.value) == expected_str
def test_record(self) -> None: def test_record(self) -> None:
with pytest.warns(UserWarning) as record: with pytest.warns(UserWarning) as record:
warnings.warn("user", UserWarning) warnings.warn("user", UserWarning)

View File

@ -783,6 +783,33 @@ class TestTerminalFunctional:
result.stdout.no_fnmatch_line("*= 1 deselected =*") result.stdout.no_fnmatch_line("*= 1 deselected =*")
assert result.ret == 0 assert result.ret == 0
def test_selected_count_with_error(self, pytester: Pytester) -> None:
pytester.makepyfile(
test_selected_count_3="""
def test_one():
pass
def test_two():
pass
def test_three():
pass
""",
test_selected_count_error="""
5/0
def test_foo():
pass
def test_bar():
pass
""",
)
result = pytester.runpytest("-k", "test_t")
result.stdout.fnmatch_lines(
[
"collected 3 items / 1 error / 1 deselected / 2 selected",
"* ERROR collecting test_selected_count_error.py *",
]
)
assert result.ret == ExitCode.INTERRUPTED
def test_no_skip_summary_if_failure(self, pytester: Pytester) -> None: def test_no_skip_summary_if_failure(self, pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(
""" """

View File

@ -1472,3 +1472,56 @@ def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None:
passed, skipped, failed = reprec.countoutcomes() passed, skipped, failed = reprec.countoutcomes()
assert failed == 2 assert failed == 2
assert passed == 1 assert passed == 1
def test_traceback_pruning(pytester: Pytester) -> None:
"""Regression test for #9610 - doesn't crash during traceback pruning."""
pytester.makepyfile(
"""
import unittest
class MyTestCase(unittest.TestCase):
def __init__(self, test_method):
unittest.TestCase.__init__(self, test_method)
class TestIt(MyTestCase):
@classmethod
def tearDownClass(cls) -> None:
assert False
def test_it(self):
pass
"""
)
reprec = pytester.inline_run()
passed, skipped, failed = reprec.countoutcomes()
assert passed == 1
assert failed == 1
assert reprec.ret == 1
def test_raising_unittest_skiptest_during_collection(
pytester: Pytester,
) -> None:
pytester.makepyfile(
"""
import unittest
class TestIt(unittest.TestCase):
def test_it(self): pass
def test_it2(self): pass
raise unittest.SkipTest()
class TestIt2(unittest.TestCase):
def test_it(self): pass
def test_it2(self): pass
"""
)
reprec = pytester.inline_run()
passed, skipped, failed = reprec.countoutcomes()
assert passed == 0
# Unittest reports one fake test for a skipped module.
assert skipped == 1
assert failed == 0
assert reprec.ret == ExitCode.NO_TESTS_COLLECTED

View File

@ -1,4 +1,5 @@
import os import os
import sys
import warnings import warnings
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -774,3 +775,57 @@ class TestStackLevel:
"*Unknown pytest.mark.unknown*", "*Unknown pytest.mark.unknown*",
] ]
) )
def test_resource_warning(pytester: Pytester, monkeypatch: pytest.MonkeyPatch) -> None:
# Some platforms (notably PyPy) don't have tracemalloc.
# We choose to explicitly not skip this in case tracemalloc is not
# available, using `importorskip("tracemalloc")` for example,
# because we want to ensure the same code path does not break in those platforms.
try:
import tracemalloc # noqa
has_tracemalloc = True
except ImportError:
has_tracemalloc = False
# Explicitly disable PYTHONTRACEMALLOC in case pytest's test suite is running
# with it enabled.
monkeypatch.delenv("PYTHONTRACEMALLOC", raising=False)
pytester.makepyfile(
"""
def open_file(p):
f = p.open("r")
assert p.read_text() == "hello"
def test_resource_warning(tmp_path):
p = tmp_path.joinpath("foo.txt")
p.write_text("hello")
open_file(p)
"""
)
result = pytester.run(sys.executable, "-Xdev", "-m", "pytest")
expected_extra = (
[
"*ResourceWarning* unclosed file*",
"*Enable tracemalloc to get traceback where the object was allocated*",
"*See https* for more info.",
]
if has_tracemalloc
else []
)
result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"])
monkeypatch.setenv("PYTHONTRACEMALLOC", "20")
result = pytester.run(sys.executable, "-Xdev", "-m", "pytest")
expected_extra = (
[
"*ResourceWarning* unclosed file*",
"*Object allocated at*",
]
if has_tracemalloc
else []
)
result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"])

View File

@ -8,6 +8,7 @@ envlist =
py38 py38
py39 py39
py310 py310
py311
pypy3 pypy3
py37-{pexpect,xdist,unittestextras,numpy,pluggymain} py37-{pexpect,xdist,unittestextras,numpy,pluggymain}
doctesting doctesting