Merge remote-tracking branch 'upstream/main' into tomllib

This commit is contained in:
hauntsaninja 2022-04-15 13:15:12 -07:00
commit a3745b2ed8
58 changed files with 794 additions and 426 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:
@ -187,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

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.1.0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
@ -10,7 +10,7 @@ repos:
- id: blacken-docs - id: blacken-docs
additional_dependencies: [black==20.8b1] additional_dependencies: [black==20.8b1]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0 rev: v4.2.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -37,17 +37,17 @@ 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.32.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.20.0 rev: v1.20.1
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
args: [--max-py-version=3.10] args: [--max-py-version=3.10]
@ -56,7 +56,7 @@ repos:
hooks: hooks:
- id: python-use-type-annotations - id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931 rev: v0.942
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)

View File

@ -185,6 +185,7 @@ 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 Kojo Idrissa
@ -289,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

@ -50,6 +50,8 @@ Fix bugs
-------- --------
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_. Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_
that are friendly to new contributors.
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going :ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
to work on a particular issue, add a comment to that effect on the specific issue. to work on a particular issue, add a comment to that effect on the specific issue.

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

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

View File

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

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 now avoids specialized assert formatting when it is detected that the default ``__eq__`` is overridden in ``attrs`` or ``dataclasses``.

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

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

View File

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

View File

@ -1 +0,0 @@
Fixed regression where ``--import-mode=importlib`` used together with :envvar:`PYTHONPATH` or :confval:`pythonpath` would cause import errors in test suites.

View File

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

View File

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

View File

@ -1 +0,0 @@
:fixture:`pytester` now requests a :fixture:`monkeypatch` fixture instead of creating one internally. This solves some issues with tests that involve pytest environment variables.

View File

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

View File

@ -1 +0,0 @@
Malformed ``pyproject.toml`` files now produce a clearer error message.

View File

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

View File

@ -0,0 +1 @@
Fix comparison of ``dataclasses`` with ``InitVar``.

View File

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

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,107 @@ 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) pytest 7.0.1 (2022-02-11)
========================= =========================

View File

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

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

@ -1,11 +1,16 @@
:orphan: :orphan:
.. sidebar:: Next Open Trainings
- `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 :doc:`previous talks and blogposts <talks>`.
.. ..
.. sidebar:: Next Open Trainings - `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
- `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.
Also see `previous talks and blogposts <talks.html>`_.
.. _features: .. _features:

File diff suppressed because it is too large Load Diff

View File

@ -5,3 +5,6 @@ sphinx-removed-in>=0.2.0
sphinx>=3.1,<4 sphinx>=3.1,<4
sphinxcontrib-trio sphinxcontrib-trio
sphinxcontrib-svg2pdfconverter sphinxcontrib-svg2pdfconverter
# XXX: sphinx<4 is broken with latest jinja2
jinja2<3.1

View File

@ -17,6 +17,8 @@ Books
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

@ -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.
@ -437,8 +437,10 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
if not has_default_eq(left): if not has_default_eq(left):
return [] return []
if isdatacls(left): if isdatacls(left):
all_fields = left.__dataclass_fields__ import dataclasses
fields_to_check = [field for field, info in all_fields.items() if info.compare]
all_fields = dataclasses.fields(left)
fields_to_check = [info.name for info in all_fields if info.compare]
elif isattrs(left): elif isattrs(left):
all_fields = left.__attrs_attrs__ all_fields = left.__attrs_attrs__
fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]

View File

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

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

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

@ -319,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 [
@ -340,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)
@ -573,7 +572,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6}) >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
True True
The comparision will be true if both mappings have the same keys and their The comparison will be true if both mappings have the same keys and their
respective values match the expected tolerances. respective values match the expected tolerances.
**Tolerances** **Tolerances**

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

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

@ -0,0 +1,12 @@
from dataclasses import dataclass
from dataclasses import InitVar
@dataclass
class Foo:
init_only: InitVar[int]
real_attr: int
def test_demonstrate():
assert Foo(1, 2) == Foo(1, 3)

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

@ -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==22.1.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]),

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

@ -882,6 +882,13 @@ class TestAssert_reprcompare_dataclass:
result.assert_outcomes(failed=1, passed=0) result.assert_outcomes(failed=1, passed=0)
result.stdout.no_re_match_line(".*Differing attributes.*") result.stdout.no_re_match_line(".*Differing attributes.*")
def test_data_classes_with_initvar(self, pytester: Pytester) -> None:
p = pytester.copy_example("dataclasses/test_compare_initvar.py")
# issue 9820
result = pytester.runpytest(p, "-vv")
result.assert_outcomes(failed=1, passed=0)
result.stdout.no_re_match_line(".*AttributeError.*")
class TestAssert_reprcompare_attrsclass: class TestAssert_reprcompare_attrsclass:
def test_attrs(self) -> None: def test_attrs(self) -> None:
@ -1695,3 +1702,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

@ -204,16 +204,8 @@ 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 sys == 42"
assert msg == (
"assert <module 'sys' (built-in)> == 42\n"
" +<module 'sys' (built-in)>\n"
" -42"
)
else:
assert msg == "assert sys == 42"
def f5() -> None: def f5() -> None:
assert cls == 42 # type: ignore[name-defined] # noqa: F821 assert cls == 42 # type: ignore[name-defined] # noqa: F821
@ -224,20 +216,7 @@ 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 == ["assert cls == 42"]
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"]
def test_assertrepr_compare_same_width(self, request) -> None: def test_assertrepr_compare_same_width(self, request) -> None:
"""Should use same width/truncation with same initial width.""" """Should use same width/truncation with same initial width."""
@ -279,14 +258,11 @@ 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 lines == ["assert 3 == 2", " +3", " -2"] "assert 3 == 2",
else: " + where 3 = Y.foo",
assert lines == [ " + where Y = cls()",
"assert 3 == 2", ]
" + where 3 = Y.foo",
" + where Y = cls()",
]
def test_assert_already_has_message(self) -> None: def test_assert_already_has_message(self) -> None:
def f(): def f():
@ -663,10 +639,7 @@ 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 + where 10 = len([0, 1, 2, 3, 4, 5, ...])"
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, ...])"
def test_custom_reprcompare(self, monkeypatch) -> None: def test_custom_reprcompare(self, monkeypatch) -> None:
def my_reprcompare1(op, left, right) -> str: def my_reprcompare1(op, left, right) -> str:
@ -732,10 +705,7 @@ 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 + where 1 = \\n{ \\n~ \\n}.a"]
assert lines == ["assert 0 == 1\n +0\n -1"]
else:
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:
def f() -> None: def f() -> None:
@ -1059,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

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,8 +1189,7 @@ 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,8 +1200,7 @@ 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*"])

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