Compare commits

..

55 Commits

Author SHA1 Message Date
pytest bot
31afeeb0df Prepare release version 8.0.2 2024-02-24 22:02:40 +00:00
Ran Benita
1b00a2f4fb Merge pull request #12025 from pytest-dev/backport-12022-to-8.0.x
[8.0.x] Revert "Fix teardown error reporting when `--maxfail=1` (#11721)"
2024-02-23 15:52:42 +02:00
Ran Benita
ff2f66d84a [8.0.x] Revert "Fix teardown error reporting when --maxfail=1 (#11721)" 2024-02-23 13:35:15 +00:00
github-actions[bot]
8a8eed609c [8.0.x] Fix collection of short paths on Windows (#12024)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-02-23 11:20:19 +00:00
github-actions[bot]
74346f027c [8.0.x] Allow Sphinx 7.x (#12005)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-02-18 10:43:49 +00:00
github-actions[bot]
b7657b4d6b [8.0.x] Disallow Sphinx 6 and 7 (#12001)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-02-17 21:02:41 +00:00
Ran Benita
feb7c5e12e Merge pull request #11999 from pytest-dev/backport-11996-to-8.0.x
[8.0.x] code: fix `IndexError` crash in `getstatementrange_ast`
2024-02-17 20:28:13 +02:00
Ran Benita
090965574e [8.0.x] code: fix IndexError crash in getstatementrange_ast 2024-02-17 16:59:38 +00:00
Ran Benita
68524d4858 Merge pull request #11993 from pytest-dev/release-8.0.1
Prepare release 8.0.1
2024-02-17 00:09:47 +02:00
pytest bot
d7d320a15a Prepare release version 8.0.1 2024-02-16 13:20:17 +00:00
Ran Benita
93699166dc Merge pull request #11992 from bluetech/backport-11991
[8.0.x] recwarn: fix pytest.warns handling of Warnings with multiple arguments
2024-02-16 14:55:28 +02:00
Ran Benita
a232abd56c [8.0.x] recwarn: fix pytest.warns handling of Warnings with multiple arguments
(cherry picked from commit fbe18fc7a9)
2024-02-16 14:34:01 +02:00
Ran Benita
92203d2b78 Merge pull request #11990 from bluetech/backport-11920
[8.0.x] recwarn: let base exceptions propagate through `pytest.warns` again
2024-02-16 12:52:17 +02:00
Ran Benita
f1aa9226ac [8.0.x] recwarn: let base exceptions propagate through pytest.warns again
(cherry picked from commit 718cd40015)
2024-02-16 12:31:51 +02:00
github-actions[bot]
d86d081563 [8.0.x] Added logot to the plugin list (#11977)
Co-authored-by: Dave Hall <dave@etianen.com>
2024-02-14 20:08:38 +01:00
Ran Benita
c554c3d200 Merge pull request #11968 from pytest-dev/backport-11957-to-8.0.x
[8.0.x] main: fix reversed collection order in Session
2024-02-13 10:01:00 +02:00
Ran Benita
a6851e3459 [8.0.x] main: fix reversed collection order in Session 2024-02-13 07:44:36 +00:00
github-actions[bot]
e6f6be3bc9 [8.0.x] Improve error message when using @pytest.fixture twice (#11958)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-02-09 14:34:45 +00:00
Ran Benita
23b91d12de [8.0.x] Merge pull request #11941 from bluetech/doctest-parsefactories (#11948)
doctest: fix autouse fixtures possibly not getting picked up
(cherry picked from commit 6c0b6c2f92)
2024-02-07 22:09:19 -03:00
github-actions[bot]
b188f4d10d [8.0.x] doc: Remove sold out training (#11927)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-02-05 08:04:32 +00:00
github-actions[bot]
d60b6b0e28 [8.0.x] doc: Update training dates and add pytest sprint (#11926)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-02-05 07:12:10 +00:00
Pierre Sassoulas
c11cdfabd1 Migrate from autoflake, black, isort, pyupgrade, flake8 and pydocstyle, to ruff (#11911)
ruff is faster and handle everything we had prior.

isort configuration done based on the indication from
https://github.com/astral-sh/ruff/issues/4670, previousely based on
reorder-python-import (#11896)

flake8-docstrings was a wrapper around pydocstyle (now archived) that
explicitly asks to use ruff in https://github.com/PyCQA/pydocstyle/pull/658.

flake8-typing-import is useful mainly for project that support python 3.7
and the one useful check will be implemented in https://github.com/astral-sh/ruff/issues/2302

We need to keep blacken-doc because ruff does not handle detection
of python code inside .md and .rst. The direct link to the repo is
now used to avoid a redirection.

Manual fixes:
- Lines that became too long
- % formatting that was not done automatically
- type: ignore that were moved around
- noqa of hard to fix issues (UP031 generally)
- fmt: off and fmt: on that is not really identical
  between black and ruff
- autofix re-order in pre-commit from faster to slower

Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-02-02 20:21:46 +01:00
Bruno Oliveira
368cc6225e [8.0.x] Upgrade blacken-doc to black's 2024 style (#11899) (#11900)
(cherry picked from commit 4546d5445a)

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2024-01-31 10:46:50 -03:00
Bruno Oliveira
06e592370e [8.0.x] Replace reorder-python-imports by isort due to black incompatibility (#11898)
Backport of #11896
2024-01-31 09:08:36 -03:00
github-actions[bot]
a76aa6ff80 [8.0.x] fix incorrect examples for group_contains (#11897)
Co-authored-by: John Litborn <11260241+jakkdl@users.noreply.github.com>
2024-01-30 20:37:18 +00:00
Ran Benita
73371a03da Merge pull request #11894 from pytest-dev/backport-11879-to-8.0.x
[8.0.x] BUG: fix an edge case where ExceptionInfo._stringify_exception could crash pytest.raises
2024-01-30 17:38:43 +02:00
Clément Robert
eb698a64a0 [8.0.x] BUG: fix an edge case where ExceptionInfo._stringify_exception could crash pytest.raises 2024-01-30 15:20:56 +00:00
github-actions[bot]
3e48ef64cd [8.0.x] Add a changelog entry about FixtureManager.getfixtureclosure losing a default argument (#11891)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-01-30 09:55:36 -03:00
github-actions[bot]
8b70bb64d3 [8.0.x] Catch OSError from getpass.getuser() (#11876)
Co-authored-by: Russell Martin <russell@rjm.li>
2024-01-29 02:25:18 +00:00
Ran Benita
169d775eec Merge pull request #11866 from pytest-dev/backport-11718-to-8.0.x
[8.0.x] build(deps): Bump hynek/build-and-inspect-python-package from 1.5.4 to 2.0.0
2024-01-28 00:25:45 +02:00
Ran Benita
42f709ed44 [8.0.x] build(deps): Bump hynek/build-and-inspect-python-package from 1.5.4 to 2.0.0 2024-01-27 22:10:59 +00:00
Ran Benita
24c681d4ee Merge pull request #11864 from bluetech/release-8.0.0
Prepare release version 8.0.0
2024-01-27 23:55:26 +02:00
Ran Benita
478f8233bc Prepare release version 8.0.0 2024-01-27 23:23:56 +02:00
github-actions[bot]
608590097a [8.0.x] fix: avoid rounding microsecond to 1_000_000 (#11863)
Co-authored-by: Dương Quốc Khánh <dqkqdlot@gmail.com>
2024-01-27 16:48:01 +00:00
github-actions[bot]
3b41c65c81 [8.0.x] Escape skip reason in junitxml (#11845)
Co-authored-by: clee2000 <44682903+clee2000@users.noreply.github.com>
2024-01-19 01:26:06 +00:00
github-actions[bot]
747072ad26 [8.0.x] Update docstring of scripts/generate-gh-release-notes.py (#11768)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-01-18 15:45:25 -03:00
Bruno Oliveira
011a475baf Properly attach packages to the GH release notes (#11839) (#11840)
Follow up to https://github.com/pytest-dev/pytest/pull/11754, noticed that the latest GitHub release does not contain the attached files.

Output log from the action:

```
🤔 Pattern 'dist/*' does not match any files.
```
2024-01-17 20:56:12 -03:00
Ran Benita
97960bdd14 Merge pull request #11835 from pytest-dev/release-8.0.0rc2
Prepare release version 8.0.0rc2
2024-01-17 23:44:01 +02:00
Ran Benita
6be0a3cbf7 Prepare release version 8.0.0rc2 2024-01-17 23:10:03 +02:00
Ran Benita
44ffe07165 Merge pull request #11837 from pytest-dev/backport-11836-to-8.0.x
[8.0.x] testing: temporarily disable test due to hypothesis issue
2024-01-17 23:09:47 +02:00
Ran Benita
14ecb04973 [8.0.x] testing: temporarily disable test due to hypothesis issue 2024-01-17 20:53:29 +00:00
Ran Benita
41c8dabee3 Merge pull request #11831 from bluetech/backport-11825-to-8.0.x
[8.0.x] avoid using __file__ in pytest_plugin_registered as can be wrong on Windows
2024-01-17 21:34:56 +02:00
Ran Benita
6f4cbd7cd4 [8.0.x] avoid using __file__ in pytest_plugin_registered as can be wrong on Windows 2024-01-17 15:40:14 +02:00
Ran Benita
b0c7f923aa Merge pull request #11813 from pytest-dev/backport-11795-to-8.0.x
[8.0.x] Improve assert mod not in mods error message
2024-01-14 13:55:56 +02:00
Ran Benita
f15aff06dc [8.0.x] Improve assert mod not in mods error message 2024-01-14 11:39:34 +00:00
Ran Benita
10c8898845 Merge pull request #11810 from pytest-dev/backport-11708-to-8.0.x
[8.0.x] FIX key formating divergence when inspecting plugin dictionary.
2024-01-13 23:02:20 +02:00
Ran Benita
5b7ddedbf9 [8.0.x] FIX key formating divergence when inspecting plugin dictionary. 2024-01-13 19:46:07 +00:00
github-actions[bot]
3ae38baca6 [8.0.x] Add summary for xfails with -rxX option (#11778)
Co-authored-by: Fabian Sturm <fabio.sturm@gmail.com>
2024-01-05 22:35:47 +00:00
Ran Benita
6123b247d4 Merge pull request #11764 from pytest-dev/backport-11721-to-8.0.x
[8.0.x] Fix teardown error reporting when `--maxfail=1`
2024-01-04 11:23:03 +02:00
Bruno Oliveira
bb6f5d1b10 Merge pull request #11766 from pytest-dev/backport-11754-to-8.0.x
[8.0.x] Improve GitHub release workflow
2024-01-03 20:33:02 -03:00
Bruno Oliveira
72eb1b7ad1 [8.0.x] Improve GitHub release workflow 2024-01-03 23:15:18 +00:00
Ben Brown
620a454dba [8.0.x] Fix teardown error reporting when --maxfail=1 2024-01-03 17:39:50 +00:00
github-actions[bot]
838151638e [8.0.x] terminalwriter: fix crash trying to highlight empty source (#11763)
Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-01-03 08:04:57 -03:00
Ran Benita
665e4e58d3 Merge pull request #11744 from pytest-dev/release-8.0.0rc1
Prepare release 8.0.0rc1
2024-01-02 10:58:20 +02:00
Ran Benita
e17d5ec871 Prepare release version 8.0.0rc1 2023-12-31 14:40:46 +02:00
189 changed files with 1906 additions and 1265 deletions

View File

@@ -26,3 +26,6 @@ afc607cfd81458d4e4f3b1f3cf8cc931b933907e
# move argument parser to own file
c9df77cbd6a365dcb73c39618e4842711817e871
# Replace reorder-python-imports by isort due to black incompatibility (#11896)
8b54596639f41dfac070030ef20394b9001fe63c

View File

@@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5.4
uses: hynek/build-and-inspect-python-package@v2.0.0
deploy:
if: github.repository == 'pytest-dev/pytest'
@@ -41,7 +41,7 @@ jobs:
- uses: actions/checkout@v4
- name: Download Package
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
@@ -72,6 +72,12 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Download Package
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -82,9 +88,14 @@ jobs:
python -m pip install --upgrade pip
pip install --upgrade tox
- name: Publish GitHub release notes
env:
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}
- name: Generate release notes
run: |
sudo apt-get install pandoc
tox -e publish-gh-release-notes
tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md
- name: Publish GitHub Release
uses: softprops/action-gh-release@v1
with:
body_path: scripts/latest-release-notes.md
files: dist/*
tag_name: ${{ github.event.inputs.version }}

View File

@@ -35,7 +35,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5.4
uses: hynek/build-and-inspect-python-package@v2.0.0
build:
needs: [package]
@@ -173,7 +173,7 @@ jobs:
persist-credentials: false
- name: Download Package
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: Packages
path: dist

View File

@@ -1,14 +1,10 @@
repos:
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: 1.16.0
hooks:
- id: blacken-docs
additional_dependencies: [black==23.7.0]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.1.15"
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
@@ -20,32 +16,11 @@ repos:
- id: debug-statements
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/PyCQA/autoflake
rev: v2.2.1
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.16.0
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
rev: 6.1.0
hooks:
- id: flake8
language_version: python3
additional_dependencies:
- flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.12.0
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py38-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- id: blacken-docs
additional_dependencies: [black==24.1.1]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0
hooks:
@@ -59,14 +34,16 @@ repos:
rev: v1.8.0
hooks:
- id: mypy
files: ^(src/|testing/)
files: ^(src/|testing/|scripts/)
args: []
additional_dependencies:
- iniconfig>=1.1.0
- attrs>=19.2.0
- pluggy
- packaging
- tomli
- types-pkg_resources
- types-tabulate
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
# on <3.11
- exceptiongroup>=1.0.0rc8

View File

@@ -54,6 +54,7 @@ Aviral Verma
Aviv Palivoda
Babak Keyvani
Barney Gale
Ben Brown
Ben Gartner
Ben Webb
Benjamin Peterson
@@ -92,6 +93,7 @@ Christopher Dignam
Christopher Gilling
Claire Cecil
Claudio Madotto
Clément M.T. Robert
CrazyMerlyn
Cristian Vera
Cyrus Maden
@@ -137,6 +139,7 @@ Erik Hasse
Erik M. Bray
Evan Kepner
Evgeny Seliverstov
Fabian Sturm
Fabien Zarifian
Fabio Zadrozny
Felix Hofstätter
@@ -336,6 +339,7 @@ Ronny Pfannschmidt
Ross Lawley
Ruaridh Williamson
Russel Winder
Russell Martin
Ryan Puddephatt
Ryan Wooden
Sadra Barikbin

View File

@@ -27,9 +27,6 @@
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main
:alt: pre-commit.ci status
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
:target: https://www.codetriage.com/pytest-dev/pytest

View File

@@ -1,10 +1,12 @@
import sys
if __name__ == "__main__":
import cProfile
import pytest # NOQA
import pstats
import pytest # NOQA
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
p = pstats.Stats("prof")

View File

@@ -4,6 +4,7 @@
# FastFilesCompleter 0.7383 1.0760
import timeit
imports = [
"from argcomplete.completers import FilesCompleter as completer",
"from _pytest._argcomplete import FastFilesCompleter as completer",

View File

@@ -1,5 +1,6 @@
import pytest
SKIP = True

View File

@@ -1,5 +1,6 @@
from unittest import TestCase # noqa: F401
for i in range(15000):
exec(
f"""

View File

@@ -5,11 +5,10 @@
<div id="searchbox" style="display: none" role="search">
<div class="searchformwrapper">
<form class="search" action="{{ pathto('search') }}" method="get">
<input type="text" name="q" aria-labelledby="searchlabel"
placeholder="Search"/>
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="{{ _('Go') }}" />
</form>
</div>
</div>
<script type="text/javascript">$('#searchbox').show(0);</script>
<script>document.getElementById('searchbox').style.display = "block"</script>
{%- endif %}

View File

@@ -6,6 +6,10 @@ Release announcements
:maxdepth: 2
release-8.0.2
release-8.0.1
release-8.0.0
release-8.0.0rc2
release-8.0.0rc1
release-7.4.4
release-7.4.3

View File

@@ -0,0 +1,26 @@
pytest-8.0.0
=======================================
The pytest team is proud to announce the 8.0.0 release!
This release contains new features, improvements, bug fixes, and breaking changes, so users
are encouraged to take a look at the CHANGELOG carefully:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,32 @@
pytest-8.0.0rc2
=======================================
The pytest team is proud to announce the 8.0.0rc2 prerelease!
This is a prerelease, not intended for production use, but to test the upcoming features and improvements
in order to catch any major problems before the final version is released to the major public.
We appreciate your help testing this out before the final release, making sure to report any
regressions to our issue tracker:
https://github.com/pytest-dev/pytest/issues
When doing so, please include the string ``[prerelease]`` in the title.
You can upgrade from PyPI via:
pip install pytest==8.0.0rc2
Users are encouraged to take a look at the CHANGELOG carefully:
https://docs.pytest.org/en/release-8.0.0rc2/changelog.html
Thanks to all the contributors to this release:
* Ben Brown
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,21 @@
pytest-8.0.1
=======================================
pytest 8.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:
* Bruno Oliveira
* Clément Robert
* Pierre Sassoulas
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,18 @@
pytest-8.0.2
=======================================
pytest 8.0.2 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

@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 0 items
cache -- .../_pytest/cacheprovider.py:526
cache -- .../_pytest/cacheprovider.py:527
Return a cache object that can persist state between testing sessions.
cache.get(key, default)
@@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Values can be any object handled by the json stdlib module.
capsysbinary -- .../_pytest/capture.py:1008
capsysbinary -- .../_pytest/capture.py:1007
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()``
@@ -43,7 +43,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsysbinary):
@@ -51,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsysbinary.readouterr()
assert captured.out == b"hello\n"
capfd -- .../_pytest/capture.py:1036
capfd -- .../_pytest/capture.py:1034
Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -61,7 +60,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfd):
@@ -69,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfd.readouterr()
assert captured.out == "hello\n"
capfdbinary -- .../_pytest/capture.py:1064
capfdbinary -- .../_pytest/capture.py:1061
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -79,7 +77,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfdbinary):
@@ -97,7 +94,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
@@ -105,7 +101,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsys.readouterr()
assert captured.out == "hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:743
doctest_namespace [session scope] -- .../_pytest/doctest.py:745
Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
@@ -119,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1365
pytestconfig [session scope] -- .../_pytest/fixtures.py:1354
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@@ -129,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
if pytestconfig.getoption("verbose") > 0:
...
record_property -- .../_pytest/junitxml.py:282
record_property -- .../_pytest/junitxml.py:283
Add extra properties to the calling test.
User properties become part of the test report and are available to the
@@ -143,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
def test_function(record_property):
record_property("example_key", 1)
record_xml_attribute -- .../_pytest/junitxml.py:305
record_xml_attribute -- .../_pytest/junitxml.py:306
Add extra xml attributes to the tag for the calling test.
The fixture is callable with ``name, value``. The value is
automatically XML-encoded.
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:343
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
This is suitable to writing global information regarding the entire test
@@ -174,10 +170,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
:issue:`7767` for details.
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:300
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
Return a :class:`pytest.TempdirFactory` instance for the test session.
tmpdir -- .../_pytest/legacypath.py:307
tmpdir -- .../_pytest/legacypath.py:309
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
@@ -196,7 +192,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
caplog -- .../_pytest/logging.py:593
caplog -- .../_pytest/logging.py:594
Access and control log capturing.
Captured logs are available through the following properties/methods::
@@ -207,7 +203,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
* caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string
monkeypatch -- .../_pytest/monkeypatch.py:30
monkeypatch -- .../_pytest/monkeypatch.py:32
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
@@ -231,16 +227,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
To undo modifications done by the fixture in a contained scope,
use :meth:`context() <pytest.MonkeyPatch.context>`.
recwarn -- .../_pytest/recwarn.py:30
recwarn -- .../_pytest/recwarn.py:32
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
on warning categories.
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:239
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241
Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:254
tmp_path -- .../_pytest/tmpdir.py:256
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.

View File

@@ -28,6 +28,90 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 8.0.2 (2024-02-24)
=========================
Bug Fixes
---------
- `#11895 <https://github.com/pytest-dev/pytest/issues/11895>`_: Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``).
- `#11953 <https://github.com/pytest-dev/pytest/issues/11953>`_: Fix an ``IndexError`` crash raising from ``getstatementrange_ast``.
- `#12021 <https://github.com/pytest-dev/pytest/issues/12021>`_: Reverted a fix to `--maxfail` handling in pytest 8.0.0 because it caused a regression in pytest-xdist whereby session fixture teardowns may get executed multiple times when the max-fails is reached.
pytest 8.0.1 (2024-02-16)
=========================
Bug Fixes
---------
- `#11875 <https://github.com/pytest-dev/pytest/issues/11875>`_: Correctly handle errors from :func:`getpass.getuser` in Python 3.13.
- `#11879 <https://github.com/pytest-dev/pytest/issues/11879>`_: Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`.
- `#11906 <https://github.com/pytest-dev/pytest/issues/11906>`_: Fix regression with :func:`pytest.warns` using custom warning subclasses which have more than one parameter in their `__init__`.
- `#11907 <https://github.com/pytest-dev/pytest/issues/11907>`_: Fix a regression in pytest 8.0.0 whereby calling :func:`pytest.skip` and similar control-flow exceptions within a :func:`pytest.warns()` block would get suppressed instead of propagating.
- `#11929 <https://github.com/pytest-dev/pytest/issues/11929>`_: Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module.
- `#11937 <https://github.com/pytest-dev/pytest/issues/11937>`_: Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances.
pytest 8.0.0 (2024-01-27)
=========================
Bug Fixes
---------
- `#11842 <https://github.com/pytest-dev/pytest/issues/11842>`_: Properly escape the ``reason`` of a :ref:`skip <pytest.mark.skip ref>` mark when writing JUnit XML files.
- `#11861 <https://github.com/pytest-dev/pytest/issues/11861>`_: Avoid microsecond exceeds ``1_000_000`` when using ``log-date-format`` with ``%f`` specifier, which might cause the test suite to crash.
pytest 8.0.0rc2 (2024-01-17)
============================
Improvements
------------
- `#11233 <https://github.com/pytest-dev/pytest/issues/11233>`_: Improvements to ``-r`` for xfailures and xpasses:
* Report tracebacks for xfailures when ``-rx`` is set.
* Report captured output for xpasses when ``-rX`` is set.
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.
- `#11825 <https://github.com/pytest-dev/pytest/issues/11825>`_: The :hook:`pytest_plugin_registered` hook has a new ``plugin_name`` parameter containing the name by which ``plugin`` is registered.
Bug Fixes
---------
- `#11706 <https://github.com/pytest-dev/pytest/issues/11706>`_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
NOTE: This change was reverted in pytest 8.0.2 to fix a `regression <https://github.com/pytest-dev/pytest-xdist/issues/1024>`_ it caused in pytest-xdist.
- `#11758 <https://github.com/pytest-dev/pytest/issues/11758>`_: Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
This bug was introduced in pytest 8.0.0rc1.
- `#9765 <https://github.com/pytest-dev/pytest/issues/9765>`_, `#11816 <https://github.com/pytest-dev/pytest/issues/11816>`_: Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangably in some places in the code.
This fix also broke the internal API of ``PytestPluginManager.consider_conftest`` by introducing a new parameter -- we mention this in case it is being used by external code, even if marked as *private*.
pytest 8.0.0rc1 (2023-12-30)
============================
@@ -223,6 +307,10 @@ These are breaking changes where deprecation was not possible.
therefore fail on the newly-re-emitted warnings.
- The internal ``FixtureManager.getfixtureclosure`` method has changed. Plugins which use this method or
which subclass ``FixtureManager`` and overwrite that method will need to adapt to the change.
Deprecations
------------

View File

@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
from _pytest import __version__ as version
if TYPE_CHECKING:
import sphinx.application
@@ -441,9 +442,10 @@ intersphinx_mapping = {
def configure_logging(app: "sphinx.application.Sphinx") -> None:
"""Configure Sphinx's WarningHandler to handle (expected) missing include."""
import sphinx.util.logging
import logging
import sphinx.util.logging
class WarnLogFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
"""Ignore warnings about missing include with "only" directive.

View File

@@ -47,11 +47,9 @@ they are in fact part of the ``nose`` support.
def teardown(self):
self.resource.close()
def test_foo(self):
...
def test_foo(self): ...
def test_bar(self):
...
def test_bar(self): ...
@@ -66,11 +64,9 @@ Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`x
def teardown_method(self):
self.resource.close()
def test_foo(self):
...
def test_foo(self): ...
def test_bar(self):
...
def test_bar(self): ...
This is easy to do in an entire code base by doing a simple find/replace.
@@ -85,17 +81,14 @@ Code using `@with_setup <with-setup-nose>`_ such as this:
from nose.tools import with_setup
def setup_some_resource():
...
def setup_some_resource(): ...
def teardown_some_resource():
...
def teardown_some_resource(): ...
@with_setup(setup_some_resource, teardown_some_resource)
def test_foo():
...
def test_foo(): ...
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
@@ -104,12 +97,10 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
import pytest
def setup_some_resource():
...
def setup_some_resource(): ...
def teardown_some_resource():
...
def teardown_some_resource(): ...
@pytest.fixture
@@ -119,8 +110,7 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
teardown_some_resource()
def test_foo(some_resource):
...
def test_foo(some_resource): ...
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
@@ -197,13 +187,11 @@ have been available since years and should be used instead.
.. code-block:: python
@pytest.mark.tryfirst
def pytest_runtest_call():
...
def pytest_runtest_call(): ...
# or
def pytest_runtest_call():
...
def pytest_runtest_call(): ...
pytest_runtest_call.tryfirst = True
@@ -213,8 +201,7 @@ should be changed to:
.. code-block:: python
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_call():
...
def pytest_runtest_call(): ...
Changed ``hookimpl`` attributes:
@@ -317,8 +304,7 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
.. code-block:: python
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None:
...
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: ...
# becomes:
@@ -326,8 +312,7 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
def pytest_load_initial_conftests(
early_config: Config, parser: Parser, args: List[str]
) -> None:
...
) -> None: ...
.. _diamond-inheritance-deprecated:
@@ -391,8 +376,7 @@ Applying a mark to a fixture function never had any effect, but it is a common u
@pytest.mark.usefixtures("clean_database")
@pytest.fixture
def user() -> User:
...
def user() -> User: ...
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
@@ -907,8 +891,7 @@ Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated
(50, 500),
],
)
def test_foo(a, b):
...
def test_foo(a, b): ...
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
call.
@@ -931,8 +914,7 @@ To update the code, use ``pytest.param``:
(50, 500),
],
)
def test_foo(a, b):
...
def test_foo(a, b): ...
.. _pytest_funcarg__ prefix deprecated:
@@ -1083,15 +1065,13 @@ This is just a matter of renaming the fixture as the API is the same:
.. code-block:: python
def test_foo(record_xml_property):
...
def test_foo(record_xml_property): ...
Change to:
.. code-block:: python
def test_foo(record_property):
...
def test_foo(record_property): ...
.. _passing command-line string to pytest.main deprecated:
@@ -1253,8 +1233,7 @@ Example of usage:
.. code-block:: python
class MySymbol:
...
class MySymbol: ...
def pytest_namespace():

View File

@@ -2,6 +2,7 @@ import os.path
import pytest
mydir = os.path.dirname(__file__)

View File

@@ -1,6 +1,7 @@
import os.path
import shutil
failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py")
pytest_plugins = ("pytester",)

View File

@@ -1,5 +1,6 @@
"""Module containing a parametrized tests testing cross-python serialization
via the pickle module."""
import shutil
import subprocess
import textwrap
@@ -32,14 +33,12 @@ class Python:
dumpfile = self.picklefile.with_name("dump.py")
dumpfile.write_text(
textwrap.dedent(
r"""
rf"""
import pickle
f = open({!r}, 'wb')
s = pickle.dump({!r}, f, protocol=2)
f = open({str(self.picklefile)!r}, 'wb')
s = pickle.dump({obj!r}, f, protocol=2)
f.close()
""".format(
str(self.picklefile), obj
)
"""
)
)
subprocess.run((self.pythonpath, str(dumpfile)), check=True)
@@ -48,17 +47,15 @@ class Python:
loadfile = self.picklefile.with_name("load.py")
loadfile.write_text(
textwrap.dedent(
r"""
rf"""
import pickle
f = open({!r}, 'rb')
f = open({str(self.picklefile)!r}, 'rb')
obj = pickle.load(f)
f.close()
res = eval({!r})
res = eval({expression!r})
if not res:
raise SystemExit(1)
""".format(
str(self.picklefile), expression
)
"""
)
)
print(loadfile)

View File

@@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
rootdir: /home/sweet/project
collected 8 items
<Dir parametrize.rst-189>
<Dir parametrize.rst-194>
<Module test_time.py>
<Function test_timedistance_v0[a0-b0-expected0]>
<Function test_timedistance_v0[a1-b1-expected1]>
@@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
rootdir: /home/sweet/project
collected 4 items
<Dir parametrize.rst-189>
<Dir parametrize.rst-194>
<Module test_scenarios.py>
<Class TestSampleWithScenarios>
<Function test_demo1[basic]>
@@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
rootdir: /home/sweet/project
collected 2 items
<Dir parametrize.rst-189>
<Dir parametrize.rst-194>
<Module test_backends.py>
<Function test_db_initialized[d1]>
<Function test_db_initialized[d2]>
@@ -505,8 +505,8 @@ Running it results in some skips if we don't have all the python interpreters in
. $ pytest -rs -q multipython.py
ssssssssssss...ssssssssssss [100%]
========================= short test summary info ==========================
SKIPPED [12] multipython.py:68: 'python3.9' not found
SKIPPED [12] multipython.py:68: 'python3.11' not found
SKIPPED [12] multipython.py:65: 'python3.9' not found
SKIPPED [12] multipython.py:65: 'python3.11' not found
3 passed, 24 skipped in 0.12s
Parametrization of optional implementations/imports

View File

@@ -152,7 +152,7 @@ The test collection would look like this:
configfile: pytest.ini
collected 2 items
<Dir pythoncollection.rst-190>
<Dir pythoncollection.rst-195>
<Module check_myapp.py>
<Class CheckMyApp>
<Function simple_check>
@@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
configfile: pytest.ini
collected 3 items
<Dir pythoncollection.rst-190>
<Dir pythoncollection.rst-195>
<Dir CWD>
<Module pythoncollection.py>
<Function test_function>

View File

@@ -660,6 +660,31 @@ If we run this:
E assert 0
test_step.py:11: AssertionError
================================ XFAILURES =================================
______________________ TestUserHandling.test_deletion ______________________
item = <Function test_deletion>
def pytest_runtest_setup(item):
if "incremental" in item.keywords:
# retrieve the class name of the test
cls_name = str(item.cls)
# check if a previous test has failed for this class
if cls_name in _test_failed_incremental:
# retrieve the index of the test (if parametrize is used in combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the first test function to fail for this class name and index
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
# if name found, test has failed for the combination of class name & test name
if test_name is not None:
> pytest.xfail(f"previous test failed ({test_name})")
E _pytest.outcomes.XFailed: previous test failed (test_modification)
conftest.py:47: XFailed
========================= short test summary info ==========================
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================

View File

@@ -1,5 +1,6 @@
import pytest
xfail = pytest.mark.xfail

View File

@@ -99,8 +99,7 @@ sets. pytest-2.3 introduces a decorator for use on the factory itself:
.. code-block:: python
@pytest.fixture(params=["mysql", "pg"])
def db(request):
... # use request.param
def db(request): ... # use request.param
Here the factory will be invoked twice (with the respective "mysql"
and "pg" values set as ``request.param`` attributes) and all of
@@ -141,8 +140,7 @@ argument:
.. code-block:: python
@pytest.fixture()
def db(request):
...
def db(request): ...
The name under which the funcarg resource can be requested is ``db``.
@@ -151,8 +149,7 @@ aka:
.. code-block:: python
def pytest_funcarg__db(request):
...
def pytest_funcarg__db(request): ...
But it is then not possible to define scoping and parametrization.

View File

@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 8.0.0rc1
pytest 8.0.2
.. _`simpletest`:

View File

@@ -227,8 +227,7 @@ to use strings:
@pytest.mark.skipif("sys.version_info >= (3,3)")
def test_function():
...
def test_function(): ...
During test function setup the skipif condition is evaluated by calling
``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains
@@ -262,8 +261,7 @@ configuration value which you might have added:
.. code-block:: python
@pytest.mark.skipif("not config.getvalue('db')")
def test_function():
...
def test_function(): ...
The equivalent with "boolean conditions" is:

View File

@@ -154,7 +154,7 @@ method to test for exceptions returned as part of an :class:`ExceptionGroup`:
.. code-block:: python
def test_exception_in_group():
with pytest.raises(RuntimeError) as excinfo:
with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup(
"Group message",
[
@@ -176,7 +176,7 @@ exception at a specific level; exceptions contained directly in the top
.. code-block:: python
def test_exception_in_group_at_given_depth():
with pytest.raises(RuntimeError) as excinfo:
with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup(
"Group message",
[

View File

@@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
rootdir: /home/sweet/project
collected 12 items
<Dir fixtures.rst-208>
<Dir fixtures.rst-213>
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>
@@ -1721,8 +1721,7 @@ You can specify multiple fixtures like this:
.. code-block:: python
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
...
def test(): ...
and you may specify fixture usage at the test module level using :globalvar:`pytestmark`:
@@ -1750,8 +1749,7 @@ into an ini-file:
@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture():
...
def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
This generates a deprecation warning, and will become an error in Pytest 8.

View File

@@ -404,10 +404,19 @@ Example:
E assert 0
test_example.py:14: AssertionError
================================ XFAILURES =================================
________________________________ test_xfail ________________________________
def test_xfail():
> pytest.xfail("xfailing this test")
E _pytest.outcomes.XFailed: xfailing this test
test_example.py:26: XFailed
================================= XPASSES ==================================
========================= short test summary info ==========================
SKIPPED [1] test_example.py:22: skipping this test
XFAIL test_example.py::test_xfail - reason: xfailing this test
XPASS test_example.py::test_xpass always xfail
XPASS test_example.py::test_xpass - always xfail
ERROR test_example.py::test_error - assert 0
FAILED test_example.py::test_fail - assert 0
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===

View File

@@ -47,8 +47,7 @@ which may be passed an optional ``reason``:
.. code-block:: python
@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown():
...
def test_the_unknown(): ...
Alternatively, it is also possible to skip imperatively during test execution or setup
@@ -93,8 +92,7 @@ when run on an interpreter earlier than Python3.10:
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
def test_function():
...
def test_function(): ...
If the condition evaluates to ``True`` during collection, the test function will be skipped,
with the specified reason appearing in the summary when using ``-rs``.
@@ -112,8 +110,7 @@ You can share ``skipif`` markers between modules. Consider this test module:
@minversion
def test_function():
...
def test_function(): ...
You can import the marker and reuse it in another test module:
@@ -124,8 +121,7 @@ You can import the marker and reuse it in another test module:
@minversion
def test_anotherfunction():
...
def test_anotherfunction(): ...
For larger test suites it's usually a good idea to have one file
where you define the markers which you then consistently apply
@@ -232,8 +228,7 @@ expect a test to fail:
.. code-block:: python
@pytest.mark.xfail
def test_function():
...
def test_function(): ...
This test will run but no traceback will be reported when it fails. Instead, terminal
reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly
@@ -275,8 +270,7 @@ that condition as the first parameter:
.. code-block:: python
@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
def test_function():
...
def test_function(): ...
Note that you have to pass a reason as well (see the parameter description at
:ref:`pytest.mark.xfail ref`).
@@ -289,8 +283,7 @@ You can specify the motive of an expected failure with the ``reason`` parameter:
.. code-block:: python
@pytest.mark.xfail(reason="known parser issue")
def test_function():
...
def test_function(): ...
``raises`` parameter
@@ -302,8 +295,7 @@ a single exception, or a tuple of exceptions, in the ``raises`` argument.
.. code-block:: python
@pytest.mark.xfail(raises=RuntimeError)
def test_function():
...
def test_function(): ...
Then the test will be reported as a regular failure if it fails with an
exception not mentioned in ``raises``.
@@ -317,8 +309,7 @@ even executed, use the ``run`` parameter as ``False``:
.. code-block:: python
@pytest.mark.xfail(run=False)
def test_function():
...
def test_function(): ...
This is specially useful for xfailing tests that are crashing the interpreter and should be
investigated later.
@@ -334,8 +325,7 @@ You can change this by setting the ``strict`` keyword-only parameter to ``True``
.. code-block:: python
@pytest.mark.xfail(strict=True)
def test_function():
...
def test_function(): ...
This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite.

View File

@@ -1,8 +1,11 @@
:orphan:
.. sidebar:: Next Open Trainings
.. sidebar:: Next Open Trainings and Events
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
* **June 11th to 13th 2024**, Remote
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
Also see :doc:`previous talks and blogposts <talks>`.

View File

@@ -164,8 +164,7 @@ Add warning filters to marked test items.
.. code-block:: python
@pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
def test_foo():
...
def test_foo(): ...
.. _`pytest.mark.parametrize ref`:
@@ -276,8 +275,7 @@ For example:
.. code-block:: python
@pytest.mark.timeout(10, "slow", method="thread")
def test_function():
...
def test_function(): ...
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
@@ -294,8 +292,7 @@ Example for using multiple custom markers:
@pytest.mark.timeout(10, "slow", method="thread")
@pytest.mark.slow
def test_function():
...
def test_function(): ...
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
@@ -2036,7 +2033,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
failure
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
--doctest-ignore-import-errors
Ignore doctest ImportErrors
Ignore doctest collection errors
--doctest-continue-on-failure
For a given doctest, continue to run after the first
failure

View File

@@ -2,7 +2,7 @@ pallets-sphinx-themes
pluggy>=1.2.0
pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0
sphinx>=5,<8
sphinx>=7
sphinxcontrib-trio
sphinxcontrib-svg2pdfconverter
# Pin packaging because it no longer handles 'latest' version, which

View File

@@ -3,6 +3,7 @@ from pathlib import Path
import requests
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"

View File

@@ -123,3 +123,58 @@ target-version = ['py38']
[tool.check-wheel-contents]
# W009: Wheel contains multiple toplevel library entries
ignore = "W009"
[tool.ruff]
src = ["src"]
line-length = 88
select = [
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
"W", # pycodestyle
]
ignore = [
# pycodestyle ignore
# pytest can do weird low-level things, and we usually know
# what we're doing when we use type(..) is ...
"E721", # Do not compare types, use `isinstance()`
# pydocstyle ignore
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D103", # Missing docstring in public function
"D104", # Missing docstring in public package
"D105", # Missing docstring in magic method
"D106", # Missing docstring in public nested class
"D107", # Missing docstring in `__init__`
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
"D205", # 1 blank line required between summary line and description
"D400", # First line should end with a period
"D401", # First line of docstring should be in imperative mood
"D402", # First line should not be the function's signature
"D404", # First word of the docstring should not be "This"
"D415", # First line should end with a period, question mark, or exclamation point
# Temp for backport 8.0.x
"E501",
"UP031",
]
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint.pycodestyle]
# In order to be able to format for 88 char in ruff format
max-line-length = 120
[tool.ruff.lint.pydocstyle]
convention = "pep257"
[tool.ruff.lint.isort]
force-single-line = true
combine-as-imports = true
force-sort-within-sections = true
order-by-type = false
known-local-folder = ["pytest", "_pytest"]
lines-after-imports = 2

1
scripts/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
latest-release-notes.md

View File

@@ -0,0 +1,66 @@
# mypy: disallow-untyped-defs
"""
Script used to generate a Markdown file containing only the changelog entries of a specific pytest release, which
is then published as a GitHub Release during deploy (see workflows/deploy.yml).
The script requires ``pandoc`` to be previously installed in the system -- we need to convert from RST (the format of
our CHANGELOG) into Markdown (which is required by GitHub Releases).
Requires Python3.6+.
"""
from pathlib import Path
import re
import sys
from typing import Sequence
import pypandoc
def extract_changelog_entries_for(version: str) -> str:
p = Path(__file__).parent.parent / "doc/en/changelog.rst"
changelog_lines = p.read_text(encoding="UTF-8").splitlines()
title_regex = re.compile(r"pytest (\d\.\d+\.\d+\w*) \(\d{4}-\d{2}-\d{2}\)")
consuming_version = False
version_lines = []
for line in changelog_lines:
m = title_regex.match(line)
if m:
# Found the version we want: start to consume lines until we find the next version title.
if m.group(1) == version:
consuming_version = True
# Found a new version title while parsing the version we want: break out.
elif consuming_version:
break
if consuming_version:
version_lines.append(line)
return "\n".join(version_lines)
def convert_rst_to_md(text: str) -> str:
result = pypandoc.convert_text(
text, "md", format="rst", extra_args=["--wrap=preserve"]
)
assert isinstance(result, str), repr(result)
return result
def main(argv: Sequence[str]) -> int:
if len(argv) != 3:
print("Usage: generate-gh-release-notes VERSION FILE")
return 2
version, filename = argv[1:3]
print(f"Generating GitHub release notes for version {version}")
rst_body = extract_changelog_entries_for(version)
md_body = convert_rst_to_md(rst_body)
Path(filename).write_text(md_body, encoding="UTF-8")
print()
print(f"Done: {filename}")
print()
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

View File

@@ -1,3 +1,4 @@
# mypy: disallow-untyped-defs
"""
This script is part of the pytest release process which is triggered manually in the Actions
tab of the repository.
@@ -13,8 +14,8 @@ After that, it will create a release using the `release` tox environment, and pu
`pytest bot <pytestbot@gmail.com>` commit author.
"""
import argparse
import re
from pathlib import Path
import re
from subprocess import check_call
from subprocess import check_output
from subprocess import run

View File

@@ -1,102 +0,0 @@
"""
Script used to publish GitHub release notes extracted from CHANGELOG.rst.
This script is meant to be executed after a successful deployment in GitHub actions.
Uses the following environment variables:
* GIT_TAG: the name of the tag of the current commit.
* GH_RELEASE_NOTES_TOKEN: a personal access token with 'repo' permissions.
Create one at:
https://github.com/settings/tokens
This token should be set in a secret in the repository, which is exposed as an
environment variable in the main.yml workflow file.
The script also requires ``pandoc`` to be previously installed in the system.
Requires Python3.6+.
"""
import os
import re
import sys
from pathlib import Path
import github3
import pypandoc
def publish_github_release(slug, token, tag_name, body):
github = github3.login(token=token)
owner, repo = slug.split("/")
repo = github.repository(owner, repo)
return repo.create_release(tag_name=tag_name, body=body)
def parse_changelog(tag_name):
p = Path(__file__).parent.parent / "doc/en/changelog.rst"
changelog_lines = p.read_text(encoding="UTF-8").splitlines()
title_regex = re.compile(r"pytest (\d\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)")
consuming_version = False
version_lines = []
for line in changelog_lines:
m = title_regex.match(line)
if m:
# found the version we want: start to consume lines until we find the next version title
if m.group(1) == tag_name:
consuming_version = True
# found a new version title while parsing the version we want: break out
elif consuming_version:
break
if consuming_version:
version_lines.append(line)
return "\n".join(version_lines)
def convert_rst_to_md(text):
return pypandoc.convert_text(
text, "md", format="rst", extra_args=["--wrap=preserve"]
)
def main(argv):
if len(argv) > 1:
tag_name = argv[1]
else:
tag_name = os.environ.get("GITHUB_REF")
if not tag_name:
print("tag_name not given and $GITHUB_REF not set", file=sys.stderr)
return 1
if tag_name.startswith("refs/tags/"):
tag_name = tag_name[len("refs/tags/") :]
token = os.environ.get("GH_RELEASE_NOTES_TOKEN")
if not token:
print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr)
return 1
slug = os.environ.get("GITHUB_REPOSITORY")
if not slug:
print("GITHUB_REPOSITORY not set", file=sys.stderr)
return 1
rst_body = parse_changelog(tag_name)
md_body = convert_rst_to_md(rst_body)
if not publish_github_release(slug, token, tag_name, md_body):
print("Could not publish release notes:", file=sys.stderr)
print(md_body, file=sys.stderr)
return 5
print()
print(f"Release notes for {tag_name} published successfully:")
print(f"https://github.com/{slug}/releases/tag/{tag_name}")
print()
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

View File

@@ -1,3 +1,4 @@
# mypy: disallow-untyped-defs
"""Invoke development tasks."""
import argparse
import os
@@ -10,15 +11,15 @@ from colorama import Fore
from colorama import init
def announce(version, template_name, doc_version):
def announce(version: str, template_name: str, doc_version: str) -> None:
"""Generates a new release announcement entry in the docs."""
# Get our list of authors
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"])
stdout = stdout.decode("utf-8")
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"], encoding="UTF-8")
last_version = stdout.strip()
stdout = check_output(["git", "log", f"{last_version}..HEAD", "--format=%aN"])
stdout = stdout.decode("utf-8")
stdout = check_output(
["git", "log", f"{last_version}..HEAD", "--format=%aN"], encoding="UTF-8"
)
contributors = {
name
@@ -61,7 +62,7 @@ def announce(version, template_name, doc_version):
check_call(["git", "add", str(target)])
def regen(version):
def regen(version: str) -> None:
"""Call regendoc tool to update examples and pytest output in the docs."""
print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs")
check_call(
@@ -70,7 +71,7 @@ def regen(version):
)
def fix_formatting():
def fix_formatting() -> None:
"""Runs pre-commit in all files to ensure they are formatted correctly"""
print(
f"{Fore.CYAN}[generate.fix linting] {Fore.RESET}Fixing formatting using pre-commit"
@@ -78,13 +79,15 @@ def fix_formatting():
call(["pre-commit", "run", "--all-files"])
def check_links():
def check_links() -> None:
"""Runs sphinx-build to check links"""
print(f"{Fore.CYAN}[generate.check_links] {Fore.RESET}Checking links")
check_call(["tox", "-e", "docs-checklinks"])
def pre_release(version, template_name, doc_version, *, skip_check_links):
def pre_release(
version: str, template_name: str, doc_version: str, *, skip_check_links: bool
) -> None:
"""Generates new docs, release announcements and creates a local tag."""
announce(version, template_name, doc_version)
regen(version)
@@ -102,12 +105,12 @@ def pre_release(version, template_name, doc_version, *, skip_check_links):
print("Please push your branch and open a PR.")
def changelog(version, write_out=False):
def changelog(version: str, write_out: bool = False) -> None:
addopts = [] if write_out else ["--draft"]
check_call(["towncrier", "--yes", "--version", version] + addopts)
def main():
def main() -> None:
init(autoreset=True)
parser = argparse.ArgumentParser()
parser.add_argument("version", help="Release version")

View File

@@ -1,11 +1,12 @@
import sys
# mypy: disallow-untyped-defs
from subprocess import call
import sys
def main():
def main() -> int:
"""
Platform agnostic wrapper script for towncrier.
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
Platform-agnostic wrapper script for towncrier.
Fixes the issue (#7251) where Windows users are unable to natively run tox -e docs to build pytest docs.
"""
with open(
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"

View File

@@ -1,18 +1,23 @@
# mypy: disallow-untyped-defs
import datetime
import pathlib
import re
from textwrap import dedent
from textwrap import indent
from typing import Any
from typing import Iterable
from typing import Iterator
from typing import TypedDict
import packaging.version
import platformdirs
import tabulate
import wcwidth
from requests_cache import CachedResponse
from requests_cache import CachedSession
from requests_cache import OriginalResponse
from requests_cache import SQLiteCache
import tabulate
from tqdm import tqdm
import wcwidth
FILE_HEAD = r"""
@@ -56,6 +61,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
)
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
"logot",
"nuts",
"flask_fixture",
}
@@ -81,7 +87,6 @@ def project_response_with_refresh(
force refresh in case of last serial mismatch
"""
response = session.get(f"https://pypi.org/pypi/{name}/json")
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
@@ -109,7 +114,17 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
}
def iter_plugins():
class PluginInfo(TypedDict):
"""Relevant information about a plugin to generate the summary."""
name: str
summary: str
last_release: str
status: str
requires: str
def iter_plugins() -> Iterator[PluginInfo]:
session = get_session()
name_2_serial = pytest_plugin_projects_from_pypi(session)
@@ -136,7 +151,7 @@ def iter_plugins():
requires = requirement
break
def version_sort_key(version_string):
def version_sort_key(version_string: str) -> Any:
"""
Return the sort key for the given version string
returned by the API.
@@ -162,20 +177,19 @@ def iter_plugins():
yield {
"name": name,
"summary": summary.strip(),
"last release": last_release,
"last_release": last_release,
"status": status,
"requires": requires,
}
def plugin_definitions(plugins):
def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]:
"""Return RST for the plugin list that fits better on a vertical page."""
for plugin in plugins:
yield dedent(
f"""
{plugin['name']}
*last release*: {plugin["last release"]},
*last release*: {plugin["last_release"]},
*status*: {plugin["status"]},
*requires*: {plugin["requires"]}
@@ -184,7 +198,7 @@ def plugin_definitions(plugins):
)
def main():
def main() -> None:
plugins = [*iter_plugins()]
reference_dir = pathlib.Path("doc", "en", "reference")

View File

@@ -1,7 +1,8 @@
__all__ = ["__version__", "version_tuple"]
try:
from ._version import version as __version__, version_tuple
from ._version import version as __version__
from ._version import version_tuple
except ImportError: # pragma: no cover
# broken installation, we don't even try
# unknown only works because we do poor mans version compare

View File

@@ -61,10 +61,11 @@ If things do not work right away:
which should throw a KeyError: 'COMPLINE' (which is properly set by the
global argcomplete script).
"""
import argparse
from glob import glob
import os
import sys
from glob import glob
from typing import Any
from typing import List
from typing import Optional

View File

@@ -1,4 +1,5 @@
"""Python inspection/code generation API."""
from .code import Code
from .code import ExceptionInfo
from .code import filter_traceback
@@ -9,6 +10,7 @@ from .code import TracebackEntry
from .source import getrawcode
from .source import Source
__all__ = [
"Code",
"ExceptionInfo",

View File

@@ -1,14 +1,14 @@
import ast
import dataclasses
import inspect
import os
import re
import sys
import traceback
from inspect import CO_VARARGS
from inspect import CO_VARKEYWORDS
from io import StringIO
import os
from pathlib import Path
import re
import sys
import traceback
from traceback import format_exception_only
from types import CodeType
from types import FrameType
@@ -50,6 +50,7 @@ from _pytest.deprecated import check_ispytest
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
@@ -486,9 +487,10 @@ class ExceptionInfo(Generic[E]):
.. versionadded:: 7.4
"""
assert (
exception.__traceback__
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
assert exception.__traceback__, (
"Exceptions passed to ExcInfo.from_exception(...)"
" must have a non-None __traceback__."
)
exc_info = (type(exception), exception, exception.__traceback__)
return cls.from_exc_info(exc_info, exprinfo)
@@ -587,9 +589,7 @@ class ExceptionInfo(Generic[E]):
def __repr__(self) -> str:
if self._excinfo is None:
return "<ExceptionInfo for raises contextmanager>"
return "<{} {} tblen={}>".format(
self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
)
return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
def exconly(self, tryshort: bool = False) -> str:
"""Return the exception as a string.
@@ -698,10 +698,21 @@ class ExceptionInfo(Generic[E]):
return fmt.repr_excinfo(self)
def _stringify_exception(self, exc: BaseException) -> str:
try:
notes = getattr(exc, "__notes__", [])
except KeyError:
# Workaround for https://github.com/python/cpython/issues/98778 on
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
if sys.version_info[:2] <= (3, 11) and isinstance(exc, HTTPError):
notes = []
else:
raise
return "\n".join(
[
str(exc),
*getattr(exc, "__notes__", []),
*notes,
]
)
@@ -1006,13 +1017,8 @@ class FormattedExcinfo:
extraline: Optional[str] = (
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
" The following exception happened when comparing locals in the stack frame:\n"
" {exc_type}: {exc_msg}\n"
" Displaying first and last {max_frames} stack frames out of {total}."
).format(
exc_type=type(e).__name__,
exc_msg=str(e),
max_frames=max_frames,
total=len(traceback),
f" {type(e).__name__}: {str(e)}\n"
f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
)
# Type ignored because adding two instances of a List subtype
# currently incorrectly has type List instead of the subtype.
@@ -1219,7 +1225,6 @@ class ReprEntry(TerminalRepr):
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
character, as doing so might break line continuations.
"""
if not self.lines:
return

View File

@@ -1,10 +1,9 @@
import ast
from bisect import bisect_right
import inspect
import textwrap
import tokenize
import types
import warnings
from bisect import bisect_right
from typing import Iterable
from typing import Iterator
from typing import List
@@ -12,6 +11,7 @@ from typing import Optional
from typing import overload
from typing import Tuple
from typing import Union
import warnings
class Source:
@@ -196,7 +196,9 @@ def getstatementrange_ast(
# by using the BlockFinder helper used which inspect.getsource() uses itself.
block_finder = inspect.BlockFinder()
# If we start with an indented line, put blockfinder to "started" mode.
block_finder.started = source.lines[start][0].isspace()
block_finder.started = (
bool(source.lines[start]) and source.lines[start][0].isspace()
)
it = ((x + "\n") for x in source.lines[start:end])
try:
for tok in tokenize.generate_tokens(lambda: next(it)):

View File

@@ -14,9 +14,9 @@
# useful, thank small children who sleep at night.
import collections as _collections
import dataclasses as _dataclasses
from io import StringIO as _StringIO
import re
import types as _types
from io import StringIO as _StringIO
from typing import Any
from typing import Callable
from typing import Dict

View File

@@ -19,8 +19,8 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str:
raise
except BaseException as exc:
exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
exc_info, type(obj).__name__, id(obj)
return (
f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
)
@@ -108,7 +108,6 @@ def saferepr(
This function is a wrapper around the Repr/reprlib functionality of the
stdlib.
"""
return SafeRepr(maxsize, use_ascii).repr(obj)

View File

@@ -1,4 +1,5 @@
"""Helper functions for writing to terminals and files."""
import os
import shutil
import sys
@@ -183,9 +184,7 @@ class TerminalWriter:
"""
if indents and len(indents) != len(lines):
raise ValueError(
"indents size ({}) should have same size as lines ({})".format(
len(indents), len(lines)
)
f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
)
if not indents:
indents = [""] * len(lines)
@@ -200,8 +199,9 @@ class TerminalWriter:
"""Highlight the given source if we have markup support."""
from _pytest.config.exceptions import UsageError
if not self.hasmarkup or not self.code_highlight:
if not source or not self.hasmarkup or not self.code_highlight:
return source
try:
from pygments.formatters.terminal import TerminalFormatter

View File

@@ -1,5 +1,5 @@
import unicodedata
from functools import lru_cache
import unicodedata
@lru_cache(100)

View File

@@ -1,4 +1,5 @@
"""create errno-specific classes for IO or os calls."""
from __future__ import annotations
import errno
@@ -8,6 +9,7 @@ from typing import Callable
from typing import TYPE_CHECKING
from typing import TypeVar
if TYPE_CHECKING:
from typing_extensions import ParamSpec

View File

@@ -1,16 +1,13 @@
"""local path implementation."""
from __future__ import annotations
import atexit
from contextlib import contextmanager
import fnmatch
import importlib.util
import io
import os
import posixpath
import sys
import uuid
import warnings
from contextlib import contextmanager
from os.path import abspath
from os.path import dirname
from os.path import exists
@@ -19,18 +16,23 @@ from os.path import isdir
from os.path import isfile
from os.path import islink
from os.path import normpath
import posixpath
from stat import S_ISDIR
from stat import S_ISLNK
from stat import S_ISREG
import sys
from typing import Any
from typing import Callable
from typing import cast
from typing import Literal
from typing import overload
from typing import TYPE_CHECKING
import uuid
import warnings
from . import error
# Moved from local.py.
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
@@ -675,7 +677,7 @@ class LocalPath:
else:
kw.setdefault("dirname", dirname)
kw.setdefault("sep", self.sep)
obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw)
obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
return obj
def _getbyspec(self, spec: str) -> list[str]:
@@ -760,7 +762,10 @@ class LocalPath:
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
# Which seems incorrect, given io.open supports the given argument types.
return error.checked_call(
io.open, self.strpath, mode, encoding=encoding # type:ignore[arg-type]
io.open,
self.strpath,
mode,
encoding=encoding, # type:ignore[arg-type]
)
return error.checked_call(open, self.strpath, mode)
@@ -779,11 +784,11 @@ class LocalPath:
valid checkers::
file=1 # is a file
file=0 # is not a file (may not even exist)
dir=1 # is a dir
link=1 # is a link
exists=1 # exists
file = 1 # is a file
file = 0 # is not a file (may not even exist)
dir = 1 # is a dir
link = 1 # is a link
exists = 1 # exists
You can specify multiple checker definitions, for example::
@@ -1167,7 +1172,8 @@ class LocalPath:
where the 'self' path points to executable.
The process is directly invoked and not through a system shell.
"""
from subprocess import Popen, PIPE
from subprocess import PIPE
from subprocess import Popen
popen_opts.pop("stdout", None)
popen_opts.pop("stderr", None)
@@ -1277,7 +1283,8 @@ class LocalPath:
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
path = error.checked_call(
tempfile.mkdtemp, dir=str(rootdir) # type:ignore[arg-type]
tempfile.mkdtemp,
dir=str(rootdir), # type:ignore[arg-type]
)
return cls(path)

View File

@@ -1,4 +1,5 @@
"""Support for presenting detailed information in failing assertions."""
import sys
from typing import Any
from typing import Generator
@@ -15,6 +16,7 @@ from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.nodes import Item
if TYPE_CHECKING:
from _pytest.main import Session
@@ -128,7 +130,6 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
reporting via the pytest_assertrepr_compare hook. This sets up this custom
comparison for the test.
"""
ihook = item.ihook
def callbinrepr(op, left: object, right: object) -> Optional[str]:

View File

@@ -1,5 +1,7 @@
"""Rewrite assertion AST to produce nice error messages."""
import ast
from collections import defaultdict
import errno
import functools
import importlib.abc
@@ -9,13 +11,12 @@ import io
import itertools
import marshal
import os
from pathlib import Path
from pathlib import PurePath
import struct
import sys
import tokenize
import types
from collections import defaultdict
from pathlib import Path
from pathlib import PurePath
from typing import Callable
from typing import Dict
from typing import IO
@@ -33,15 +34,17 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import saferepr
from _pytest._version import version
from _pytest.assertion import util
from _pytest.assertion.util import ( # noqa: F401
format_explanation as _format_explanation,
)
from _pytest.config import Config
from _pytest.main import Session
from _pytest.pathlib import absolutepath
from _pytest.pathlib import fnmatch_ex
from _pytest.stash import StashKey
# fmt: off
from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
# fmt:on
if TYPE_CHECKING:
from _pytest.assertion import AssertionState
@@ -858,9 +861,10 @@ class AssertionRewriter(ast.NodeVisitor):
the expression is false.
"""
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
from _pytest.warning_types import PytestAssertRewriteWarning
import warnings
from _pytest.warning_types import PytestAssertRewriteWarning
# TODO: This assert should not be needed.
assert self.module_path is not None
warnings.warn_explicit(
@@ -1016,9 +1020,7 @@ class AssertionRewriter(ast.NodeVisitor):
]
):
pytest_temp = self.variable()
self.variables_overwrite[self.scope][
v.left.target.id
] = v.left # type:ignore[assignment]
self.variables_overwrite[self.scope][v.left.target.id] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp
self.push_format_context()
res, expl = self.visit(v)
@@ -1062,9 +1064,7 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
self.scope, {}
):
arg = self.variables_overwrite[self.scope][
arg.id
] # type:ignore[assignment]
arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
res, expl = self.visit(arg)
arg_expls.append(expl)
new_args.append(res)
@@ -1072,9 +1072,7 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance(
keyword.value, ast.Name
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
keyword.value = self.variables_overwrite[self.scope][
keyword.value.id
] # type:ignore[assignment]
keyword.value = self.variables_overwrite[self.scope][keyword.value.id] # type:ignore[assignment]
res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg:
@@ -1111,13 +1109,9 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance(
comp.left, ast.Name
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
comp.left = self.variables_overwrite[self.scope][
comp.left.id
] # type:ignore[assignment]
comp.left = self.variables_overwrite[self.scope][comp.left.id] # type:ignore[assignment]
if isinstance(comp.left, ast.NamedExpr):
self.variables_overwrite[self.scope][
comp.left.target.id
] = comp.left # type:ignore[assignment]
self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment]
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})"
@@ -1135,9 +1129,7 @@ class AssertionRewriter(ast.NodeVisitor):
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
self.variables_overwrite[self.scope][
left_res.id
] = next_operand # type:ignore[assignment]
self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment]
next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})"

View File

@@ -3,6 +3,7 @@
Current default behaviour is to truncate assertion explanations at
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
"""
from typing import List
from typing import Optional

View File

@@ -1,4 +1,5 @@
"""Utilities for assertion debugging."""
import collections.abc
import os
import pprint
@@ -14,13 +15,14 @@ from typing import Protocol
from typing import Sequence
from unicodedata import normalize
import _pytest._code
from _pytest import outcomes
import _pytest._code
from _pytest._io.pprint import PrettyPrinter
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
from _pytest.config import Config
# The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the
@@ -301,8 +303,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
if i > 42:
i -= 10 # Provide some context
explanation += [
"Skipping {} identical trailing "
"characters in diff, use -v to show".format(i)
f"Skipping {i} identical trailing "
"characters in diff, use -v to show"
]
left = left[:-i]
right = right[:-i]

View File

@@ -1,4 +1,5 @@
"""Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
import dataclasses
@@ -31,6 +32,7 @@ from _pytest.nodes import Directory
from _pytest.nodes import File
from _pytest.reports import TestReport
README_CONTENT = """\
# pytest cache directory #
@@ -111,6 +113,7 @@ class Cache:
"""
check_ispytest(_ispytest)
import warnings
from _pytest.warning_types import PytestCacheWarning
warnings.warn(
@@ -366,15 +369,13 @@ class LFPlugin:
noun = "failure" if self._previously_failed_count == 1 else "failures"
suffix = " first" if self.config.getoption("failedfirst") else ""
self._report_status = "rerun previous {count} {noun}{suffix}".format(
count=self._previously_failed_count, suffix=suffix, noun=noun
self._report_status = (
f"rerun previous {self._previously_failed_count} {noun}{suffix}"
)
if self._skipped_files > 0:
files_noun = "file" if self._skipped_files == 1 else "files"
self._report_status += " (skipped {files} {files_noun})".format(
files=self._skipped_files, files_noun=files_noun
)
self._report_status += f" (skipped {self._skipped_files} {files_noun})"
else:
self._report_status = "no previously failed tests, "
if self.config.getoption("last_failed_no_failures") == "none":

View File

@@ -1,11 +1,12 @@
"""Per-test stdout/stderr capturing mechanism."""
import abc
import collections
import contextlib
import io
from io import UnsupportedOperation
import os
import sys
from io import UnsupportedOperation
from tempfile import TemporaryFile
from types import TracebackType
from typing import Any
@@ -38,6 +39,7 @@ from _pytest.nodes import File
from _pytest.nodes import Item
from _pytest.reports import CollectReport
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@@ -789,9 +791,7 @@ class CaptureManager:
current_fixture = self._capture_fixture.request.fixturename
requested_fixture = capture_fixture.request.fixturename
capture_fixture.request.raiseerror(
"cannot use {} and {} at the same time".format(
requested_fixture, current_fixture
)
f"cannot use {requested_fixture} and {current_fixture} at the same time"
)
self._capture_fixture = capture_fixture
@@ -987,7 +987,6 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
@@ -1015,7 +1014,6 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsysbinary):
@@ -1043,7 +1041,6 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfd):
@@ -1071,7 +1068,6 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfdbinary):

View File

@@ -1,15 +1,16 @@
"""Python version compatibility code."""
from __future__ import annotations
import dataclasses
import enum
import functools
import inspect
import os
import sys
from inspect import Parameter
from inspect import signature
import os
from pathlib import Path
import sys
from typing import Any
from typing import Callable
from typing import Final
@@ -258,9 +259,7 @@ def get_real_func(obj):
from _pytest._io.saferepr import saferepr
raise ValueError(
("could not find real function of {start}\nstopped at {current}").format(
start=saferepr(start_obj), current=saferepr(obj)
)
f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}"
)
if isinstance(obj, functools.partial):
obj = obj.func

View File

@@ -1,21 +1,21 @@
"""Command line options, ini-file and conftest.py processing."""
import argparse
import collections.abc
import copy
import dataclasses
import enum
from functools import lru_cache
import glob
import importlib.metadata
import inspect
import os
from pathlib import Path
import re
import shlex
import sys
import types
import warnings
from functools import lru_cache
from pathlib import Path
from textwrap import dedent
import types
from types import FunctionType
from types import TracebackType
from typing import Any
@@ -37,6 +37,7 @@ from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import warnings
import pluggy
from pluggy import HookimplMarker
@@ -45,16 +46,16 @@ from pluggy import HookspecMarker
from pluggy import HookspecOpts
from pluggy import PluginManager
import _pytest._code
import _pytest.deprecated
import _pytest.hookspec
from .compat import PathAwareHookProxy
from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError
from .findpaths import determine_setup
import _pytest._code
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter
import _pytest.deprecated
import _pytest.hookspec
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath
@@ -67,10 +68,12 @@ from _pytest.stash import Stash
from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING:
from .argparsing import Argument
from .argparsing import Parser
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument, Parser
_PluggyPlugin = object
@@ -121,9 +124,7 @@ class ConftestImportFailure(Exception):
self.excinfo = excinfo
def __str__(self) -> str:
return "{}: {} (from {})".format(
self.excinfo[0].__name__, self.excinfo[1], self.path
)
return f"{self.excinfo[0].__name__}: {self.excinfo[1]} (from {self.path})"
def filter_traceback_for_conftest_import_failure(
@@ -496,15 +497,19 @@ class PytestPluginManager(PluginManager):
)
)
return None
ret: Optional[str] = super().register(plugin, name)
if ret:
plugin_name = super().register(plugin, name)
if plugin_name is not None:
self.hook.pytest_plugin_registered.call_historic(
kwargs=dict(plugin=plugin, manager=self)
kwargs=dict(
plugin=plugin,
plugin_name=plugin_name,
manager=self,
)
)
if isinstance(plugin, types.ModuleType):
self.consider_module(plugin)
return ret
return plugin_name
def getplugin(self, name: str):
# Support deprecated naming because plugins (xdist e.g.) use it.
@@ -637,7 +642,8 @@ class PytestPluginManager(PluginManager):
def _importconftest(
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> types.ModuleType:
existing = self.get_plugin(str(conftestpath))
conftestpath_plugin_name = str(conftestpath)
existing = self.get_plugin(conftestpath_plugin_name)
if existing is not None:
return cast(types.ModuleType, existing)
@@ -659,10 +665,15 @@ class PytestPluginManager(PluginManager):
if dirpath in self._dirpath2confmods:
for path, mods in self._dirpath2confmods.items():
if dirpath in path.parents or path == dirpath:
assert mod not in mods
if mod in mods:
raise AssertionError(
f"While trying to load conftest path {str(conftestpath)}, "
f"found that the module {mod} is already loaded with path {mod.__file__}. "
"This is not supposed to happen. Please report this issue to pytest."
)
mods.append(mod)
self.trace(f"loading conftestmodule {mod!r}")
self.consider_conftest(mod)
self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
return mod
def _check_non_top_pytest_plugins(
@@ -742,9 +753,11 @@ class PytestPluginManager(PluginManager):
del self._name2plugin["pytest_" + name]
self.import_plugin(arg, consider_entry_points=True)
def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
def consider_conftest(
self, conftestmodule: types.ModuleType, registration_name: str
) -> None:
""":meta private:"""
self.register(conftestmodule, name=conftestmodule.__file__)
self.register(conftestmodule, name=registration_name)
def consider_env(self) -> None:
""":meta private:"""
@@ -800,7 +813,7 @@ class PytestPluginManager(PluginManager):
def _get_plugin_specs_as_list(
specs: Union[None, types.ModuleType, str, Sequence[str]]
specs: Union[None, types.ModuleType, str, Sequence[str]],
) -> List[str]:
"""Parse a plugins specification into a list of plugin names."""
# None means empty.
@@ -968,7 +981,8 @@ class Config:
*,
invocation_params: Optional[InvocationParams] = None,
) -> None:
from .argparsing import Parser, FILE_OR_DIR
from .argparsing import FILE_OR_DIR
from .argparsing import Parser
if invocation_params is None:
invocation_params = self.InvocationParams(
@@ -1387,8 +1401,9 @@ class Config:
return
# Imported lazily to improve start-up time.
from packaging.requirements import InvalidRequirement
from packaging.requirements import Requirement
from packaging.version import Version
from packaging.requirements import InvalidRequirement, Requirement
plugin_info = self.pluginmanager.list_plugin_distinfo()
plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
@@ -1610,9 +1625,7 @@ class Config:
key, user_ini_value = ini_config.split("=", 1)
except ValueError as e:
raise UsageError(
"-o/--override-ini expects option=value style (got: {!r}).".format(
ini_config
)
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
) from e
else:
if key == name:
@@ -1670,7 +1683,6 @@ class Config:
can be used to explicitly use the global verbosity level.
Example:
.. code-block:: ini
# content of pytest.ini

View File

@@ -1,8 +1,7 @@
import argparse
from gettext import gettext
import os
import sys
import warnings
from gettext import gettext
from typing import Any
from typing import Callable
from typing import cast
@@ -16,6 +15,7 @@ from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
import warnings
import _pytest._io
from _pytest.config.exceptions import UsageError
@@ -24,6 +24,7 @@ from _pytest.deprecated import ARGUMENT_TYPE_STR
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
from _pytest.deprecated import check_ispytest
FILE_OR_DIR = "file_or_dir"
@@ -219,7 +220,7 @@ class Parser:
def get_ini_default_for_type(
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]],
) -> Any:
"""
Used by addini to get the default value for a given ini-option type, when
@@ -480,7 +481,7 @@ class MyOptionParser(argparse.ArgumentParser):
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
if not arg_string:
return None
if not arg_string[0] in self.prefix_chars:
if arg_string[0] not in self.prefix_chars:
return None
if arg_string in self._option_string_actions:
action = self._option_string_actions[arg_string]

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
import functools
import warnings
from pathlib import Path
from typing import Mapping
import warnings
import pluggy
@@ -11,6 +11,7 @@ from ..compat import LEGACY_PATH
from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG
# hookname: (Path, LEGACY_PATH)
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
"pytest_ignore_collect": ("collection_path", "path"),

View File

@@ -1,6 +1,6 @@
import os
import sys
from pathlib import Path
import sys
from typing import Dict
from typing import Iterable
from typing import List
@@ -37,7 +37,6 @@ def load_config_dict_from_file(
Return None if the file does not contain valid pytest configuration.
"""
# Configuration from ini files are obtained from the [pytest] section, if present.
if filepath.suffix == ".ini":
iniconfig = _parse_ini_config(filepath)
@@ -211,9 +210,7 @@ def determine_setup(
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
if not rootdir.is_dir():
raise UsageError(
"Directory '{}' not found. Check your '--rootdir' option.".format(
rootdir
)
f"Directory '{rootdir}' not found. Check your '--rootdir' option."
)
assert rootdir is not None
return rootdir, inipath, inicfg or {}

View File

@@ -1,9 +1,9 @@
"""Interactive debugging with PDB, the Python Debugger."""
import argparse
import functools
import sys
import types
import unittest
from typing import Any
from typing import Callable
from typing import Generator
@@ -13,6 +13,7 @@ from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import unittest
from _pytest import outcomes
from _pytest._code import ExceptionInfo
@@ -25,6 +26,7 @@ from _pytest.config.exceptions import UsageError
from _pytest.nodes import Node
from _pytest.reports import BaseReport
if TYPE_CHECKING:
from _pytest.capture import CaptureManager
from _pytest.runner import CallInfo
@@ -263,8 +265,7 @@ class pytestPDB:
elif capturing:
tw.sep(
">",
"PDB %s (IO-capturing turned off for %s)"
% (method, capturing),
f"PDB {method} (IO-capturing turned off for {capturing})",
)
else:
tw.sep(">", f"PDB {method}")

View File

@@ -8,6 +8,7 @@ All constants defined in this module should be either instances of
:class:`PytestWarning`, or :class:`UnformattedWarning`
in case of warnings which need to format their messages.
"""
from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning
@@ -15,6 +16,7 @@ from _pytest.warning_types import PytestRemovedIn8Warning
from _pytest.warning_types import PytestRemovedIn9Warning
from _pytest.warning_types import UnformattedWarning
# set of plugins which have been integrated into the core; we use this list to ignore
# them during registration to avoid conflicts
DEPRECATED_EXTERNAL_PLUGINS = {

View File

@@ -1,15 +1,15 @@
"""Discover and run doctests in modules and test files."""
import bdb
from contextlib import contextmanager
import functools
import inspect
import os
from pathlib import Path
import platform
import sys
import traceback
import types
import warnings
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Dict
@@ -23,6 +23,7 @@ from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import warnings
from _pytest import outcomes
from _pytest._code.code import ExceptionInfo
@@ -39,11 +40,11 @@ from _pytest.nodes import Item
from _pytest.outcomes import OutcomeException
from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.python import Module
from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
import doctest
@@ -105,7 +106,7 @@ def pytest_addoption(parser: Parser) -> None:
"--doctest-ignore-import-errors",
action="store_true",
default=False,
help="Ignore doctest ImportErrors",
help="Ignore doctest collection errors",
dest="doctest_ignore_import_errors",
)
group.addoption(
@@ -485,9 +486,9 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
except Exception as e:
warnings.warn(
"Got %r when unwrapping %r. This is usually caused "
f"Got {e!r} when unwrapping {func!r}. This is usually caused "
"by a violation of Python's object protocol; see e.g. "
"https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
"https://github.com/pytest-dev/pytest/issues/5080",
PytestWarning,
)
raise
@@ -566,16 +567,17 @@ class DoctestModule(Module):
)
else:
try:
module = import_path(
self.path,
root=self.config.rootpath,
mode=self.config.getoption("importmode"),
)
except ImportError:
module = self.obj
except Collector.CollectError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
else:
raise
# While doctests currently don't support fixtures directly, we still
# need to pick up autouse fixtures.
self.session._fixturemanager.parsefactories(self)
# Uses internal doctest module parsing mechanism.
finder = MockAwareDocTestFinder()
optionflags = get_optionflags(self.config)

View File

@@ -2,11 +2,11 @@ import os
import sys
from typing import Generator
import pytest
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.nodes import Item
from _pytest.stash import StashKey
import pytest
fault_handler_original_stderr_fd_key = StashKey[int]()

View File

@@ -1,12 +1,11 @@
import abc
from collections import defaultdict
from collections import deque
from contextlib import suppress
import dataclasses
import functools
import inspect
import os
import warnings
from collections import defaultdict
from collections import deque
from contextlib import suppress
from pathlib import Path
from typing import AbstractSet
from typing import Any
@@ -30,6 +29,7 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import warnings
import _pytest
from _pytest import nodes
@@ -607,13 +607,9 @@ class FixtureRequest(abc.ABC):
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
if has_params and fixtures_not_supported:
msg = (
"{name} does not support fixtures, maybe unittest.TestCase subclass?\n"
"Node id: {nodeid}\n"
"Function type: {typename}"
).format(
name=funcitem.name,
nodeid=funcitem.nodeid,
typename=type(funcitem).__name__,
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
f"Node id: {funcitem.nodeid}\n"
f"Function type: {type(funcitem).__name__}"
)
fail(msg, pytrace=False)
if has_params:
@@ -746,9 +742,7 @@ class SubRequest(FixtureRequest):
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
assert node, f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
return node
def _check_scope(
@@ -852,8 +846,8 @@ class FixtureLookupError(LookupError):
if faclist:
available.add(name)
if self.argname in available:
msg = " recursive dependency involving fixture '{}' detected".format(
self.argname
msg = (
f" recursive dependency involving fixture '{self.argname}' detected"
)
else:
msg = f"fixture '{self.argname}' not found"
@@ -947,15 +941,13 @@ def _eval_scope_callable(
result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
except Exception as e:
raise TypeError(
"Error evaluating {} while defining fixture '{}'.\n"
"Expected a function with the signature (*, fixture_name, config)".format(
scope_callable, fixture_name
)
f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n"
"Expected a function with the signature (*, fixture_name, config)"
) from e
if not isinstance(result, str):
fail(
"Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
"{!r}".format(scope_callable, fixture_name, result),
f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n"
f"{result!r}",
pytrace=False,
)
return result
@@ -1099,9 +1091,7 @@ class FixtureDef(Generic[FixtureValue]):
return request.param_index if not hasattr(request, "param") else request.param
def __repr__(self) -> str:
return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format(
self.argname, self.scope, self.baseid
)
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
def resolve_fixture_function(
@@ -1122,7 +1112,8 @@ def resolve_fixture_function(
# Handle the case where fixture is defined not in a test class, but some other class
# (for example a plugin class with a fixture), see #2270.
if hasattr(fixturefunc, "__self__") and not isinstance(
request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr]
request.instance,
fixturefunc.__self__.__class__, # type: ignore[union-attr]
):
return fixturefunc
fixturefunc = getimfunc(fixturedef.func)
@@ -1205,7 +1196,7 @@ class FixtureFunctionMarker:
if getattr(function, "_pytestfixturefunction", False):
raise ValueError(
"fixture is being applied more than once to the same function"
f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
)
if hasattr(function, "pytestmark"):
@@ -1217,9 +1208,7 @@ class FixtureFunctionMarker:
if name == "request":
location = getlocation(function)
fail(
"'request' is a reserved word for fixtures, use another name:\n {}".format(
location
),
f"'request' is a reserved word for fixtures, use another name:\n {location}",
pytrace=False,
)
@@ -1495,25 +1484,27 @@ class FixtureManager:
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
nodeid = None
try:
p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
except AttributeError:
pass
def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None:
# Fixtures defined in conftest plugins are only visible to within the
# conftest's directory. This is unlike fixtures in non-conftest plugins
# which have global visibility. So for conftests, construct the base
# nodeid from the plugin name (which is the conftest path).
if plugin_name and plugin_name.endswith("conftest.py"):
# Note: we explicitly do *not* use `plugin.__file__` here -- The
# difference is that plugin_name has the correct capitalization on
# case-insensitive systems (Windows) and other normalization issues
# (issue #11816).
conftestpath = absolutepath(plugin_name)
try:
nodeid = str(conftestpath.parent.relative_to(self.config.rootpath))
except ValueError:
nodeid = ""
if nodeid == ".":
nodeid = ""
if os.sep != nodes.SEP:
nodeid = nodeid.replace(os.sep, nodes.SEP)
else:
# Construct the base nodeid which is later used to check
# what fixtures are visible for particular tests (as denoted
# by their test id).
if p.name == "conftest.py":
try:
nodeid = str(p.parent.relative_to(self.config.rootpath))
except ValueError:
nodeid = ""
if nodeid == ".":
nodeid = ""
if os.sep != nodes.SEP:
nodeid = nodeid.replace(os.sep, nodes.SEP)
nodeid = None
self.parsefactories(plugin, nodeid)

View File

@@ -1,5 +1,6 @@
"""Provides a function to report all internal modules for using freezing
tools."""
import types
from typing import Iterator
from typing import List

View File

@@ -1,18 +1,19 @@
"""Version info, help messages, tracing configuration."""
from argparse import Action
import os
import sys
from argparse import Action
from typing import Generator
from typing import List
from typing import Optional
from typing import Union
import pytest
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PrintHelp
from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter
import pytest
class HelpAction(Action):
@@ -135,9 +136,7 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
def showversion(config: Config) -> None:
if config.option.version > 1:
sys.stdout.write(
"This is pytest version {}, imported from {}\n".format(
pytest.__version__, pytest.__file__
)
f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
)
plugininfo = getpluginversioninfo(config)
if plugininfo:

View File

@@ -1,5 +1,6 @@
"""Hook specifications for pytest plugins which are invoked by pytest itself
and by builtin plugins."""
from pathlib import Path
from typing import Any
from typing import Dict
@@ -15,17 +16,19 @@ from pluggy import HookspecMarker
from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
if TYPE_CHECKING:
import pdb
import warnings
from typing import Literal
import warnings
from _pytest._code.code import ExceptionRepr
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ExceptionRepr
from _pytest.compat import LEGACY_PATH
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager
from _pytest.config import _PluggyPlugin
from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import SubRequest
@@ -42,7 +45,6 @@ if TYPE_CHECKING:
from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter
from _pytest.terminal import TestShortLogReport
from _pytest.compat import LEGACY_PATH
hookspec = HookspecMarker("pytest")
@@ -66,12 +68,15 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
@hookspec(historic=True)
def pytest_plugin_registered(
plugin: "_PluggyPlugin", manager: "PytestPluginManager"
plugin: "_PluggyPlugin",
plugin_name: str,
manager: "PytestPluginManager",
) -> None:
"""A new pytest plugin got registered.
:param plugin: The plugin module or instance.
:param pytest.PytestPluginManager manager: pytest plugin manager.
:param plugin_name: The name by which the plugin is registered.
:param manager: The pytest plugin manager.
.. note::
This hook is incompatible with hook wrappers.

View File

@@ -6,12 +6,12 @@ Based on initial code from Ross Lawley.
Output conforms to
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
"""
from datetime import datetime
import functools
import os
import platform
import re
import xml.etree.ElementTree as ET
from datetime import datetime
from typing import Callable
from typing import Dict
from typing import List
@@ -19,8 +19,8 @@ from typing import Match
from typing import Optional
from typing import Tuple
from typing import Union
import xml.etree.ElementTree as ET
import pytest
from _pytest import nodes
from _pytest import timing
from _pytest._code.code import ExceptionRepr
@@ -32,6 +32,7 @@ from _pytest.fixtures import FixtureRequest
from _pytest.reports import TestReport
from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter
import pytest
xml_key = StashKey["LogXML"]()
@@ -248,7 +249,9 @@ class _NodeReporter:
skipreason = skipreason[9:]
details = f"{filename}:{lineno}: {skipreason}"
skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
skipped = ET.Element(
"skipped", type="pytest.skip", message=bin_xml_escape(skipreason)
)
skipped.text = bin_xml_escape(details)
self.append(skipped)
self.write_captured_output(report)
@@ -271,9 +274,7 @@ def _warn_incompatibility_with_xunit2(
if xml is not None and xml.family not in ("xunit1", "legacy"):
request.node.warn(
PytestWarning(
"{fixture_name} is incompatible with junit_family '{family}' (use 'legacy' or 'xunit1')".format(
fixture_name=fixture_name, family=xml.family
)
f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')"
)
)
@@ -365,7 +366,6 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
:issue:`7767` for details.
"""
__tracebackhide__ = True
def record_func(name: str, value: object) -> None:

View File

@@ -1,8 +1,9 @@
"""Add backward compatibility support for the legacy py path type."""
import dataclasses
from pathlib import Path
import shlex
import subprocess
from pathlib import Path
from typing import Final
from typing import final
from typing import List
@@ -32,6 +33,7 @@ from _pytest.pytester import RunResult
from _pytest.terminal import TerminalReporter
from _pytest.tmpdir import TempPathFactory
if TYPE_CHECKING:
import pexpect

View File

@@ -1,16 +1,17 @@
"""Access and control log capturing."""
import io
import logging
import os
import re
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import datetime
from datetime import timedelta
from datetime import timezone
import io
from io import StringIO
import logging
from logging import LogRecord
import os
from pathlib import Path
import re
from types import TracebackType
from typing import AbstractSet
from typing import Dict
@@ -43,6 +44,7 @@ from _pytest.main import Session
from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter
if TYPE_CHECKING:
logging_StreamHandler = logging.StreamHandler[StringIO]
else:
@@ -71,7 +73,8 @@ class DatetimeFormatter(logging.Formatter):
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
# Construct `datetime.datetime` object from `struct_time`
# and msecs information from `record`
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
# Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861).
dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz)
return dt.strftime(datefmt)
# Use `logging.Formatter` for non-microsecond formats
return super().formatTime(record, datefmt)
@@ -114,7 +117,6 @@ class ColoredLevelFormatter(DatetimeFormatter):
.. warning::
This is an experimental API.
"""
assert self._fmt is not None
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
if not levelname_fmt_match:
@@ -181,7 +183,6 @@ class PercentStyleMultiline(logging.PercentStyle):
0 (auto-indent turned off) or
>0 (explicitly set indentation position).
"""
if auto_indent_option is None:
return 0
elif isinstance(auto_indent_option, bool):
@@ -623,9 +624,9 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
except ValueError as e:
# Python logging does not recognise this as a logging level
raise UsageError(
"'{}' is not recognized as a logging level name for "
"'{}'. Please consider passing the "
"logging level num instead.".format(log_level, setting_name)
f"'{log_level}' is not recognized as a logging level name for "
f"'{setting_name}'. Please consider passing the "
"logging level num instead."
) from e

View File

@@ -1,12 +1,13 @@
"""Core implementation of the testing process: init, session, runtest loop."""
import argparse
import dataclasses
import fnmatch
import functools
import importlib
import os
import sys
from pathlib import Path
import sys
from typing import AbstractSet
from typing import Callable
from typing import Dict
@@ -21,11 +22,12 @@ from typing import overload
from typing import Sequence
from typing import Tuple
from typing import Union
import warnings
import pluggy
import _pytest._code
from _pytest import nodes
import _pytest._code
from _pytest.config import Config
from _pytest.config import directory_arg
from _pytest.config import ExitCode
@@ -45,6 +47,7 @@ from _pytest.reports import CollectReport
from _pytest.reports import TestReport
from _pytest.runner import collect_one_node
from _pytest.runner import SetupState
from _pytest.warning_types import PytestWarning
def pytest_addoption(parser: Parser) -> None:
@@ -550,8 +553,8 @@ class Session(nodes.Collector):
)
self.testsfailed = 0
self.testscollected = 0
self.shouldstop: Union[bool, str] = False
self.shouldfail: Union[bool, str] = False
self._shouldstop: Union[bool, str] = False
self._shouldfail: Union[bool, str] = False
self.trace = config.trace.root.get("collection")
self._initialpaths: FrozenSet[Path] = frozenset()
self._initialpaths_with_parents: FrozenSet[Path] = frozenset()
@@ -578,6 +581,42 @@ class Session(nodes.Collector):
self.testscollected,
)
@property
def shouldstop(self) -> Union[bool, str]:
return self._shouldstop
@shouldstop.setter
def shouldstop(self, value: Union[bool, str]) -> None:
# The runner checks shouldfail and assumes that if it is set we are
# definitely stopping, so prevent unsetting it.
if value is False and self._shouldstop:
warnings.warn(
PytestWarning(
"session.shouldstop cannot be unset after it has been set; ignoring."
),
stacklevel=2,
)
return
self._shouldstop = value
@property
def shouldfail(self) -> Union[bool, str]:
return self._shouldfail
@shouldfail.setter
def shouldfail(self, value: Union[bool, str]) -> None:
# The runner checks shouldfail and assumes that if it is set we are
# definitely stopping, so prevent unsetting it.
if value is False and self._shouldfail:
warnings.warn(
PytestWarning(
"session.shouldfail cannot be unset after it has been set; ignoring."
),
stacklevel=2,
)
return
self._shouldfail = value
@property
def startpath(self) -> Path:
"""The path from which pytest was invoked.
@@ -859,10 +898,14 @@ class Session(nodes.Collector):
# Prune this level.
any_matched_in_collector = False
for node in subnodes:
for node in reversed(subnodes):
# Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
if isinstance(matchparts[0], Path):
is_match = node.path == matchparts[0]
if sys.platform == "win32" and not is_match:
# In case the file paths do not match, fallback to samefile() to
# account for short-paths on Windows (#11895).
is_match = os.path.samefile(node.path, matchparts[0])
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
else:
# TODO: Remove parametrized workaround once collection structure contains

View File

@@ -1,4 +1,5 @@
"""Generic mechanism for marking and selecting python functions."""
import dataclasses
from typing import AbstractSet
from typing import Collection
@@ -23,6 +24,7 @@ from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
from _pytest.stash import StashKey
if TYPE_CHECKING:
from _pytest.nodes import Item
@@ -267,8 +269,8 @@ def pytest_configure(config: Config) -> None:
if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
raise UsageError(
"{!s} must be one of skip, xfail or fail_at_collect"
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)
f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect"
f" but it is {empty_parameterset!r}"
)

View File

@@ -14,6 +14,7 @@ The semantics are:
- ident evaluates to True of False according to a provided matcher function.
- or/and/not evaluate according to the usual boolean semantics.
"""
import ast
import dataclasses
import enum
@@ -26,6 +27,7 @@ from typing import NoReturn
from typing import Optional
from typing import Sequence
__all__ = [
"Expression",
"ParseError",

View File

@@ -1,7 +1,6 @@
import collections.abc
import dataclasses
import inspect
import warnings
from typing import Any
from typing import Callable
from typing import Collection
@@ -21,6 +20,7 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import warnings
from .._code import getfslineno
from ..compat import ascii_escaped
@@ -32,6 +32,7 @@ from _pytest.deprecated import MARKED_FIXTURE
from _pytest.outcomes import fail
from _pytest.warning_types import PytestUnknownMarkWarning
if TYPE_CHECKING:
from ..nodes import Node
@@ -111,7 +112,6 @@ class ParameterSet(NamedTuple):
Enforce tuple wrapping so single argument tuple values
don't get decomposed and break tests.
"""
if isinstance(parameterset, cls):
return parameterset
if force_tuple:
@@ -271,8 +271,8 @@ class MarkDecorator:
``MarkDecorators`` are created with ``pytest.mark``::
mark1 = pytest.mark.NAME # Simple MarkDecorator
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
mark1 = pytest.mark.NAME # Simple MarkDecorator
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
and can then be applied as decorators to test functions::
@@ -393,7 +393,7 @@ def get_unpacked_marks(
def normalize_mark_list(
mark_list: Iterable[Union[Mark, MarkDecorator]]
mark_list: Iterable[Union[Mark, MarkDecorator]],
) -> Iterable[Mark]:
"""
Normalize an iterable of Mark or MarkDecorator objects into a list of marks
@@ -503,9 +503,10 @@ class MarkGenerator:
import pytest
@pytest.mark.slowtest
def test_function():
pass
pass
applies a 'slowtest' :class:`Mark` on ``test_function``.
"""

View File

@@ -1,9 +1,9 @@
"""Monkeypatching and mocking functionality."""
from contextlib import contextmanager
import os
import re
import sys
import warnings
from contextlib import contextmanager
from typing import Any
from typing import final
from typing import Generator
@@ -15,10 +15,12 @@ from typing import overload
from typing import Tuple
from typing import TypeVar
from typing import Union
import warnings
from _pytest.fixtures import fixture
from _pytest.warning_types import PytestWarning
RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
@@ -89,9 +91,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object:
obj = getattr(obj, name)
except AttributeError as e:
raise AttributeError(
"{!r} object at {} has no attribute {!r}".format(
type(obj).__name__, ann, name
)
f"{type(obj).__name__!r} object at {ann} has no attribute {name!r}"
) from e
return obj
@@ -141,7 +141,6 @@ class MonkeyPatch:
which undoes any patching done inside the ``with`` block upon exit.
Example:
.. code-block:: python
import functools
@@ -321,10 +320,8 @@ class MonkeyPatch:
if not isinstance(value, str):
warnings.warn( # type: ignore[unreachable]
PytestWarning(
"Value of environment variable {name} type should be str, but got "
"{value!r} (type: {type}); converted to str implicitly".format(
name=name, value=value, type=type(value).__name__
)
f"Value of environment variable {name} type should be str, but got "
f"{value!r} (type: {type(value).__name__}); converted to str implicitly"
),
stacklevel=2,
)
@@ -344,7 +341,6 @@ class MonkeyPatch:
def syspath_prepend(self, path) -> None:
"""Prepend ``path`` to ``sys.path`` list of import locations."""
if self._savesyspath is None:
self._savesyspath = sys.path[:]
sys.path.insert(0, str(path))

View File

@@ -1,9 +1,8 @@
import abc
import os
import pathlib
import warnings
from functools import cached_property
from inspect import signature
import os
import pathlib
from pathlib import Path
from typing import Any
from typing import Callable
@@ -20,6 +19,7 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import warnings
import pluggy
@@ -43,10 +43,11 @@ from _pytest.pathlib import commonpath
from _pytest.stash import Stash
from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
# Imported here due to circular import.
from _pytest.main import Session
from _pytest._code.code import _TracebackStyle
from _pytest.main import Session
SEP = "/"
@@ -179,8 +180,8 @@ class Node(abc.ABC, metaclass=NodeMeta):
#: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
#: for methods not migrated to ``pathlib.Path`` yet, such as
#: :meth:`Item.reportinfo <pytest.Item.reportinfo>`. Will be deprecated in
#: a future release, prefer using :attr:`path` instead.
fspath: LEGACY_PATH
#: a future release, prefer using :attr:`path` instead.
# Use __slots__ to make attribute access faster.
# Note that __dict__ is still available.
@@ -306,9 +307,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
# enforce type checks here to avoid getting a generic type error later otherwise.
if not isinstance(warning, Warning):
raise ValueError(
"warning must be an instance of Warning or subclass, got {!r}".format(
warning
)
f"warning must be an instance of Warning or subclass, got {warning!r}"
)
path, lineno = get_fslocation_from_item(self)
assert lineno is not None

View File

@@ -1,4 +1,5 @@
"""Run testsuites written for nose."""
import warnings
from _pytest.config import hookimpl

View File

@@ -1,7 +1,7 @@
"""Exception classes and constants handling test outcomes as well as
functions creating them."""
import sys
import warnings
from typing import Any
from typing import Callable
from typing import cast
@@ -10,6 +10,7 @@ from typing import Optional
from typing import Protocol
from typing import Type
from typing import TypeVar
import warnings
from _pytest.deprecated import KEYWORD_MSG_ARG
@@ -296,8 +297,7 @@ def importorskip(
if verattr is None or Version(verattr) < Version(minversion):
raise Skipped(
"module %r has __version__ %r, required is: %r"
% (modname, verattr, minversion),
f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}",
allow_module_level=True,
)
return mod

View File

@@ -1,15 +1,16 @@
"""Submit failure or test session information to a pastebin service."""
import tempfile
from io import StringIO
import tempfile
from typing import IO
from typing import Union
import pytest
from _pytest.config import Config
from _pytest.config import create_terminal_writer
from _pytest.config.argparsing import Parser
from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter
import pytest
pastebinfile_key = StashKey[IO[bytes]]()
@@ -73,8 +74,8 @@ def create_new_paste(contents: Union[str, bytes]) -> str:
:returns: URL to the pasted contents, or an error message.
"""
import re
from urllib.request import urlopen
from urllib.parse import urlencode
from urllib.request import urlopen
params = {"code": contents, "lexer": "text", "expiry": "1week"}
url = "https://bpa.st"

View File

@@ -1,20 +1,15 @@
import atexit
import contextlib
import fnmatch
import importlib.util
import itertools
import os
import shutil
import sys
import types
import uuid
import warnings
from enum import Enum
from errno import EBADF
from errno import ELOOP
from errno import ENOENT
from errno import ENOTDIR
import fnmatch
from functools import partial
import importlib.util
import itertools
import os
from os.path import expanduser
from os.path import expandvars
from os.path import isabs
@@ -22,6 +17,9 @@ from os.path import sep
from pathlib import Path
from pathlib import PurePath
from posixpath import sep as posix_sep
import shutil
import sys
import types
from types import ModuleType
from typing import Callable
from typing import Dict
@@ -34,11 +32,14 @@ from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
import uuid
import warnings
from _pytest.compat import assert_never
from _pytest.outcomes import skip
from _pytest.warning_types import PytestWarning
LOCK_TIMEOUT = 60 * 60 * 24 * 3
@@ -101,9 +102,7 @@ def on_rm_rf_error(
if func not in (os.open,):
warnings.warn(
PytestWarning(
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
func, path, type(exc), exc
)
f"(rm_rf) unknown function {func} when removing {path}:\n{type(exc)}: {exc}"
)
)
return False
@@ -242,7 +241,7 @@ def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path:
else:
raise OSError(
"could not create numbered dir with prefix "
"{prefix} in {root} after 10 tries".format(prefix=prefix, root=root)
f"{prefix} in {root} after 10 tries"
)

View File

@@ -2,21 +2,22 @@
PYTEST_DONT_REWRITE
"""
import collections.abc
import contextlib
from fnmatch import fnmatch
import gc
import importlib
from io import StringIO
import locale
import os
from pathlib import Path
import platform
import re
import shutil
import subprocess
import sys
import traceback
from fnmatch import fnmatch
from io import StringIO
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Dict
@@ -69,6 +70,7 @@ from _pytest.reports import TestReport
from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
import pexpect
@@ -186,7 +188,7 @@ class LsofFdLeakChecker:
"*** After:",
*(str(f) for f in lines2),
"***** %s FD leakage detected" % len(leaked_files),
"*** function %s:%s: %s " % item.location,
"*** function {}:{}: {} ".format(*item.location),
"See issue #2366",
]
item.warn(PytestWarning("\n".join(error)))
@@ -375,14 +377,12 @@ class HookRecorder:
values.append(rep)
if not values:
raise ValueError(
"could not find test report matching %r: "
"no test reports at all!" % (inamepart,)
f"could not find test report matching {inamepart!r}: "
"no test reports at all!"
)
if len(values) > 1:
raise ValueError(
"found 2 or more testreports matching {!r}: {}".format(
inamepart, values
)
f"found 2 or more testreports matching {inamepart!r}: {values}"
)
return values[0]
@@ -807,7 +807,6 @@ class Pytester:
The first created file.
Examples:
.. code-block:: python
pytester.makefile(".txt", "line1", "line2")
@@ -861,7 +860,6 @@ class Pytester:
existing files.
Examples:
.. code-block:: python
def test_something(pytester):
@@ -881,7 +879,6 @@ class Pytester:
existing files.
Examples:
.. code-block:: python
def test_something(pytester):
@@ -1271,9 +1268,7 @@ class Pytester:
for item in items:
if item.name == funcname:
return item
assert 0, "{!r} item not found in module:\n{}\nitems: {}".format(
funcname, source, items
)
assert 0, f"{funcname!r} item not found in module:\n{source}\nitems: {items}"
def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]:
"""Return all test items collected from the module.
@@ -1432,10 +1427,7 @@ class Pytester:
def handle_timeout() -> None:
__tracebackhide__ = True
timeout_message = (
"{seconds} second timeout expired running:"
" {command}".format(seconds=timeout, command=cmdargs)
)
timeout_message = f"{timeout} second timeout expired running: {cmdargs}"
popen.kill()
popen.wait()

View File

@@ -1,4 +1,5 @@
"""Helper plugin for pytester; should not be loaded on its own."""
# This plugin contains assertions used by pytester. pytester cannot
# contain them itself, since it is imported by the `pytest` module,
# hence cannot be subject to assertion rewriting, which requires a

View File

@@ -1,18 +1,18 @@
"""Python test discovery, setup and run of test functions."""
import abc
from collections import Counter
from collections import defaultdict
import dataclasses
import enum
import fnmatch
from functools import partial
import inspect
import itertools
import os
from pathlib import Path
import sys
import types
import warnings
from collections import Counter
from collections import defaultdict
from functools import partial
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Dict
@@ -29,6 +29,7 @@ from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Union
import warnings
import _pytest
from _pytest import fixtures
@@ -267,8 +268,8 @@ def pytest_pycollect_makeitem(
elif getattr(obj, "__test__", True):
if is_generator(obj):
res: Function = Function.from_parent(collector, name=name)
reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
name=name
reason = (
f"yield tests were removed in pytest 4.0 - {name} will be ignored"
)
res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
res.warn(PytestCollectionWarning(reason))
@@ -560,10 +561,10 @@ def importtestmodule(
)
formatted_tb = str(exc_repr)
raise nodes.Collector.CollectError(
"ImportError while importing test module '{path}'.\n"
f"ImportError while importing test module '{path}'.\n"
"Hint: make sure your test modules/packages have valid Python names.\n"
"Traceback:\n"
"{traceback}".format(path=path, traceback=formatted_tb)
f"{formatted_tb}"
) from e
except skip.Exception as e:
if e.allow_module_level:
@@ -1496,17 +1497,13 @@ class Metafunc:
for arg in indirect:
if arg not in argnames:
fail(
"In {}: indirect fixture '{}' doesn't exist".format(
self.function.__name__, arg
),
f"In {self.function.__name__}: indirect fixture '{arg}' doesn't exist",
pytrace=False,
)
arg_directness[arg] = "indirect"
else:
fail(
"In {func}: expected Sequence or boolean for indirect, got {type}".format(
type=type(indirect).__name__, func=self.function.__name__
),
f"In {self.function.__name__}: expected Sequence or boolean for indirect, got {type(indirect).__name__}",
pytrace=False,
)
return arg_directness
@@ -1528,9 +1525,7 @@ class Metafunc:
if arg not in self.fixturenames:
if arg in default_arg_names:
fail(
"In {}: function already takes an argument '{}' with a default value".format(
func_name, arg
),
f"In {func_name}: function already takes an argument '{arg}' with a default value",
pytrace=False,
)
else:
@@ -1857,9 +1852,11 @@ class Function(PyobjMixin, nodes.Item):
if self.config.getoption("tbstyle", "auto") == "auto":
if len(ntraceback) > 2:
ntraceback = Traceback(
entry
if i == 0 or i == len(ntraceback) - 1
else entry.with_repr_style("short")
(
entry
if i == 0 or i == len(ntraceback) - 1
else entry.with_repr_style("short")
)
for i, entry in enumerate(ntraceback)
)

View File

@@ -1,9 +1,9 @@
import math
import pprint
from collections.abc import Collection
from collections.abc import Sized
from decimal import Decimal
import math
from numbers import Complex
import pprint
from types import TracebackType
from typing import Any
from typing import Callable
@@ -26,6 +26,7 @@ import _pytest._code
from _pytest.compat import STRING_TYPES
from _pytest.outcomes import fail
if TYPE_CHECKING:
from numpy import ndarray
@@ -237,9 +238,7 @@ class ApproxMapping(ApproxBase):
with numeric values (the keys can be anything)."""
def __repr__(self) -> str:
return "approx({!r})".format(
{k: self._approx_scalar(v) for k, v in self.expected.items()}
)
return f"approx({({k: self._approx_scalar(v) for k, v in self.expected.items()})!r})"
def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:
import math
@@ -314,9 +313,7 @@ class ApproxSequenceLike(ApproxBase):
seq_type = type(self.expected)
if seq_type not in (tuple, list):
seq_type = list
return "approx({!r})".format(
seq_type(self._approx_scalar(x) for x in self.expected)
)
return f"approx({seq_type(self._approx_scalar(x) for x in self.expected)!r})"
def _repr_compare(self, other_side: Sequence[float]) -> List[str]:
import math
@@ -696,7 +693,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
``approx`` falls back to strict equality for nonnumeric types instead
of raising ``TypeError``.
"""
# Delegate the comparison to a class that knows how to deal with the type
# of the expected value (e.g. int, float, list, dict, numpy.array, etc).
#
@@ -838,10 +834,10 @@ def raises( # noqa: F811
The ``match`` argument searches the formatted exception string, which includes any
`PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``:
>>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP
... e = ValueError("value must be 42")
... e.add_note("had a note added")
... raise e
>>> with pytest.raises(ValueError, match=r"had a note added"): # doctest: +SKIP
... e = ValueError("value must be 42")
... e.add_note("had a note added")
... raise e
The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
details of the captured exception::
@@ -856,7 +852,7 @@ def raises( # noqa: F811
Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this::
with pytest.raises(Exception): # Careful, this will catch ANY exception raised.
some_function()
some_function()
Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide
real bugs, where the user wrote this expecting a specific exception, but some other exception is being

View File

@@ -1,7 +1,7 @@
"""Record warnings during test function execution."""
import re
import warnings
from pprint import pformat
import re
from types import TracebackType
from typing import Any
from typing import Callable
@@ -16,10 +16,12 @@ from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
import warnings
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import WARNS_NONE_ARG
from _pytest.fixtures import fixture
from _pytest.outcomes import Exit
from _pytest.outcomes import fail
@@ -311,6 +313,17 @@ class WarningsChecker(WarningsRecorder):
# nothing to do in this deprecated case, see WARNS_NONE_ARG above
return
# BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within
# pytest.warns should *not* trigger "DID NOT WARN" and get suppressed
# when the warning doesn't happen. Control-flow exceptions should always
# propagate.
if exc_val is not None and (
not isinstance(exc_val, Exception)
# Exit is an Exception, not a BaseException, for some reason.
or isinstance(exc_val, Exit)
):
return
def found_str():
return pformat([record.message for record in self], indent=2)
@@ -331,10 +344,10 @@ class WarningsChecker(WarningsRecorder):
for w in self:
if not self.matches(w):
warnings.warn_explicit(
str(w.message),
w.message.__class__, # type: ignore[arg-type]
w.filename,
w.lineno,
message=w.message,
category=w.category,
filename=w.filename,
lineno=w.lineno,
module=w.__module__,
source=w.source,
)

View File

@@ -1,6 +1,6 @@
import dataclasses
import os
from io import StringIO
import os
from pprint import pprint
from typing import Any
from typing import cast
@@ -36,6 +36,7 @@ from _pytest.nodes import Collector
from _pytest.nodes import Item
from _pytest.outcomes import skip
if TYPE_CHECKING:
from _pytest.runner import CallInfo
@@ -45,7 +46,7 @@ def getworkerinfoline(node):
return node._workerinfocache
except AttributeError:
d = node.workerinfo
ver = "%s.%s.%s" % d["version_info"][:3]
ver = "{}.{}.{}".format(*d["version_info"][:3])
node._workerinfocache = s = "[{}] {} -- Python {} {}".format(
d["id"], d["sysplatform"], ver, d["executable"]
)
@@ -313,9 +314,7 @@ class TestReport(BaseReport):
self.__dict__.update(extra)
def __repr__(self) -> str:
return "<{} {!r} when={!r} outcome={!r}>".format(
self.__class__.__name__, self.nodeid, self.when, self.outcome
)
return f"<{self.__class__.__name__} {self.nodeid!r} when={self.when!r} outcome={self.outcome!r}>"
@classmethod
def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
@@ -430,9 +429,7 @@ class CollectReport(BaseReport):
return (self.fspath, None, self.fspath)
def __repr__(self) -> str:
return "<CollectReport {!r} lenresult={} outcome={!r}>".format(
self.nodeid, len(self.result), self.outcome
)
return f"<CollectReport {self.nodeid!r} lenresult={len(self.result)} outcome={self.outcome!r}>"
class CollectErrorRepr(TerminalRepr):
@@ -444,7 +441,7 @@ class CollectErrorRepr(TerminalRepr):
def pytest_report_to_serializable(
report: Union[CollectReport, TestReport]
report: Union[CollectReport, TestReport],
) -> Optional[Dict[str, Any]]:
if isinstance(report, (TestReport, CollectReport)):
data = report._to_json()
@@ -476,7 +473,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
"""
def serialize_repr_entry(
entry: Union[ReprEntry, ReprEntryNative]
entry: Union[ReprEntry, ReprEntryNative],
) -> Dict[str, Any]:
data = dataclasses.asdict(entry)
for key, value in data.items():

View File

@@ -1,4 +1,5 @@
"""Basic collect and runtest protocol implementations."""
import bdb
import dataclasses
import os
@@ -36,6 +37,7 @@ from _pytest.outcomes import OutcomeException
from _pytest.outcomes import Skipped
from _pytest.outcomes import TEST_OUTCOME
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
@@ -93,8 +95,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
if verbose < 2 and rep.duration < durations_min:
tr.write_line("")
tr.write_line(
"(%s durations < %gs hidden. Use -vv to show these durations.)"
% (len(dlist) - i, durations_min)
f"({len(dlist) - i} durations < {durations_min:g}s hidden. Use -vv to show these durations.)"
)
break
tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")

View File

@@ -7,6 +7,7 @@ would cause circular references.
Also this makes the module light to import, as it should.
"""
from enum import Enum
from functools import total_ordering
from typing import Literal

View File

@@ -2,7 +2,6 @@ from typing import Generator
from typing import Optional
from typing import Union
import pytest
from _pytest._io.saferepr import saferepr
from _pytest.config import Config
from _pytest.config import ExitCode
@@ -10,6 +9,7 @@ from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import SubRequest
from _pytest.scope import Scope
import pytest
def pytest_addoption(parser: Parser) -> None:
@@ -71,7 +71,7 @@ def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None:
scope_indent = list(reversed(Scope)).index(fixturedef._scope)
tw.write(" " * 2 * scope_indent)
tw.write(
"{step} {scope} {fixture}".format(
"{step} {scope} {fixture}".format( # noqa: UP032 (Readability)
step=msg.ljust(8), # align the output to TEARDOWN
scope=fixturedef.scope[0].upper(),
fixture=fixturedef.argname,

View File

@@ -1,12 +1,12 @@
from typing import Optional
from typing import Union
import pytest
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import SubRequest
import pytest
def pytest_addoption(parser: Parser) -> None:

View File

@@ -1,10 +1,11 @@
"""Support for skip/xfail functions and markers."""
from collections.abc import Mapping
import dataclasses
import os
import platform
import sys
import traceback
from collections.abc import Mapping
from typing import Generator
from typing import Optional
from typing import Tuple
@@ -104,9 +105,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
):
if not isinstance(dictionary, Mapping):
raise ValueError(
"pytest_markeval_namespace() needs to return a dict, got {!r}".format(
dictionary
)
f"pytest_markeval_namespace() needs to return a dict, got {dictionary!r}"
)
globals_.update(dictionary)
if hasattr(item, "obj"):

Some files were not shown because too many files have changed in this diff Show More