Merge branch 'refs/heads/main' into update_terminal_formatter
# Conflicts: # AUTHORS # src/_pytest/_io/terminalwriter.py
This commit is contained in:
commit
25e1bc0115
|
@ -19,6 +19,11 @@ jobs:
|
||||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
# Required by attest-build-provenance-github.
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
@ -26,7 +31,9 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v2.4.0
|
uses: hynek/build-and-inspect-python-package@v2.6.0
|
||||||
|
with:
|
||||||
|
attest-build-provenance-github: 'true'
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: github.repository == 'pytest-dev/pytest'
|
if: github.repository == 'pytest-dev/pytest'
|
||||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v2.4.0
|
uses: hynek/build-and-inspect-python-package@v2.6.0
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [package]
|
needs: [package]
|
||||||
|
@ -55,6 +55,7 @@ jobs:
|
||||||
"windows-py310",
|
"windows-py310",
|
||||||
"windows-py311",
|
"windows-py311",
|
||||||
"windows-py312",
|
"windows-py312",
|
||||||
|
"windows-py313",
|
||||||
|
|
||||||
"ubuntu-py38",
|
"ubuntu-py38",
|
||||||
"ubuntu-py38-pluggy",
|
"ubuntu-py38-pluggy",
|
||||||
|
@ -63,12 +64,14 @@ jobs:
|
||||||
"ubuntu-py310",
|
"ubuntu-py310",
|
||||||
"ubuntu-py311",
|
"ubuntu-py311",
|
||||||
"ubuntu-py312",
|
"ubuntu-py312",
|
||||||
|
"ubuntu-py313",
|
||||||
"ubuntu-pypy3",
|
"ubuntu-pypy3",
|
||||||
|
|
||||||
"macos-py38",
|
"macos-py38",
|
||||||
"macos-py39",
|
"macos-py39",
|
||||||
"macos-py310",
|
"macos-py310",
|
||||||
"macos-py312",
|
"macos-py312",
|
||||||
|
"macos-py313",
|
||||||
|
|
||||||
"doctesting",
|
"doctesting",
|
||||||
"plugins",
|
"plugins",
|
||||||
|
@ -97,9 +100,13 @@ jobs:
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py311"
|
tox_env: "py311"
|
||||||
- name: "windows-py312"
|
- name: "windows-py312"
|
||||||
python: "3.12-dev"
|
python: "3.12"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py312"
|
tox_env: "py312"
|
||||||
|
- name: "windows-py313"
|
||||||
|
python: "3.13-dev"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py313"
|
||||||
|
|
||||||
- name: "ubuntu-py38"
|
- name: "ubuntu-py38"
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
|
@ -128,10 +135,15 @@ jobs:
|
||||||
tox_env: "py311"
|
tox_env: "py311"
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
- name: "ubuntu-py312"
|
- name: "ubuntu-py312"
|
||||||
python: "3.12-dev"
|
python: "3.12"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py312"
|
tox_env: "py312"
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py313"
|
||||||
|
python: "3.13-dev"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py313"
|
||||||
|
use_coverage: true
|
||||||
- name: "ubuntu-pypy3"
|
- name: "ubuntu-pypy3"
|
||||||
python: "pypy-3.8"
|
python: "pypy-3.8"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -151,9 +163,13 @@ jobs:
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
- name: "macos-py312"
|
- name: "macos-py312"
|
||||||
python: "3.12-dev"
|
python: "3.12"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py312-xdist"
|
tox_env: "py312-xdist"
|
||||||
|
- name: "macos-py313"
|
||||||
|
python: "3.13-dev"
|
||||||
|
os: macos-latest
|
||||||
|
tox_env: "py313-xdist"
|
||||||
|
|
||||||
- name: "plugins"
|
- name: "plugins"
|
||||||
python: "3.12"
|
python: "3.12"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: "v0.4.3"
|
rev: "v0.4.8"
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: ["--fix"]
|
args: ["--fix"]
|
||||||
|
@ -38,11 +38,16 @@ repos:
|
||||||
# on <3.11
|
# on <3.11
|
||||||
- exceptiongroup>=1.0.0rc8
|
- exceptiongroup>=1.0.0rc8
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: "1.8.0"
|
rev: "2.1.3"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
|
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
|
||||||
additional_dependencies: ["tox>=4.9"]
|
additional_dependencies: ["tox>=4.9"]
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v3.16.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
stages: [manual]
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: pylint
|
- id: pylint
|
||||||
|
|
6
AUTHORS
6
AUTHORS
|
@ -193,6 +193,7 @@ Jakob van Santen
|
||||||
Jakub Mitoraj
|
Jakub Mitoraj
|
||||||
James Bourbeau
|
James Bourbeau
|
||||||
James Hutchins
|
James Hutchins
|
||||||
|
James Frost
|
||||||
Jan Balster
|
Jan Balster
|
||||||
Janne Vanhala
|
Janne Vanhala
|
||||||
Jason R. Coombs
|
Jason R. Coombs
|
||||||
|
@ -259,6 +260,7 @@ Marc Bresson
|
||||||
Marco Gorelli
|
Marco Gorelli
|
||||||
Mark Abramowitz
|
Mark Abramowitz
|
||||||
Mark Dickinson
|
Mark Dickinson
|
||||||
|
Mark Vong
|
||||||
Marko Pacak
|
Marko Pacak
|
||||||
Markus Unterwaditzer
|
Markus Unterwaditzer
|
||||||
Martijn Faassen
|
Martijn Faassen
|
||||||
|
@ -279,6 +281,7 @@ Michael Droettboom
|
||||||
Michael Goerz
|
Michael Goerz
|
||||||
Michael Krebs
|
Michael Krebs
|
||||||
Michael Seifert
|
Michael Seifert
|
||||||
|
Michael Vogt
|
||||||
Michal Wajszczuk
|
Michal Wajszczuk
|
||||||
Michał Górny
|
Michał Górny
|
||||||
Michał Zięba
|
Michał Zięba
|
||||||
|
@ -290,6 +293,7 @@ Mike Lundy
|
||||||
Milan Lesnek
|
Milan Lesnek
|
||||||
Miro Hrončok
|
Miro Hrončok
|
||||||
mrbean-bremen
|
mrbean-bremen
|
||||||
|
Nathan Goldbaum
|
||||||
Nathaniel Compton
|
Nathaniel Compton
|
||||||
Nathaniel Waisbrot
|
Nathaniel Waisbrot
|
||||||
Ned Batchelder
|
Ned Batchelder
|
||||||
|
@ -299,6 +303,7 @@ Nicholas Devenish
|
||||||
Nicholas Murphy
|
Nicholas Murphy
|
||||||
Niclas Olofsson
|
Niclas Olofsson
|
||||||
Nicolas Delaby
|
Nicolas Delaby
|
||||||
|
Nico Vidal
|
||||||
Nikolay Kondratyev
|
Nikolay Kondratyev
|
||||||
Nipunn Koorapati
|
Nipunn Koorapati
|
||||||
Oleg Pidsadnyi
|
Oleg Pidsadnyi
|
||||||
|
@ -359,6 +364,7 @@ Sadra Barikbin
|
||||||
Saiprasad Kale
|
Saiprasad Kale
|
||||||
Samuel Colvin
|
Samuel Colvin
|
||||||
Samuel Dion-Girardeau
|
Samuel Dion-Girardeau
|
||||||
|
Samuel Jirovec
|
||||||
Samuel Searles-Bryant
|
Samuel Searles-Bryant
|
||||||
Samuel Therrien (Avasam)
|
Samuel Therrien (Avasam)
|
||||||
Samuele Pedroni
|
Samuele Pedroni
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
============================
|
============================
|
||||||
Contribution getting started
|
Contributing
|
||||||
============================
|
============================
|
||||||
|
|
||||||
Contributions are highly welcomed and appreciated. Every little bit of help counts,
|
Contributions are highly welcomed and appreciated. Every little bit of help counts,
|
||||||
so do not hesitate!
|
so do not hesitate!
|
||||||
|
|
||||||
.. contents::
|
|
||||||
:depth: 2
|
|
||||||
:backlinks: none
|
|
||||||
|
|
||||||
|
|
||||||
.. _submitfeedback:
|
.. _submitfeedback:
|
||||||
|
|
||||||
|
@ -128,7 +124,7 @@ For example:
|
||||||
Submitting Plugins to pytest-dev
|
Submitting Plugins to pytest-dev
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
Pytest development of the core, some plugins and support code happens
|
Development of the pytest core, support code, and some plugins happens
|
||||||
in repositories living under the ``pytest-dev`` organisations:
|
in repositories living under the ``pytest-dev`` organisations:
|
||||||
|
|
||||||
- `pytest-dev on GitHub <https://github.com/pytest-dev>`_
|
- `pytest-dev on GitHub <https://github.com/pytest-dev>`_
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Keyboard interrupts and system exits are now properly handled during the test collection.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fix collection error upon encountering an :mod:`abstract <abc>` class, including abstract `unittest.TestCase` subclasses.
|
|
@ -1 +0,0 @@
|
||||||
Fixed handling of 'Function not implemented' error under squashfuse_ll, which is a different way to say that the mountpoint is read-only.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fix a regression in pytest 8.0.0 where package-scoped parameterized items were not correctly reordered to minimize setups/teardowns in some cases.
|
|
@ -0,0 +1 @@
|
||||||
|
Fix crash with `assert testcase is not None` assertion failure when re-running unittest tests using plugins like pytest-rerunfailures. Regressed in 8.2.2.
|
|
@ -0,0 +1 @@
|
||||||
|
Do not truncate arguments to functions in output when running with `-vvv`.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
@ -0,0 +1,7 @@
|
||||||
|
<style>
|
||||||
|
.logo {text-align: center;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<a class="logo" href="{{ pathto('contents') }}">
|
||||||
|
<img src="{{ pathto('_static/pytest1.png', 1) }}" width="70%" height="70%" text="Pytest Logo"/>
|
||||||
|
</a>
|
|
@ -1,14 +0,0 @@
|
||||||
{#
|
|
||||||
basic/searchbox.html with heading removed.
|
|
||||||
#}
|
|
||||||
{%- if pagename != "search" and builder != "singlehtml" %}
|
|
||||||
<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" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
|
||||||
<input type="submit" value="{{ _('Go') }}" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
|
||||||
{%- endif %}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<style>
|
||||||
|
ul {list-style: none;}
|
||||||
|
li {margin: 0.4em 0;}
|
||||||
|
@media (min-width: 46em) {
|
||||||
|
#features {width: 50%;}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,6 +6,8 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-8.2.2
|
||||||
|
release-8.2.1
|
||||||
release-8.2.0
|
release-8.2.0
|
||||||
release-8.1.2
|
release-8.1.2
|
||||||
release-8.1.1
|
release-8.1.1
|
||||||
|
|
|
@ -62,7 +62,7 @@ New Features
|
||||||
- new "-q" option which decreases verbosity and prints a more
|
- new "-q" option which decreases verbosity and prints a more
|
||||||
nose/unittest-style "dot" output.
|
nose/unittest-style "dot" output.
|
||||||
|
|
||||||
- many many more detailed improvements details
|
- many, many, more detailed improvements details
|
||||||
|
|
||||||
Fixes
|
Fixes
|
||||||
-----------------------
|
-----------------------
|
||||||
|
@ -109,7 +109,7 @@ Important Notes
|
||||||
in conftest.py files. They will cause nothing special.
|
in conftest.py files. They will cause nothing special.
|
||||||
- removed support for calling the pre-1.0 collection API of "run()" and "join"
|
- removed support for calling the pre-1.0 collection API of "run()" and "join"
|
||||||
- removed reading option values from conftest.py files or env variables.
|
- removed reading option values from conftest.py files or env variables.
|
||||||
This can now be done much much better and easier through the ini-file
|
This can now be done much, much, better and easier through the ini-file
|
||||||
mechanism and the "addopts" entry in particular.
|
mechanism and the "addopts" entry in particular.
|
||||||
- removed the "disabled" attribute in test classes. Use the skipping
|
- removed the "disabled" attribute in test classes. Use the skipping
|
||||||
and pytestmark mechanism to skip or xfail a test class.
|
and pytestmark mechanism to skip or xfail a test class.
|
||||||
|
|
|
@ -4,7 +4,7 @@ pytest-2.2.2: bug fixes
|
||||||
pytest-2.2.2 (updated to 2.2.3 to fix packaging issues) is a minor
|
pytest-2.2.2 (updated to 2.2.3 to fix packaging issues) is a minor
|
||||||
backward-compatible release of the versatile py.test testing tool. It
|
backward-compatible release of the versatile py.test testing tool. It
|
||||||
contains bug fixes and a few refinements particularly to reporting with
|
contains bug fixes and a few refinements particularly to reporting with
|
||||||
"--collectonly", see below for betails.
|
"--collectonly", see below for details.
|
||||||
|
|
||||||
For general information see here:
|
For general information see here:
|
||||||
|
|
||||||
|
|
|
@ -181,7 +181,7 @@ Bug fixes:
|
||||||
partially failed (finalizers would not always be called before)
|
partially failed (finalizers would not always be called before)
|
||||||
|
|
||||||
- fix issue320 - fix class scope for fixtures when mixed with
|
- fix issue320 - fix class scope for fixtures when mixed with
|
||||||
module-level functions. Thanks Anatloy Bubenkoff.
|
module-level functions. Thanks Anatoly Bubenkoff.
|
||||||
|
|
||||||
- you can specify "-q" or "-qq" to get different levels of "quieter"
|
- you can specify "-q" or "-qq" to get different levels of "quieter"
|
||||||
reporting (thanks Katarzyna Jachim)
|
reporting (thanks Katarzyna Jachim)
|
||||||
|
|
|
@ -83,7 +83,7 @@ holger krekel
|
||||||
Thanks Ralph Schmitt for the precise failure example.
|
Thanks Ralph Schmitt for the precise failure example.
|
||||||
|
|
||||||
- fix issue244 by implementing special index for parameters to only use
|
- fix issue244 by implementing special index for parameters to only use
|
||||||
indices for paramentrized test ids
|
indices for parametrized test ids
|
||||||
|
|
||||||
- fix issue287 by running all finalizers but saving the exception
|
- fix issue287 by running all finalizers but saving the exception
|
||||||
from the first failing finalizer and re-raising it so teardown will
|
from the first failing finalizer and re-raising it so teardown will
|
||||||
|
|
|
@ -73,7 +73,7 @@ holger krekel
|
||||||
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
|
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
|
||||||
Gospodnetic for the PR.
|
Gospodnetic for the PR.
|
||||||
|
|
||||||
- change XPASS colour to yellow rather then red when tests are run
|
- change XPASS colour to yellow rather than red when tests are run
|
||||||
with -v.
|
with -v.
|
||||||
|
|
||||||
- fix issue473: work around mock putting an unbound method into a class
|
- fix issue473: work around mock putting an unbound method into a class
|
||||||
|
|
|
@ -55,7 +55,7 @@ holger krekel
|
||||||
github. See https://pytest.org/en/stable/contributing.html .
|
github. See https://pytest.org/en/stable/contributing.html .
|
||||||
Thanks to Anatoly for pushing and initial work on this.
|
Thanks to Anatoly for pushing and initial work on this.
|
||||||
|
|
||||||
- fix issue650: new option ``--docttest-ignore-import-errors`` which
|
- fix issue650: new option ``--doctest-ignore-import-errors`` which
|
||||||
will turn import errors in doctests into skips. Thanks Charles Cloud
|
will turn import errors in doctests into skips. Thanks Charles Cloud
|
||||||
for the complete PR.
|
for the complete PR.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
pytest-8.2.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 8.2.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
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,19 @@
|
||||||
|
pytest-8.2.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 8.2.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:
|
||||||
|
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -49,7 +49,7 @@ place on 20th, 21st, 22nd, 24th and 25th. On the 23rd we took a break
|
||||||
day for some hot hiking in the Black Forest.
|
day for some hot hiking in the Black Forest.
|
||||||
|
|
||||||
Sprint activity was organised heavily around pairing, with plenty of group
|
Sprint activity was organised heavily around pairing, with plenty of group
|
||||||
discusssions to take advantage of the high bandwidth, and lightning talks
|
discussions to take advantage of the high bandwidth, and lightning talks
|
||||||
as well.
|
as well.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,30 +5,26 @@ Backwards Compatibility Policy
|
||||||
|
|
||||||
.. versionadded: 6.0
|
.. versionadded: 6.0
|
||||||
|
|
||||||
pytest is actively evolving and is a project that has been decades in the making,
|
Pytest is an actively evolving project that has been decades in the making.
|
||||||
we keep learning about new and better structures to express different details about testing.
|
We keep learning about new and better structures to express different details about testing.
|
||||||
|
|
||||||
While we implement those modifications we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors.
|
While we implement those modifications, we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors.
|
||||||
|
|
||||||
As of now, pytest considers multiple types of backward compatibility transitions:
|
As of now, pytest considers multiple types of backward compatibility transitions:
|
||||||
|
|
||||||
a) trivial: APIs which trivially translate to the new mechanism,
|
a) trivial: APIs that trivially translate to the new mechanism and do not cause problematic changes.
|
||||||
and do not cause problematic changes.
|
|
||||||
|
|
||||||
We try to support those indefinitely while encouraging users to switch to newer/better mechanisms through documentation.
|
We try to support those indefinitely while encouraging users to switch to newer or better mechanisms through documentation.
|
||||||
|
|
||||||
b) transitional: the old and new API don't conflict
|
b) transitional: the old and new APIs don't conflict, and we can help users transition by using warnings while supporting both for a prolonged period of time.
|
||||||
and we can help users transition by using warnings, while supporting both for a prolonged time.
|
|
||||||
|
|
||||||
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
We will only start the removal of deprecated functionality in major releases (e.g., if we deprecate something in 3.0, we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g., if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
||||||
|
|
||||||
A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationWarning`).
|
A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationWarning`).
|
||||||
|
|
||||||
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
When the deprecation expires (e.g., 4.0 is released), we won't remove the deprecated functionality immediately but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g., `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g., 4.1), the feature will be effectively removed.
|
||||||
|
|
||||||
|
c) True breakage should only be considered when a normal transition is unreasonably unsustainable and would offset important developments or features by years. In addition, they should be limited to APIs where the number of actual users is very small (for example, only impacting some plugins) and can be coordinated with the community in advance.
|
||||||
c) true breakage: should only be considered when normal transition is unreasonably unsustainable and would offset important development/features by years.
|
|
||||||
In addition, they should be limited to APIs where the number of actual users is very small (for example only impacting some plugins), and can be coordinated with the community in advance.
|
|
||||||
|
|
||||||
Examples for such upcoming changes:
|
Examples for such upcoming changes:
|
||||||
|
|
||||||
|
@ -62,11 +58,11 @@ Focus primary on smooth transition - stance (pre 6.0)
|
||||||
|
|
||||||
Keeping backwards compatibility has a very high priority in the pytest project. Although we have deprecated functionality over the years, most of it is still supported. All deprecations in pytest were done because simpler or more efficient ways of accomplishing the same tasks have emerged, making the old way of doing things unnecessary.
|
Keeping backwards compatibility has a very high priority in the pytest project. Although we have deprecated functionality over the years, most of it is still supported. All deprecations in pytest were done because simpler or more efficient ways of accomplishing the same tasks have emerged, making the old way of doing things unnecessary.
|
||||||
|
|
||||||
With the pytest 3.0 release we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around.
|
With the pytest 3.0 release, we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around.
|
||||||
|
|
||||||
To communicate changes we issue deprecation warnings using a custom warning hierarchy (see :ref:`internal-warnings`). These warnings may be suppressed using the standard means: ``-W`` command-line flag or ``filterwarnings`` ini options (see :ref:`warnings`), but we suggest to use these sparingly and temporarily, and heed the warnings when possible.
|
To communicate changes, we issue deprecation warnings using a custom warning hierarchy (see :ref:`internal-warnings`). These warnings may be suppressed using the standard means: ``-W`` command-line flag or ``filterwarnings`` ini options (see :ref:`warnings`), but we suggest to use these sparingly and temporarily, and heed the warnings when possible.
|
||||||
|
|
||||||
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0, we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
||||||
|
|
||||||
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
cachedir: .pytest_cache
|
cachedir: .pytest_cache
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 0 items
|
collected 0 items
|
||||||
cache -- .../_pytest/cacheprovider.py:542
|
cache -- .../_pytest/cacheprovider.py:560
|
||||||
Return a cache object that can persist state between testing sessions.
|
Return a cache object that can persist state between testing sessions.
|
||||||
|
|
||||||
cache.get(key, default)
|
cache.get(key, default)
|
||||||
|
@ -115,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
|
|
||||||
For more details: :ref:`doctest_namespace`.
|
For more details: :ref:`doctest_namespace`.
|
||||||
|
|
||||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1335
|
pytestconfig [session scope] -- .../_pytest/fixtures.py:1338
|
||||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||||
object.
|
object.
|
||||||
|
|
||||||
|
|
|
@ -28,9 +28,88 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 8.2.2 (2024-06-04)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#12355 <https://github.com/pytest-dev/pytest/issues/12355>`_: Fix possible catastrophic performance slowdown on a certain parametrization pattern involving many higher-scoped parameters.
|
||||||
|
|
||||||
|
|
||||||
|
- `#12367 <https://github.com/pytest-dev/pytest/issues/12367>`_: Fix a regression in pytest 8.2.0 where unittest class instances (a fresh one is created for each test) were not released promptly on test teardown but only on session teardown.
|
||||||
|
|
||||||
|
|
||||||
|
- `#12381 <https://github.com/pytest-dev/pytest/issues/12381>`_: Fix possible "Directory not empty" crashes arising from concurent cache dir (``.pytest_cache``) creation. Regressed in pytest 8.2.0.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#12290 <https://github.com/pytest-dev/pytest/issues/12290>`_: Updated Sphinx theme to use Furo instead of Flask, enabling Dark mode theme.
|
||||||
|
|
||||||
|
|
||||||
|
- `#12356 <https://github.com/pytest-dev/pytest/issues/12356>`_: Added a subsection to the documentation for debugging flaky tests to mention
|
||||||
|
lack of thread safety in pytest as a possible source of flakyness.
|
||||||
|
|
||||||
|
|
||||||
|
- `#12363 <https://github.com/pytest-dev/pytest/issues/12363>`_: The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 8.2.1 (2024-05-19)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#12334 <https://github.com/pytest-dev/pytest/issues/12334>`_: Support for Python 3.13 (beta1 at the time of writing).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#12120 <https://github.com/pytest-dev/pytest/issues/12120>`_: Fix `PermissionError` crashes arising from directories which are not selected on the command-line.
|
||||||
|
|
||||||
|
|
||||||
|
- `#12191 <https://github.com/pytest-dev/pytest/issues/12191>`_: Keyboard interrupts and system exits are now properly handled during the test collection.
|
||||||
|
|
||||||
|
|
||||||
|
- `#12300 <https://github.com/pytest-dev/pytest/issues/12300>`_: Fixed handling of 'Function not implemented' error under squashfuse_ll, which is a different way to say that the mountpoint is read-only.
|
||||||
|
|
||||||
|
|
||||||
|
- `#12308 <https://github.com/pytest-dev/pytest/issues/12308>`_: Fix a regression in pytest 8.2.0 where the permissions of automatically-created ``.pytest_cache`` directories became ``rwx------`` instead of the expected ``rwxr-xr-x``.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#12333 <https://github.com/pytest-dev/pytest/issues/12333>`_: pytest releases are now attested using the recent `Artifact Attestation <https://github.blog/2024-05-02-introducing-artifact-attestations-now-in-public-beta/>`_ support from GitHub, allowing users to verify the provenance of pytest's sdist and wheel artifacts.
|
||||||
|
|
||||||
|
|
||||||
pytest 8.2.0 (2024-04-27)
|
pytest 8.2.0 (2024-04-27)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
Breaking Changes
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- `#12089 <https://github.com/pytest-dev/pytest/pull/12089>`_: pytest now requires that :class:`unittest.TestCase` subclasses can be instantiated freely using ``MyTestCase('runTest')``.
|
||||||
|
|
||||||
|
If the class doesn't allow this, you may see an error during collection such as ``AttributeError: 'MyTestCase' object has no attribute 'runTest'``.
|
||||||
|
|
||||||
|
Classes which do not override ``__init__``, or do not access the test method in ``__init__`` using ``getattr`` or similar, are unaffected.
|
||||||
|
|
||||||
|
Classes which do should take care to not crash when ``"runTest"`` is given, as is shown in `unittest.TestCases's implementation <https://github.com/python/cpython/blob/51aefc5bf907ddffaaf083ded0de773adcdf08c8/Lib/unittest/case.py#L419-L426>`_.
|
||||||
|
Alternatively, consider using :meth:`setUp <unittest.TestCase.setUp>` instead of ``__init__``.
|
||||||
|
|
||||||
|
If you run into this issue using ``tornado.AsyncTestCase``, please see `issue 12263 <https://github.com/pytest-dev/pytest/issues/12263>`_.
|
||||||
|
|
||||||
|
If you run into this issue using an abstract ``TestCase`` subclass, please see `issue 12275 <https://github.com/pytest-dev/pytest/issues/12275>`_.
|
||||||
|
|
||||||
|
Historical note: the effect of this change on custom TestCase implementations was not properly considered initially, this is why it was done in a minor release. We apologize for the inconvenience.
|
||||||
|
|
||||||
Deprecations
|
Deprecations
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -150,7 +229,7 @@ Improvements
|
||||||
- `#11311 <https://github.com/pytest-dev/pytest/issues/11311>`_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
|
- `#11311 <https://github.com/pytest-dev/pytest/issues/11311>`_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
|
||||||
as the relative directory.
|
as the relative directory.
|
||||||
|
|
||||||
Previoulsy this would raise an :class:`AssertionError`.
|
Previously this would raise an :class:`AssertionError`.
|
||||||
|
|
||||||
|
|
||||||
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: :ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
|
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: :ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
|
||||||
|
@ -1393,7 +1472,7 @@ Deprecations
|
||||||
``__init__`` method, they should take ``**kwargs``. See
|
``__init__`` method, they should take ``**kwargs``. See
|
||||||
:ref:`uncooperative-constructors-deprecated` for details.
|
:ref:`uncooperative-constructors-deprecated` for details.
|
||||||
|
|
||||||
Note that a deprection warning is only emitted when there is a conflict in the
|
Note that a deprecation warning is only emitted when there is a conflict in the
|
||||||
arguments pytest expected to pass. This deprecation was already part of pytest
|
arguments pytest expected to pass. This deprecation was already part of pytest
|
||||||
7.0.0rc1 but wasn't documented.
|
7.0.0rc1 but wasn't documented.
|
||||||
|
|
||||||
|
@ -1435,7 +1514,7 @@ Breaking Changes
|
||||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||||
|
|
||||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||||
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
|
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffected.
|
||||||
|
|
||||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||||
|
@ -1943,7 +2022,7 @@ Bug Fixes
|
||||||
the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with
|
the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with
|
||||||
private permissions.
|
private permissions.
|
||||||
|
|
||||||
pytest used to silently use a pre-existing ``/tmp/pytest-of-<username>`` directory,
|
pytest used to silently use a preexisting ``/tmp/pytest-of-<username>`` directory,
|
||||||
even if owned by another user. This means another user could pre-create such a
|
even if owned by another user. This means another user could pre-create such a
|
||||||
directory and gain control of another user's temporary directory. Now such a
|
directory and gain control of another user's temporary directory. Now such a
|
||||||
condition results in an error.
|
condition results in an error.
|
||||||
|
@ -2670,7 +2749,7 @@ Features
|
||||||
also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules
|
also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules
|
||||||
that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``).
|
that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``).
|
||||||
|
|
||||||
``--import-mode=importlib`` uses more fine grained import mechanisms from ``importlib`` which don't
|
``--import-mode=importlib`` uses more fine-grained import mechanisms from ``importlib`` which don't
|
||||||
require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks
|
require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks
|
||||||
of the previous mode.
|
of the previous mode.
|
||||||
|
|
||||||
|
@ -2687,7 +2766,7 @@ Improvements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- :issue:`4375`: The ``pytest`` command now suppresses the ``BrokenPipeError`` error message that
|
- :issue:`4375`: The ``pytest`` command now suppresses the ``BrokenPipeError`` error message that
|
||||||
is printed to stderr when the output of ``pytest`` is piped and and the pipe is
|
is printed to stderr when the output of ``pytest`` is piped and the pipe is
|
||||||
closed by the piped-to program (common examples are ``less`` and ``head``).
|
closed by the piped-to program (common examples are ``less`` and ``head``).
|
||||||
|
|
||||||
|
|
||||||
|
@ -2989,7 +3068,7 @@ Breaking Changes
|
||||||
This hook has been marked as deprecated and not been even called by pytest for over 10 years now.
|
This hook has been marked as deprecated and not been even called by pytest for over 10 years now.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that sth. expected is missing in the result and "+" means that there are unexpected extras in the result.
|
- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that something expected is missing in the result and "+" means that there are unexpected extras in the result.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6737`: The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when
|
- :issue:`6737`: The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when
|
||||||
|
@ -4594,7 +4673,7 @@ Bug Fixes
|
||||||
Improved Documentation
|
Improved Documentation
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability liminations
|
- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability limitations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -6452,7 +6531,7 @@ Features
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
---------
|
---------
|
||||||
|
|
||||||
- Fix hanging pexpect test on MacOS by using flush() instead of wait().
|
- Fix hanging pexpect test on macOS by using flush() instead of wait().
|
||||||
(:issue:`2022`)
|
(:issue:`2022`)
|
||||||
|
|
||||||
- Fix restoring Python state after in-process pytest runs with the
|
- Fix restoring Python state after in-process pytest runs with the
|
||||||
|
@ -6500,7 +6579,7 @@ Trivial/Internal Changes
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
- Show a simple and easy error when keyword expressions trigger a syntax error
|
- Show a simple and easy error when keyword expressions trigger a syntax error
|
||||||
(for example, ``"-k foo and import"`` will show an error that you can not use
|
(for example, ``"-k foo and import"`` will show an error that you cannot use
|
||||||
the ``import`` keyword in expressions). (:issue:`2953`)
|
the ``import`` keyword in expressions). (:issue:`2953`)
|
||||||
|
|
||||||
- Change parametrized automatic test id generation to use the ``__name__``
|
- Change parametrized automatic test id generation to use the ``__name__``
|
||||||
|
@ -8276,7 +8355,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||||
one will also have a "reprec" attribute with the recorded events/reports.
|
one will also have a "reprec" attribute with the recorded events/reports.
|
||||||
|
|
||||||
- fix monkeypatch.setattr("x.y", raising=False) to actually not raise
|
- fix monkeypatch.setattr("x.y", raising=False) to actually not raise
|
||||||
if "y" is not a pre-existing attribute. Thanks Florian Bruhin.
|
if "y" is not a preexisting attribute. Thanks Florian Bruhin.
|
||||||
|
|
||||||
- fix issue741: make running output from testdir.run copy/pasteable
|
- fix issue741: make running output from testdir.run copy/pasteable
|
||||||
Thanks Bruno Oliveira.
|
Thanks Bruno Oliveira.
|
||||||
|
@ -8332,7 +8411,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||||
|
|
||||||
- fix issue854: autouse yield_fixtures defined as class members of
|
- fix issue854: autouse yield_fixtures defined as class members of
|
||||||
unittest.TestCase subclasses now work as expected.
|
unittest.TestCase subclasses now work as expected.
|
||||||
Thannks xmo-odoo for the report and Bruno Oliveira for the PR.
|
Thanks xmo-odoo for the report and Bruno Oliveira for the PR.
|
||||||
|
|
||||||
- fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the
|
- fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the
|
||||||
fixtures declared on the first one.
|
fixtures declared on the first one.
|
||||||
|
@ -8436,7 +8515,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||||
github. See https://pytest.org/en/stable/contributing.html .
|
github. See https://pytest.org/en/stable/contributing.html .
|
||||||
Thanks to Anatoly for pushing and initial work on this.
|
Thanks to Anatoly for pushing and initial work on this.
|
||||||
|
|
||||||
- fix issue650: new option ``--docttest-ignore-import-errors`` which
|
- fix issue650: new option ``--doctest-ignore-import-errors`` which
|
||||||
will turn import errors in doctests into skips. Thanks Charles Cloud
|
will turn import errors in doctests into skips. Thanks Charles Cloud
|
||||||
for the complete PR.
|
for the complete PR.
|
||||||
|
|
||||||
|
@ -8624,7 +8703,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||||
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
|
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
|
||||||
Gospodnetic for the PR.
|
Gospodnetic for the PR.
|
||||||
|
|
||||||
- change XPASS colour to yellow rather then red when tests are run
|
- change XPASS colour to yellow rather than red when tests are run
|
||||||
with -v.
|
with -v.
|
||||||
|
|
||||||
- fix issue473: work around mock putting an unbound method into a class
|
- fix issue473: work around mock putting an unbound method into a class
|
||||||
|
@ -8797,7 +8876,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||||
Thanks Ralph Schmitt for the precise failure example.
|
Thanks Ralph Schmitt for the precise failure example.
|
||||||
|
|
||||||
- fix issue244 by implementing special index for parameters to only use
|
- fix issue244 by implementing special index for parameters to only use
|
||||||
indices for paramentrized test ids
|
indices for parametrized test ids
|
||||||
|
|
||||||
- fix issue287 by running all finalizers but saving the exception
|
- fix issue287 by running all finalizers but saving the exception
|
||||||
from the first failing finalizer and re-raising it so teardown will
|
from the first failing finalizer and re-raising it so teardown will
|
||||||
|
@ -8805,7 +8884,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||||
it might be the cause for other finalizers to fail.
|
it might be the cause for other finalizers to fail.
|
||||||
|
|
||||||
- fix ordering when mock.patch or other standard decorator-wrappings
|
- fix ordering when mock.patch or other standard decorator-wrappings
|
||||||
are used with test methods. This fixues issue346 and should
|
are used with test methods. This fixes issue346 and should
|
||||||
help with random "xdist" collection failures. Thanks to
|
help with random "xdist" collection failures. Thanks to
|
||||||
Ronny Pfannschmidt and Donald Stufft for helping to isolate it.
|
Ronny Pfannschmidt and Donald Stufft for helping to isolate it.
|
||||||
|
|
||||||
|
@ -9062,7 +9141,7 @@ Bug fixes:
|
||||||
partially failed (finalizers would not always be called before)
|
partially failed (finalizers would not always be called before)
|
||||||
|
|
||||||
- fix issue320 - fix class scope for fixtures when mixed with
|
- fix issue320 - fix class scope for fixtures when mixed with
|
||||||
module-level functions. Thanks Anatloy Bubenkoff.
|
module-level functions. Thanks Anatoly Bubenkoff.
|
||||||
|
|
||||||
- you can specify "-q" or "-qq" to get different levels of "quieter"
|
- you can specify "-q" or "-qq" to get different levels of "quieter"
|
||||||
reporting (thanks Katarzyna Jachim)
|
reporting (thanks Katarzyna Jachim)
|
||||||
|
@ -9484,7 +9563,7 @@ Bug fixes:
|
||||||
unexpected exceptions
|
unexpected exceptions
|
||||||
- fix issue47: timing output in junitxml for test cases is now correct
|
- fix issue47: timing output in junitxml for test cases is now correct
|
||||||
- fix issue48: typo in MarkInfo repr leading to exception
|
- fix issue48: typo in MarkInfo repr leading to exception
|
||||||
- fix issue49: avoid confusing error when initizaliation partially fails
|
- fix issue49: avoid confusing error when initialization partially fails
|
||||||
- fix issue44: env/username expansion for junitxml file path
|
- fix issue44: env/username expansion for junitxml file path
|
||||||
- show releaselevel information in test runs for pypy
|
- show releaselevel information in test runs for pypy
|
||||||
- reworked doc pages for better navigation and PDF generation
|
- reworked doc pages for better navigation and PDF generation
|
||||||
|
@ -9609,7 +9688,7 @@ Bug fixes:
|
||||||
collection-before-running semantics were not
|
collection-before-running semantics were not
|
||||||
setup as with pytest 1.3.4. Note, however, that
|
setup as with pytest 1.3.4. Note, however, that
|
||||||
the recommended and much cleaner way to do test
|
the recommended and much cleaner way to do test
|
||||||
parametraization remains the "pytest_generate_tests"
|
parameterization remains the "pytest_generate_tests"
|
||||||
mechanism, see the docs.
|
mechanism, see the docs.
|
||||||
|
|
||||||
2.0.0 (2010-11-25)
|
2.0.0 (2010-11-25)
|
||||||
|
|
|
@ -15,9 +15,7 @@
|
||||||
#
|
#
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -65,7 +63,6 @@ latex_elements = {
|
||||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
"pallets_sphinx_themes",
|
|
||||||
"pygments_pytest",
|
"pygments_pytest",
|
||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinx.ext.autosummary",
|
"sphinx.ext.autosummary",
|
||||||
|
@ -140,10 +137,6 @@ add_module_names = False
|
||||||
# output. They are ignored by default.
|
# output. They are ignored by default.
|
||||||
# show_authors = False
|
# show_authors = False
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = "sphinx"
|
|
||||||
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
# A list of ignored prefixes for module index sorting.
|
||||||
# modindex_common_prefix = []
|
# modindex_common_prefix = []
|
||||||
|
|
||||||
|
@ -216,12 +209,9 @@ nitpick_ignore = [
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
sys.path.append(os.path.abspath("_themes"))
|
|
||||||
html_theme_path = ["_themes"]
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = "flask"
|
html_theme = "furo"
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
|
@ -266,18 +256,24 @@ html_favicon = "img/favicon.png"
|
||||||
|
|
||||||
html_sidebars = {
|
html_sidebars = {
|
||||||
"index": [
|
"index": [
|
||||||
"slim_searchbox.html",
|
"sidebar/brand.html",
|
||||||
|
"sidebar/search.html",
|
||||||
|
"sidebar/scroll-start.html",
|
||||||
"sidebarintro.html",
|
"sidebarintro.html",
|
||||||
"globaltoc.html",
|
"globaltoc.html",
|
||||||
"links.html",
|
"links.html",
|
||||||
"sourcelink.html",
|
"sidebar/scroll-end.html",
|
||||||
|
"style.html",
|
||||||
],
|
],
|
||||||
"**": [
|
"**": [
|
||||||
"slim_searchbox.html",
|
"sidebar/brand.html",
|
||||||
|
"sidebar/search.html",
|
||||||
|
"sidebar/scroll-start.html",
|
||||||
"globaltoc.html",
|
"globaltoc.html",
|
||||||
"relations.html",
|
"relations.html",
|
||||||
"links.html",
|
"links.html",
|
||||||
"sourcelink.html",
|
"sidebar/scroll-end.html",
|
||||||
|
"style.html",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,6 +312,9 @@ html_show_sourcelink = False
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = "pytestdoc"
|
htmlhelp_basename = "pytestdoc"
|
||||||
|
|
||||||
|
# The base URL which points to the root of the HTML documentation. It is used
|
||||||
|
# to indicate the location of document using the canonical link relation (#12363).
|
||||||
|
html_baseurl = "https://docs.pytest.org/en/stable/"
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
@ -337,10 +336,6 @@ latex_documents = [
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
|
||||||
# the title page.
|
|
||||||
latex_logo = "img/pytest1.png"
|
|
||||||
|
|
||||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
# not chapters.
|
# not chapters.
|
||||||
# latex_use_parts = False
|
# latex_use_parts = False
|
||||||
|
|
|
@ -26,19 +26,12 @@ Contact channels
|
||||||
<https://web.libera.chat/#pytest>`_, or `via Matrix
|
<https://web.libera.chat/#pytest>`_, or `via Matrix
|
||||||
<https://matrix.to/#/%23pytest:libera.chat>`_).
|
<https://matrix.to/#/%23pytest:libera.chat>`_).
|
||||||
|
|
||||||
- private mail to Holger.Krekel at gmail com if you want to communicate sensitive issues
|
|
||||||
|
|
||||||
|
|
||||||
- `merlinux.eu`_ offers pytest and tox-related professional teaching and
|
|
||||||
consulting.
|
|
||||||
|
|
||||||
.. _`pytest issue tracker`: https://github.com/pytest-dev/pytest/issues
|
.. _`pytest issue tracker`: https://github.com/pytest-dev/pytest/issues
|
||||||
.. _`old issue tracker`: https://bitbucket.org/hpk42/py-trunk/issues/
|
.. _`old issue tracker`: https://bitbucket.org/hpk42/py-trunk/issues/
|
||||||
|
|
||||||
.. _`pytest discussions`: https://github.com/pytest-dev/pytest/discussions
|
.. _`pytest discussions`: https://github.com/pytest-dev/pytest/discussions
|
||||||
|
|
||||||
.. _`merlinux.eu`: https://merlinux.eu/
|
|
||||||
|
|
||||||
.. _`get an account`:
|
.. _`get an account`:
|
||||||
|
|
||||||
.. _tetamap: https://tetamap.wordpress.com/
|
.. _tetamap: https://tetamap.wordpress.com/
|
||||||
|
|
|
@ -7,10 +7,6 @@ This page lists all pytest features that are currently deprecated or have been r
|
||||||
The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives
|
The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives
|
||||||
should be used instead.
|
should be used instead.
|
||||||
|
|
||||||
.. contents::
|
|
||||||
:depth: 3
|
|
||||||
:local:
|
|
||||||
|
|
||||||
|
|
||||||
Deprecated Features
|
Deprecated Features
|
||||||
-------------------
|
-------------------
|
||||||
|
@ -462,7 +458,7 @@ Now :class:`~pytest.Class` collects the test methods directly.
|
||||||
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
|
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
|
||||||
using a check such as ``if isinstance(node, Instance): return``.
|
using a check such as ``if isinstance(node, Instance): return``.
|
||||||
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
|
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
|
||||||
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
|
However, to keep such uses working, a dummy type has been instanced in ``pytest.Instance`` and ``_pytest.python.Instance``,
|
||||||
and importing it emits a deprecation warning. This was removed in pytest 8.
|
and importing it emits a deprecation warning. This was removed in pytest 8.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ class ManifestDirectory(pytest.Directory):
|
||||||
|
|
||||||
@pytest.hookimpl
|
@pytest.hookimpl
|
||||||
def pytest_collect_directory(path, parent):
|
def pytest_collect_directory(path, parent):
|
||||||
# Use our custom collector for directories containing a `mainfest.json` file.
|
# Use our custom collector for directories containing a `manifest.json` file.
|
||||||
if path.joinpath("manifest.json").is_file():
|
if path.joinpath("manifest.json").is_file():
|
||||||
return ManifestDirectory.from_parent(parent=parent, path=path)
|
return ManifestDirectory.from_parent(parent=parent, path=path)
|
||||||
# Otherwise fallback to the standard behavior.
|
# Otherwise fallback to the standard behavior.
|
||||||
|
|
|
@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 8 items
|
collected 8 items
|
||||||
|
|
||||||
<Dir parametrize.rst-198>
|
<Dir parametrize.rst-200>
|
||||||
<Module test_time.py>
|
<Module test_time.py>
|
||||||
<Function test_timedistance_v0[a0-b0-expected0]>
|
<Function test_timedistance_v0[a0-b0-expected0]>
|
||||||
<Function test_timedistance_v0[a1-b1-expected1]>
|
<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
|
rootdir: /home/sweet/project
|
||||||
collected 4 items
|
collected 4 items
|
||||||
|
|
||||||
<Dir parametrize.rst-198>
|
<Dir parametrize.rst-200>
|
||||||
<Module test_scenarios.py>
|
<Module test_scenarios.py>
|
||||||
<Class TestSampleWithScenarios>
|
<Class TestSampleWithScenarios>
|
||||||
<Function test_demo1[basic]>
|
<Function test_demo1[basic]>
|
||||||
|
@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
<Dir parametrize.rst-198>
|
<Dir parametrize.rst-200>
|
||||||
<Module test_backends.py>
|
<Module test_backends.py>
|
||||||
<Function test_db_initialized[d1]>
|
<Function test_db_initialized[d1]>
|
||||||
<Function test_db_initialized[d2]>
|
<Function test_db_initialized[d2]>
|
||||||
|
|
|
@ -152,7 +152,7 @@ The test collection would look like this:
|
||||||
configfile: pytest.ini
|
configfile: pytest.ini
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
<Dir pythoncollection.rst-199>
|
<Dir pythoncollection.rst-201>
|
||||||
<Module check_myapp.py>
|
<Module check_myapp.py>
|
||||||
<Class CheckMyApp>
|
<Class CheckMyApp>
|
||||||
<Function simple_check>
|
<Function simple_check>
|
||||||
|
@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
|
||||||
configfile: pytest.ini
|
configfile: pytest.ini
|
||||||
collected 3 items
|
collected 3 items
|
||||||
|
|
||||||
<Dir pythoncollection.rst-199>
|
<Dir pythoncollection.rst-201>
|
||||||
<Dir CWD>
|
<Dir CWD>
|
||||||
<Module pythoncollection.py>
|
<Module pythoncollection.py>
|
||||||
<Function test_function>
|
<Function test_function>
|
||||||
|
|
|
@ -18,7 +18,7 @@ System state
|
||||||
|
|
||||||
Broadly speaking, a flaky test indicates that the test relies on some system state that is not being appropriately controlled - the test environment is not sufficiently isolated. Higher level tests are more likely to be flaky as they rely on more state.
|
Broadly speaking, a flaky test indicates that the test relies on some system state that is not being appropriately controlled - the test environment is not sufficiently isolated. Higher level tests are more likely to be flaky as they rely on more state.
|
||||||
|
|
||||||
Flaky tests sometimes appear when a test suite is run in parallel (such as use of pytest-xdist). This can indicate a test is reliant on test ordering.
|
Flaky tests sometimes appear when a test suite is run in parallel (such as use of `pytest-xdist`_). This can indicate a test is reliant on test ordering.
|
||||||
|
|
||||||
- Perhaps a different test is failing to clean up after itself and leaving behind data which causes the flaky test to fail.
|
- Perhaps a different test is failing to clean up after itself and leaving behind data which causes the flaky test to fail.
|
||||||
- The flaky test is reliant on data from a previous test that doesn't clean up after itself, and in parallel runs that previous test is not always present
|
- The flaky test is reliant on data from a previous test that doesn't clean up after itself, and in parallel runs that previous test is not always present
|
||||||
|
@ -30,9 +30,22 @@ Overly strict assertion
|
||||||
|
|
||||||
Overly strict assertions can cause problems with floating point comparison as well as timing issues. :func:`pytest.approx` is useful here.
|
Overly strict assertions can cause problems with floating point comparison as well as timing issues. :func:`pytest.approx` is useful here.
|
||||||
|
|
||||||
|
Thread safety
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
Pytest features
|
pytest is single-threaded, executing its tests always in the same thread, sequentially, never spawning any threads itself.
|
||||||
^^^^^^^^^^^^^^^
|
|
||||||
|
Even in case of plugins which run tests in parallel, for example `pytest-xdist`_, usually work by spawning multiple *processes* and running tests in batches, without using multiple threads.
|
||||||
|
|
||||||
|
It is of course possible (and common) for tests and fixtures to spawn threads themselves as part of their testing workflow (for example, a fixture that starts a server thread in the background, or a test which executes production code that spawns threads), but some care must be taken:
|
||||||
|
|
||||||
|
* Make sure to eventually wait on any spawned threads -- for example at the end of a test, or during the teardown of a fixture.
|
||||||
|
* Avoid using primitives provided by pytest (:func:`pytest.warns`, :func:`pytest.raises`, etc) from multiple threads, as they are not thread-safe.
|
||||||
|
|
||||||
|
If your test suite uses threads and your are seeing flaky test results, do not discount the possibility that the test is implicitly using global state in pytest itself.
|
||||||
|
|
||||||
|
Related features
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Xfail strict
|
Xfail strict
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
@ -123,3 +136,6 @@ Resources
|
||||||
|
|
||||||
* `Flaky Tests at Google and How We Mitigate Them <https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html>`_ by John Micco, 2016
|
* `Flaky Tests at Google and How We Mitigate Them <https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html>`_ by John Micco, 2016
|
||||||
* `Where do Google's flaky tests come from? <https://testing.googleblog.com/2017/04/where-do-our-flaky-tests-come-from.html>`_ by Jeff Listfield, 2017
|
* `Where do Google's flaky tests come from? <https://testing.googleblog.com/2017/04/where-do-our-flaky-tests-come-from.html>`_ by Jeff Listfield, 2017
|
||||||
|
|
||||||
|
|
||||||
|
.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist
|
||||||
|
|
|
@ -8,15 +8,15 @@ pytest import mechanisms and ``sys.path``/``PYTHONPATH``
|
||||||
Import modes
|
Import modes
|
||||||
------------
|
------------
|
||||||
|
|
||||||
pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution.
|
pytest as a testing framework that needs to import test modules and ``conftest.py`` files for execution.
|
||||||
|
|
||||||
Importing files in Python is a non-trivial processes, so aspects of the
|
Importing files in Python is a non-trivial process, so aspects of the
|
||||||
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
|
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
|
||||||
these values:
|
these values:
|
||||||
|
|
||||||
.. _`import-mode-prepend`:
|
.. _`import-mode-prepend`:
|
||||||
|
|
||||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
* ``prepend`` (default): The directory path containing each module will be inserted into the *beginning*
|
||||||
of :py:data:`sys.path` if not already there, and then imported with
|
of :py:data:`sys.path` if not already there, and then imported with
|
||||||
the :func:`importlib.import_module <importlib.import_module>` function.
|
the :func:`importlib.import_module <importlib.import_module>` function.
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ these values:
|
||||||
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
||||||
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||||
|
|
||||||
This better allows to run test modules against installed versions of a package even if the
|
This better allows users to run test modules against installed versions of a package even if the
|
||||||
package under test has the same import root. For example:
|
package under test has the same import root. For example:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
@ -45,7 +45,7 @@ these values:
|
||||||
|
|
||||||
the tests will run against the installed version
|
the tests will run against the installed version
|
||||||
of ``pkg_under_test`` when ``--import-mode=append`` is used whereas
|
of ``pkg_under_test`` when ``--import-mode=append`` is used whereas
|
||||||
with ``prepend`` they would pick up the local version. This kind of confusion is why
|
with ``prepend``, they would pick up the local version. This kind of confusion is why
|
||||||
we advocate for using :ref:`src-layouts <src-layout>`.
|
we advocate for using :ref:`src-layouts <src-layout>`.
|
||||||
|
|
||||||
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
||||||
|
@ -67,7 +67,7 @@ these values:
|
||||||
are not importable. The recommendation in this case it to place testing utility modules together with the application/library
|
are not importable. The recommendation in this case it to place testing utility modules together with the application/library
|
||||||
code, for example ``app.testing.helpers``.
|
code, for example ``app.testing.helpers``.
|
||||||
|
|
||||||
Important: by "test utility modules" we mean functions/classes which are imported by
|
Important: by "test utility modules", we mean functions/classes which are imported by
|
||||||
other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along
|
other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along
|
||||||
with the test modules, and are discovered automatically by pytest.
|
with the test modules, and are discovered automatically by pytest.
|
||||||
|
|
||||||
|
@ -76,8 +76,8 @@ these values:
|
||||||
1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name
|
1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name
|
||||||
like ``tests.core.test_models`` and tries to import it.
|
like ``tests.core.test_models`` and tries to import it.
|
||||||
|
|
||||||
For non-test modules this will work if they are accessible via :py:data:`sys.path`, so
|
For non-test modules, this will work if they are accessible via :py:data:`sys.path`. So
|
||||||
for example ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``.
|
for example, ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``.
|
||||||
This is happens when plugins import non-test modules (for example doctesting).
|
This is happens when plugins import non-test modules (for example doctesting).
|
||||||
|
|
||||||
If this step succeeds, the module is returned.
|
If this step succeeds, the module is returned.
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 8.2.0
|
pytest 8.2.2
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
@ -274,7 +274,7 @@ Continue reading
|
||||||
Check out additional pytest resources to help you customize tests for your unique workflow:
|
Check out additional pytest resources to help you customize tests for your unique workflow:
|
||||||
|
|
||||||
* ":ref:`usage`" for command line invocation examples
|
* ":ref:`usage`" for command line invocation examples
|
||||||
* ":ref:`existingtestsuite`" for working with pre-existing tests
|
* ":ref:`existingtestsuite`" for working with preexisting tests
|
||||||
* ":ref:`mark`" for information on the ``pytest.mark`` mechanism
|
* ":ref:`mark`" for information on the ``pytest.mark`` mechanism
|
||||||
* ":ref:`fixtures`" for providing a functional baseline to your tests
|
* ":ref:`fixtures`" for providing a functional baseline to your tests
|
||||||
* ":ref:`plugins`" for managing and writing plugins
|
* ":ref:`plugins`" for managing and writing plugins
|
||||||
|
|
|
@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 12 items
|
collected 12 items
|
||||||
|
|
||||||
<Dir fixtures.rst-217>
|
<Dir fixtures.rst-219>
|
||||||
<Module test_anothersmtp.py>
|
<Module test_anothersmtp.py>
|
||||||
<Function test_showhelo[smtp.gmail.com]>
|
<Function test_showhelo[smtp.gmail.com]>
|
||||||
<Function test_showhelo[mail.python.org]>
|
<Function test_showhelo[mail.python.org]>
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
:orphan:
|
:orphan:
|
||||||
|
|
||||||
.. 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/>`_ (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/sprint>`_, **June 17th -- 22nd 2024**
|
|
||||||
- pytest tips and tricks for a better testsuite, `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague
|
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
|
||||||
|
|
||||||
.. _features:
|
.. _features:
|
||||||
|
|
||||||
|
.. sidebar:: **Next Open Trainings and Events**
|
||||||
|
|
||||||
|
- `pytest development sprint <https://github.com/pytest-dev/sprint>`_, **June 17th -- 22nd 2024**, Klaus (AT) / Remote
|
||||||
|
- `pytest tips and tricks for a better testsuite <https://ep2024.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague (CZ)
|
||||||
|
- `pytest: Professionelles Testen (nicht nur) für Python <https://pretalx.com/workshoptage-2024/talk/9VUHYB/>`_, at `CH Open Workshoptage <https://workshoptage.ch/>`_, **September 2nd 2024**, HSLU Rotkreuz (CH)
|
||||||
|
- `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), **March 4th -- 6th 2025**, Leipzig (DE) / Remote
|
||||||
|
|
||||||
|
Also see :doc:`previous talks and blogposts <talks>`
|
||||||
|
|
||||||
pytest: helps you write better programs
|
pytest: helps you write better programs
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
@ -25,7 +24,6 @@ scale to support complex functional testing for applications and libraries.
|
||||||
|
|
||||||
**PyPI package name**: :pypi:`pytest`
|
**PyPI package name**: :pypi:`pytest`
|
||||||
|
|
||||||
|
|
||||||
A quick example
|
A quick example
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ If you used older version of the ``py`` distribution (which
|
||||||
included the py.test command line tool and Python name space)
|
included the py.test command line tool and Python name space)
|
||||||
you accessed helpers and possibly collection classes through
|
you accessed helpers and possibly collection classes through
|
||||||
the ``py.test`` Python namespaces. The new ``pytest``
|
the ``py.test`` Python namespaces. The new ``pytest``
|
||||||
Python module flaty provides the same objects, following
|
Python module flatly provides the same objects, following
|
||||||
these renaming rules::
|
these renaming rules::
|
||||||
|
|
||||||
py.test.XYZ -> pytest.XYZ
|
py.test.XYZ -> pytest.XYZ
|
||||||
|
|
|
@ -39,7 +39,7 @@ Built-in fixtures
|
||||||
Store and retrieve values across pytest runs.
|
Store and retrieve values across pytest runs.
|
||||||
|
|
||||||
:fixture:`doctest_namespace`
|
:fixture:`doctest_namespace`
|
||||||
Provide a dict injected into the docstests namespace.
|
Provide a dict injected into the doctests namespace.
|
||||||
|
|
||||||
:fixture:`monkeypatch`
|
:fixture:`monkeypatch`
|
||||||
Temporarily modify classes, functions, dictionaries,
|
Temporarily modify classes, functions, dictionaries,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,9 +7,6 @@ API Reference
|
||||||
|
|
||||||
This page contains the full reference to pytest's API.
|
This page contains the full reference to pytest's API.
|
||||||
|
|
||||||
.. contents::
|
|
||||||
:depth: 3
|
|
||||||
:local:
|
|
||||||
|
|
||||||
Constants
|
Constants
|
||||||
---------
|
---------
|
||||||
|
@ -1940,7 +1937,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
|
|
||||||
general:
|
general:
|
||||||
-k EXPRESSION Only run tests which match the given substring
|
-k EXPRESSION Only run tests which match the given substring
|
||||||
expression. An expression is a Python evaluatable
|
expression. An expression is a Python evaluable
|
||||||
expression where all names are substring-matched
|
expression where all names are substring-matched
|
||||||
against test names and their parent classes.
|
against test names and their parent classes.
|
||||||
Example: -k 'test_method or test_other' matches all
|
Example: -k 'test_method or test_other' matches all
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
pallets-sphinx-themes
|
|
||||||
pluggy>=1.5.0
|
pluggy>=1.5.0
|
||||||
pygments-pytest>=2.3.0
|
pygments-pytest>=2.3.0
|
||||||
sphinx-removed-in>=0.2.0
|
sphinx-removed-in>=0.2.0
|
||||||
|
@ -8,4 +7,5 @@ sphinxcontrib-svg2pdfconverter
|
||||||
# Pin packaging because it no longer handles 'latest' version, which
|
# Pin packaging because it no longer handles 'latest' version, which
|
||||||
# is the version that is assigned to the docs.
|
# is the version that is assigned to the docs.
|
||||||
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
|
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
|
||||||
packaging <22
|
packaging
|
||||||
|
furo
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ def get_issues():
|
||||||
if r.status_code == 403:
|
if r.status_code == 403:
|
||||||
# API request limit exceeded
|
# API request limit exceeded
|
||||||
print(data["message"])
|
print(data["message"])
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
issues.extend(data)
|
issues.extend(data)
|
||||||
|
|
||||||
# Look for next page
|
# Look for next page
|
||||||
|
|
287
pyproject.toml
287
pyproject.toml
|
@ -1,3 +1,10 @@
|
||||||
|
[build-system]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
requires = [
|
||||||
|
"setuptools>=61",
|
||||||
|
"setuptools-scm[toml]>=6.2.3",
|
||||||
|
]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
|
@ -6,15 +13,15 @@ keywords = [
|
||||||
"test",
|
"test",
|
||||||
"unittest",
|
"unittest",
|
||||||
]
|
]
|
||||||
license = {text = "MIT"}
|
license = { text = "MIT" }
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Holger Krekel"},
|
{ name = "Holger Krekel" },
|
||||||
{name = "Bruno Oliveira"},
|
{ name = "Bruno Oliveira" },
|
||||||
{name = "Ronny Pfannschmidt"},
|
{ name = "Ronny Pfannschmidt" },
|
||||||
{name = "Floris Bruynooghe"},
|
{ name = "Floris Bruynooghe" },
|
||||||
{name = "Brianna Laugher"},
|
{ name = "Brianna Laugher" },
|
||||||
{name = "Florian Bruhin"},
|
{ name = "Florian Bruhin" },
|
||||||
{name = "Others (See AUTHORS)"},
|
{ name = "Others (See AUTHORS)" },
|
||||||
]
|
]
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
@ -39,15 +46,14 @@ dynamic = [
|
||||||
"version",
|
"version",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
'colorama; sys_platform == "win32"',
|
"colorama; sys_platform=='win32'",
|
||||||
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
|
"exceptiongroup>=1.0.0rc8; python_version<'3.11'",
|
||||||
"iniconfig",
|
"iniconfig",
|
||||||
"packaging",
|
"packaging",
|
||||||
"pluggy<2.0,>=1.5",
|
"pluggy<2,>=1.5",
|
||||||
'tomli>=1; python_version < "3.11"',
|
"tomli>=1; python_version<'3.11'",
|
||||||
]
|
]
|
||||||
[project.optional-dependencies]
|
optional-dependencies.dev = [
|
||||||
dev = [
|
|
||||||
"argcomplete",
|
"argcomplete",
|
||||||
"attrs>=19.2",
|
"attrs>=19.2",
|
||||||
"hypothesis>=3.56",
|
"hypothesis>=3.56",
|
||||||
|
@ -57,59 +63,54 @@ dev = [
|
||||||
"setuptools",
|
"setuptools",
|
||||||
"xmlschema",
|
"xmlschema",
|
||||||
]
|
]
|
||||||
[project.urls]
|
urls.Changelog = "https://docs.pytest.org/en/stable/changelog.html"
|
||||||
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
|
urls.Homepage = "https://docs.pytest.org/en/latest/"
|
||||||
Homepage = "https://docs.pytest.org/en/latest/"
|
urls.Source = "https://github.com/pytest-dev/pytest"
|
||||||
Source = "https://github.com/pytest-dev/pytest"
|
urls.Tracker = "https://github.com/pytest-dev/pytest/issues"
|
||||||
Tracker = "https://github.com/pytest-dev/pytest/issues"
|
urls.Twitter = "https://twitter.com/pytestdotorg"
|
||||||
Twitter = "https://twitter.com/pytestdotorg"
|
scripts."py.test" = "pytest:console_main"
|
||||||
[project.scripts]
|
scripts.pytest = "pytest:console_main"
|
||||||
"py.test" = "pytest:console_main"
|
|
||||||
pytest = "pytest:console_main"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
requires = [
|
|
||||||
"setuptools>=61",
|
|
||||||
"setuptools-scm[toml]>=6.2.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"_pytest" = ["py.typed"]
|
"_pytest" = [
|
||||||
"pytest" = ["py.typed"]
|
"py.typed",
|
||||||
|
]
|
||||||
|
"pytest" = [
|
||||||
|
"py.typed",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
write_to = "src/_pytest/_version.py"
|
write_to = "src/_pytest/_version.py"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
target-version = ['py38']
|
target-version = [
|
||||||
|
'py38',
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
src = ["src"]
|
|
||||||
line-length = 88
|
line-length = 88
|
||||||
|
src = [
|
||||||
[tool.ruff.format]
|
"src",
|
||||||
docstring-code-format = true
|
|
||||||
|
|
||||||
[tool.ruff.lint]
|
|
||||||
select = [
|
|
||||||
"B", # bugbear
|
|
||||||
"D", # pydocstyle
|
|
||||||
"E", # pycodestyle
|
|
||||||
"F", # pyflakes
|
|
||||||
"I", # isort
|
|
||||||
"PYI", # flake8-pyi
|
|
||||||
"UP", # pyupgrade
|
|
||||||
"RUF", # ruff
|
|
||||||
"W", # pycodestyle
|
|
||||||
"PIE", # flake8-pie
|
|
||||||
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
|
|
||||||
"PLE", # pylint error
|
|
||||||
"PLW", # pylint warning
|
|
||||||
"PLR1714", # Consider merging multiple comparisons
|
|
||||||
"T100", # flake8-debugger
|
|
||||||
]
|
]
|
||||||
ignore = [
|
format.docstring-code-format = true
|
||||||
|
lint.select = [
|
||||||
|
"B", # bugbear
|
||||||
|
"D", # pydocstyle
|
||||||
|
"E", # pycodestyle
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
|
||||||
|
"PIE", # flake8-pie
|
||||||
|
"PLE", # pylint error
|
||||||
|
"PLR1714", # Consider merging multiple comparisons
|
||||||
|
"PLW", # pylint warning
|
||||||
|
"PYI", # flake8-pyi
|
||||||
|
"RUF", # ruff
|
||||||
|
"T100", # flake8-debugger
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"W", # pycodestyle
|
||||||
|
]
|
||||||
|
lint.ignore = [
|
||||||
# bugbear ignore
|
# bugbear ignore
|
||||||
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
|
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
|
||||||
"B007", # Loop control variable `i` not used within loop body
|
"B007", # Loop control variable `i` not used within loop body
|
||||||
|
@ -117,10 +118,6 @@ ignore = [
|
||||||
"B010", # [*] Do not call `setattr` with a constant attribute value.
|
"B010", # [*] Do not call `setattr` with a constant attribute value.
|
||||||
"B011", # Do not `assert False` (`python -O` removes these calls)
|
"B011", # Do not `assert False` (`python -O` removes these calls)
|
||||||
"B028", # No explicit `stacklevel` keyword argument found
|
"B028", # No explicit `stacklevel` keyword argument found
|
||||||
# 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
|
# pydocstyle ignore
|
||||||
"D100", # Missing docstring in public module
|
"D100", # Missing docstring in public module
|
||||||
"D101", # Missing docstring in public class
|
"D101", # Missing docstring in public class
|
||||||
|
@ -130,46 +127,51 @@ ignore = [
|
||||||
"D105", # Missing docstring in magic method
|
"D105", # Missing docstring in magic method
|
||||||
"D106", # Missing docstring in public nested class
|
"D106", # Missing docstring in public nested class
|
||||||
"D107", # Missing docstring in `__init__`
|
"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
|
"D205", # 1 blank line required between summary line and description
|
||||||
|
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
|
||||||
"D400", # First line should end with a period
|
"D400", # First line should end with a period
|
||||||
"D401", # First line of docstring should be in imperative mood
|
"D401", # First line of docstring should be in imperative mood
|
||||||
"D402", # First line should not be the function's signature
|
"D402", # First line should not be the function's signature
|
||||||
"D404", # First word of the docstring should not be "This"
|
"D404", # First word of the docstring should not be "This"
|
||||||
"D415", # First line should end with a period, question mark, or exclamation point
|
"D415", # First line should end with a period, question mark, or exclamation point
|
||||||
|
# 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()`
|
||||||
|
# pylint ignore
|
||||||
|
"PLR5501", # Use `elif` instead of `else` then `if`
|
||||||
|
"PLW0120", # remove the else and dedent its contents
|
||||||
|
"PLW0603", # Using the global statement
|
||||||
|
"PLW2901", # for loop variable overwritten by assignment target
|
||||||
# ruff ignore
|
# ruff ignore
|
||||||
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
||||||
# pylint ignore
|
|
||||||
"PLW0603", # Using the global statement
|
|
||||||
"PLW0120", # remove the else and dedent its contents
|
|
||||||
"PLW2901", # for loop variable overwritten by assignment target
|
|
||||||
"PLR5501", # Use `elif` instead of `else` then `if`
|
|
||||||
]
|
]
|
||||||
|
lint.per-file-ignores."src/_pytest/_py/**/*.py" = [
|
||||||
[tool.ruff.lint.pycodestyle]
|
"B",
|
||||||
|
"PYI",
|
||||||
|
]
|
||||||
|
lint.per-file-ignores."src/_pytest/_version.py" = [
|
||||||
|
"I001",
|
||||||
|
]
|
||||||
|
lint.per-file-ignores."testing/python/approx.py" = [
|
||||||
|
"B015",
|
||||||
|
]
|
||||||
|
lint.isort.combine-as-imports = true
|
||||||
|
lint.isort.force-single-line = true
|
||||||
|
lint.isort.force-sort-within-sections = true
|
||||||
|
lint.isort.known-local-folder = [
|
||||||
|
"pytest",
|
||||||
|
"_pytest",
|
||||||
|
]
|
||||||
|
lint.isort.lines-after-imports = 2
|
||||||
|
lint.isort.order-by-type = false
|
||||||
# In order to be able to format for 88 char in ruff format
|
# In order to be able to format for 88 char in ruff format
|
||||||
max-line-length = 120
|
lint.pycodestyle.max-line-length = 120
|
||||||
|
lint.pydocstyle.convention = "pep257"
|
||||||
[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
|
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
|
||||||
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
|
|
||||||
"src/_pytest/_version.py" = ["I001"]
|
|
||||||
"testing/python/approx.py" = ["B015"]
|
|
||||||
|
|
||||||
[tool.pylint.main]
|
[tool.pylint.main]
|
||||||
# Maximum number of characters on a single line.
|
# Maximum number of characters on a single line.
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
disable= [
|
disable = [
|
||||||
"abstract-method",
|
"abstract-method",
|
||||||
"arguments-differ",
|
"arguments-differ",
|
||||||
"arguments-renamed",
|
"arguments-renamed",
|
||||||
|
@ -179,28 +181,25 @@ disable= [
|
||||||
"bad-mcs-method-argument",
|
"bad-mcs-method-argument",
|
||||||
"broad-exception-caught",
|
"broad-exception-caught",
|
||||||
"broad-exception-raised",
|
"broad-exception-raised",
|
||||||
"cell-var-from-loop",
|
"cell-var-from-loop", # B023 from ruff / flake8-bugbear
|
||||||
"comparison-of-constants",
|
"comparison-of-constants",
|
||||||
"comparison-with-callable",
|
"comparison-with-callable",
|
||||||
"comparison-with-itself",
|
"comparison-with-itself",
|
||||||
"condition-evals-to-constant",
|
"condition-evals-to-constant",
|
||||||
"consider-using-dict-items",
|
"consider-using-dict-items",
|
||||||
"consider-using-enumerate",
|
|
||||||
"consider-using-from-import",
|
"consider-using-from-import",
|
||||||
"consider-using-f-string",
|
"consider-using-f-string",
|
||||||
"consider-using-in",
|
"consider-using-in",
|
||||||
"consider-using-sys-exit",
|
|
||||||
"consider-using-ternary",
|
"consider-using-ternary",
|
||||||
"consider-using-with",
|
"consider-using-with",
|
||||||
"cyclic-import",
|
"cyclic-import",
|
||||||
"disallowed-name",
|
"disallowed-name", # foo / bar are used often in tests
|
||||||
"duplicate-code",
|
"duplicate-code",
|
||||||
"eval-used",
|
"eval-used",
|
||||||
"exec-used",
|
"exec-used",
|
||||||
"expression-not-assigned",
|
"expression-not-assigned",
|
||||||
"fixme",
|
"fixme",
|
||||||
"global-statement",
|
"global-statement",
|
||||||
"implicit-str-concat",
|
|
||||||
"import-error",
|
"import-error",
|
||||||
"import-outside-toplevel",
|
"import-outside-toplevel",
|
||||||
"inconsistent-return-statements",
|
"inconsistent-return-statements",
|
||||||
|
@ -211,10 +210,9 @@ disable= [
|
||||||
"keyword-arg-before-vararg",
|
"keyword-arg-before-vararg",
|
||||||
"line-too-long",
|
"line-too-long",
|
||||||
"method-hidden",
|
"method-hidden",
|
||||||
"misplaced-bare-raise",
|
|
||||||
"missing-docstring",
|
"missing-docstring",
|
||||||
"missing-timeout",
|
"missing-timeout",
|
||||||
"multiple-statements",
|
"multiple-statements", # multiple-statements-on-one-line-colon (E701) from ruff
|
||||||
"no-else-break",
|
"no-else-break",
|
||||||
"no-else-continue",
|
"no-else-continue",
|
||||||
"no-else-raise",
|
"no-else-raise",
|
||||||
|
@ -227,6 +225,7 @@ disable= [
|
||||||
"pointless-exception-statement",
|
"pointless-exception-statement",
|
||||||
"pointless-statement",
|
"pointless-statement",
|
||||||
"pointless-string-statement",
|
"pointless-string-statement",
|
||||||
|
"possibly-used-before-assignment",
|
||||||
"protected-access",
|
"protected-access",
|
||||||
"raise-missing-from",
|
"raise-missing-from",
|
||||||
"redefined-argument-from-local",
|
"redefined-argument-from-local",
|
||||||
|
@ -274,7 +273,6 @@ disable= [
|
||||||
"useless-else-on-loop",
|
"useless-else-on-loop",
|
||||||
"useless-import-alias",
|
"useless-import-alias",
|
||||||
"useless-return",
|
"useless-return",
|
||||||
"use-maxsplit-arg",
|
|
||||||
"using-constant-test",
|
"using-constant-test",
|
||||||
"wrong-import-order",
|
"wrong-import-order",
|
||||||
]
|
]
|
||||||
|
@ -290,16 +288,27 @@ indent = 4
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
minversion = "2.0"
|
minversion = "2.0"
|
||||||
addopts = "-rfEX -p pytester --strict-markers"
|
addopts = "-rfEX -p pytester --strict-markers"
|
||||||
python_files = ["test_*.py", "*_test.py", "testing/python/*.py"]
|
python_files = [
|
||||||
python_classes = ["Test", "Acceptance"]
|
"test_*.py",
|
||||||
python_functions = ["test"]
|
"*_test.py",
|
||||||
|
"testing/python/*.py",
|
||||||
|
]
|
||||||
|
python_classes = [
|
||||||
|
"Test",
|
||||||
|
"Acceptance",
|
||||||
|
]
|
||||||
|
python_functions = [
|
||||||
|
"test",
|
||||||
|
]
|
||||||
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
|
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
|
||||||
testpaths = ["testing"]
|
testpaths = [
|
||||||
|
"testing",
|
||||||
|
]
|
||||||
norecursedirs = [
|
norecursedirs = [
|
||||||
"testing/example_scripts",
|
"testing/example_scripts",
|
||||||
".*",
|
".*",
|
||||||
"build",
|
"build",
|
||||||
"dist",
|
"dist",
|
||||||
]
|
]
|
||||||
xfail_strict = true
|
xfail_strict = true
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
|
@ -354,49 +363,55 @@ directory = "changelog/"
|
||||||
title_format = "pytest {version} ({project_date})"
|
title_format = "pytest {version} ({project_date})"
|
||||||
template = "changelog/_template.rst"
|
template = "changelog/_template.rst"
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "breaking"
|
directory = "breaking"
|
||||||
name = "Breaking Changes"
|
name = "Breaking Changes"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "deprecation"
|
directory = "deprecation"
|
||||||
name = "Deprecations"
|
name = "Deprecations"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "feature"
|
directory = "feature"
|
||||||
name = "Features"
|
name = "Features"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "improvement"
|
directory = "improvement"
|
||||||
name = "Improvements"
|
name = "Improvements"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "bugfix"
|
directory = "bugfix"
|
||||||
name = "Bug Fixes"
|
name = "Bug Fixes"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "vendor"
|
directory = "vendor"
|
||||||
name = "Vendored Libraries"
|
name = "Vendored Libraries"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "doc"
|
directory = "doc"
|
||||||
name = "Improved Documentation"
|
name = "Improved Documentation"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "trivial"
|
directory = "trivial"
|
||||||
name = "Trivial/Internal Changes"
|
name = "Trivial/Internal Changes"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
files = ["src", "testing", "scripts"]
|
files = [
|
||||||
mypy_path = ["src"]
|
"src",
|
||||||
|
"testing",
|
||||||
|
"scripts",
|
||||||
|
]
|
||||||
|
mypy_path = [
|
||||||
|
"src",
|
||||||
|
]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_any_generics = true
|
disallow_any_generics = true
|
||||||
disallow_untyped_defs = true
|
disallow_untyped_defs = true
|
||||||
|
|
|
@ -55,7 +55,7 @@ from _pytest.pathlib import bestrelpath
|
||||||
if sys.version_info < (3, 11):
|
if sys.version_info < (3, 11):
|
||||||
from exceptiongroup import BaseExceptionGroup
|
from exceptiongroup import BaseExceptionGroup
|
||||||
|
|
||||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||||
|
|
||||||
|
|
||||||
class Code:
|
class Code:
|
||||||
|
@ -199,8 +199,8 @@ class TracebackEntry:
|
||||||
rawentry: TracebackType,
|
rawentry: TracebackType,
|
||||||
repr_style: Optional['Literal["short", "long"]'] = None,
|
repr_style: Optional['Literal["short", "long"]'] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._rawentry: "Final" = rawentry
|
self._rawentry: Final = rawentry
|
||||||
self._repr_style: "Final" = repr_style
|
self._repr_style: Final = repr_style
|
||||||
|
|
||||||
def with_repr_style(
|
def with_repr_style(
|
||||||
self, repr_style: Optional['Literal["short", "long"]']
|
self, repr_style: Optional['Literal["short", "long"]']
|
||||||
|
@ -424,15 +424,14 @@ class Traceback(List[TracebackEntry]):
|
||||||
# which generates code objects that have hash/value equality
|
# which generates code objects that have hash/value equality
|
||||||
# XXX needs a test
|
# XXX needs a test
|
||||||
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
|
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
|
||||||
# print "checking for recursion at", key
|
|
||||||
values = cache.setdefault(key, [])
|
values = cache.setdefault(key, [])
|
||||||
|
# Since Python 3.13 f_locals is a proxy, freeze it.
|
||||||
|
loc = dict(entry.frame.f_locals)
|
||||||
if values:
|
if values:
|
||||||
f = entry.frame
|
|
||||||
loc = f.f_locals
|
|
||||||
for otherloc in values:
|
for otherloc in values:
|
||||||
if otherloc == loc:
|
if otherloc == loc:
|
||||||
return i
|
return i
|
||||||
values.append(entry.frame.f_locals)
|
values.append(loc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -629,13 +628,14 @@ class ExceptionInfo(Generic[E]):
|
||||||
def getrepr(
|
def getrepr(
|
||||||
self,
|
self,
|
||||||
showlocals: bool = False,
|
showlocals: bool = False,
|
||||||
style: _TracebackStyle = "long",
|
style: TracebackStyle = "long",
|
||||||
abspath: bool = False,
|
abspath: bool = False,
|
||||||
tbfilter: Union[
|
tbfilter: Union[
|
||||||
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||||
] = True,
|
] = True,
|
||||||
funcargs: bool = False,
|
funcargs: bool = False,
|
||||||
truncate_locals: bool = True,
|
truncate_locals: bool = True,
|
||||||
|
truncate_args: bool = True,
|
||||||
chain: bool = True,
|
chain: bool = True,
|
||||||
) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
|
) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
|
||||||
"""Return str()able representation of this exception info.
|
"""Return str()able representation of this exception info.
|
||||||
|
@ -666,6 +666,9 @@ class ExceptionInfo(Generic[E]):
|
||||||
:param bool truncate_locals:
|
:param bool truncate_locals:
|
||||||
With ``showlocals==True``, make sure locals can be safely represented as strings.
|
With ``showlocals==True``, make sure locals can be safely represented as strings.
|
||||||
|
|
||||||
|
:param bool truncate_args:
|
||||||
|
With ``showargs==True``, make sure args can be safely represented as strings.
|
||||||
|
|
||||||
:param bool chain:
|
:param bool chain:
|
||||||
If chained exceptions in Python 3 should be shown.
|
If chained exceptions in Python 3 should be shown.
|
||||||
|
|
||||||
|
@ -692,6 +695,7 @@ class ExceptionInfo(Generic[E]):
|
||||||
tbfilter=tbfilter,
|
tbfilter=tbfilter,
|
||||||
funcargs=funcargs,
|
funcargs=funcargs,
|
||||||
truncate_locals=truncate_locals,
|
truncate_locals=truncate_locals,
|
||||||
|
truncate_args=truncate_args,
|
||||||
chain=chain,
|
chain=chain,
|
||||||
)
|
)
|
||||||
return fmt.repr_excinfo(self)
|
return fmt.repr_excinfo(self)
|
||||||
|
@ -805,11 +809,12 @@ class FormattedExcinfo:
|
||||||
fail_marker: ClassVar = "E"
|
fail_marker: ClassVar = "E"
|
||||||
|
|
||||||
showlocals: bool = False
|
showlocals: bool = False
|
||||||
style: _TracebackStyle = "long"
|
style: TracebackStyle = "long"
|
||||||
abspath: bool = True
|
abspath: bool = True
|
||||||
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||||
funcargs: bool = False
|
funcargs: bool = False
|
||||||
truncate_locals: bool = True
|
truncate_locals: bool = True
|
||||||
|
truncate_args: bool = True
|
||||||
chain: bool = True
|
chain: bool = True
|
||||||
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
|
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
|
||||||
default_factory=dict, init=False, repr=False
|
default_factory=dict, init=False, repr=False
|
||||||
|
@ -840,7 +845,11 @@ class FormattedExcinfo:
|
||||||
if self.funcargs:
|
if self.funcargs:
|
||||||
args = []
|
args = []
|
||||||
for argname, argvalue in entry.frame.getargs(var=True):
|
for argname, argvalue in entry.frame.getargs(var=True):
|
||||||
args.append((argname, saferepr(argvalue)))
|
if self.truncate_args:
|
||||||
|
str_repr = saferepr(argvalue)
|
||||||
|
else:
|
||||||
|
str_repr = saferepr(argvalue, maxsize=None)
|
||||||
|
args.append((argname, str_repr))
|
||||||
return ReprFuncArgs(args)
|
return ReprFuncArgs(args)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -1165,7 +1174,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||||
class ReprTraceback(TerminalRepr):
|
class ReprTraceback(TerminalRepr):
|
||||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
||||||
extraline: Optional[str]
|
extraline: Optional[str]
|
||||||
style: _TracebackStyle
|
style: TracebackStyle
|
||||||
|
|
||||||
entrysep: ClassVar = "_ "
|
entrysep: ClassVar = "_ "
|
||||||
|
|
||||||
|
@ -1199,7 +1208,7 @@ class ReprTracebackNative(ReprTraceback):
|
||||||
class ReprEntryNative(TerminalRepr):
|
class ReprEntryNative(TerminalRepr):
|
||||||
lines: Sequence[str]
|
lines: Sequence[str]
|
||||||
|
|
||||||
style: ClassVar[_TracebackStyle] = "native"
|
style: ClassVar[TracebackStyle] = "native"
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
tw.write("".join(self.lines))
|
tw.write("".join(self.lines))
|
||||||
|
@ -1211,7 +1220,7 @@ class ReprEntry(TerminalRepr):
|
||||||
reprfuncargs: Optional["ReprFuncArgs"]
|
reprfuncargs: Optional["ReprFuncArgs"]
|
||||||
reprlocals: Optional["ReprLocals"]
|
reprlocals: Optional["ReprLocals"]
|
||||||
reprfileloc: Optional["ReprFileLocation"]
|
reprfileloc: Optional["ReprFileLocation"]
|
||||||
style: _TracebackStyle
|
style: TracebackStyle
|
||||||
|
|
||||||
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
||||||
"""Write the source code portions of a list of traceback entries with syntax highlighting.
|
"""Write the source code portions of a list of traceback entries with syntax highlighting.
|
||||||
|
|
|
@ -8,13 +8,17 @@ from typing import Literal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from pygments.style import Style
|
|
||||||
|
|
||||||
from ..compat import assert_never
|
from ..compat import assert_never
|
||||||
from .wcwidth import wcswidth
|
from .wcwidth import wcswidth
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pygments.formatter import Formatter
|
||||||
|
from pygments.lexer import Lexer
|
||||||
|
|
||||||
|
|
||||||
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
|
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
|
||||||
|
|
||||||
|
|
||||||
|
@ -196,205 +200,76 @@ class TerminalWriter:
|
||||||
for indent, new_line in zip(indents, new_lines):
|
for indent, new_line in zip(indents, new_lines):
|
||||||
self.line(indent + new_line)
|
self.line(indent + new_line)
|
||||||
|
|
||||||
def _highlight(
|
def _get_pygments_lexer(
|
||||||
self, source: str, lexer: Literal["diff", "python"] = "python"
|
self, lexer: Literal["python", "diff"]
|
||||||
) -> str:
|
) -> Optional["Lexer"]:
|
||||||
"""Highlight the given source if we have markup support."""
|
try:
|
||||||
|
if lexer == "python":
|
||||||
|
from pygments.lexers.python import PythonLexer
|
||||||
|
|
||||||
|
return PythonLexer()
|
||||||
|
elif lexer == "diff":
|
||||||
|
from pygments.lexers.diff import DiffLexer
|
||||||
|
|
||||||
|
return DiffLexer()
|
||||||
|
else:
|
||||||
|
assert_never(lexer)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_pygments_formatter(self) -> Optional["Formatter"]:
|
||||||
|
try:
|
||||||
|
import pygments.util
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
|
|
||||||
if not source or not self.hasmarkup or not self.code_highlight:
|
theme = os.getenv("PYTEST_THEME")
|
||||||
return source
|
theme_mode = os.getenv("PYTEST_THEME_MODE", "dark")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pygments.formatters.terminal import TerminalFormatter
|
from pygments.formatters.terminal import TerminalFormatter
|
||||||
|
|
||||||
if lexer == "python":
|
return TerminalFormatter(bg=theme_mode, style=theme)
|
||||||
from pygments.lexers.python import PythonLexer as Lexer
|
|
||||||
elif lexer == "diff":
|
except pygments.util.ClassNotFound as e:
|
||||||
from pygments.lexers.diff import DiffLexer as Lexer
|
raise UsageError(
|
||||||
else:
|
f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
|
||||||
assert_never(lexer)
|
"Hint: See available pygments styles with `pygmentize -L styles`."
|
||||||
from pygments import highlight
|
) from e
|
||||||
import pygments.util
|
except pygments.util.OptionError as e:
|
||||||
except ImportError:
|
raise UsageError(
|
||||||
|
f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. "
|
||||||
|
"The allowed values are 'dark' (default) and 'light'."
|
||||||
|
) from e
|
||||||
|
|
||||||
|
def _highlight(
|
||||||
|
self, source: str, lexer: Literal["diff", "python"] = "python"
|
||||||
|
) -> str:
|
||||||
|
"""Highlight the given source if we have markup support."""
|
||||||
|
if not source or not self.hasmarkup or not self.code_highlight:
|
||||||
return source
|
return source
|
||||||
else:
|
|
||||||
try:
|
|
||||||
# Establishes the style to be used.
|
|
||||||
if os.environ.get("COLORTERM", "") not in (
|
|
||||||
"truecolor",
|
|
||||||
"24bit",
|
|
||||||
) and "256" not in os.environ.get("TERM", ""):
|
|
||||||
# The default case
|
|
||||||
|
|
||||||
PytestTerminalFormat = TerminalFormatter(
|
pygments_lexer = self._get_pygments_lexer(lexer)
|
||||||
bg=os.getenv("PYTEST_THEME_MODE", "dark"),
|
if pygments_lexer is None:
|
||||||
style=os.getenv("PYTEST_THEME"),
|
return source
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
pygments_formatter = self._get_pygments_formatter()
|
||||||
if os.getenv("PYTEST_THEME") is None:
|
if pygments_formatter is None:
|
||||||
if os.getenv("PYTEST_THEME_MODE") is None:
|
return source
|
||||||
# Neither PYTEST_THEME nor PYTEST_THEME_MODE have been set so using dark mode
|
|
||||||
SelectedStyle = DarkModeStyle
|
|
||||||
|
|
||||||
elif os.getenv("PYTEST_THEME_MODE") == "light":
|
from pygments import highlight
|
||||||
# PYTEST_THEME has not been set but PYTEST_THEME_MODE has been set to light mode
|
|
||||||
SelectedStyle = LightModeStyle
|
|
||||||
|
|
||||||
else:
|
highlighted: str = highlight(source, pygments_lexer, pygments_formatter)
|
||||||
# PYTEST_THEME has not been set and PYTEST_THEME_MODE is not light so use dark
|
# pygments terminal formatter may add a newline when there wasn't one.
|
||||||
SelectedStyle = DarkModeStyle
|
# We don't want this, remove.
|
||||||
|
if highlighted[-1] == "\n" and source[-1] != "\n":
|
||||||
|
highlighted = highlighted[:-1]
|
||||||
|
|
||||||
else:
|
# Some lexers will not set the initial color explicitly
|
||||||
# PYTEST_THEME has been set so use it
|
# which may lead to the previous color being propagated to the
|
||||||
SelectedStyle = None
|
# start of the expression, so reset first.
|
||||||
|
highlighted = "\x1b[0m" + highlighted
|
||||||
|
|
||||||
# The style has now been selected the right formatter needs to be used
|
return highlighted
|
||||||
if os.environ.get("COLORTERM", "") in ("truecolor", "24bit"):
|
|
||||||
# The true color formatter
|
|
||||||
from pygments.formatters.terminal256 import (
|
|
||||||
TerminalTrueColorFormatter,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the style is user input
|
|
||||||
if SelectedStyle is None:
|
|
||||||
PytestTerminalFormat = TerminalTrueColorFormatter(
|
|
||||||
style=os.getenv("PYTEST_THEME")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
PytestTerminalFormat = TerminalTrueColorFormatter(
|
|
||||||
style=SelectedStyle
|
|
||||||
)
|
|
||||||
|
|
||||||
elif "256" in os.environ.get("TERM", ""):
|
|
||||||
# The 256 color formater
|
|
||||||
from pygments.formatters.terminal256 import Terminal256Formatter
|
|
||||||
|
|
||||||
# If the style is user input
|
|
||||||
if SelectedStyle is None:
|
|
||||||
PytestTerminalFormat = Terminal256Formatter(
|
|
||||||
style=os.getenv("PYTEST_THEME")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
PytestTerminalFormat = Terminal256Formatter(
|
|
||||||
style=SelectedStyle
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# The default case (Although this code should not be reached)
|
|
||||||
PytestTerminalFormat = TerminalFormatter(
|
|
||||||
bg=os.getenv("PYTEST_THEME_MODE", "dark"),
|
|
||||||
style=os.getenv("PYTEST_THEME"),
|
|
||||||
)
|
|
||||||
|
|
||||||
highlighted: str = highlight(source, Lexer(), PytestTerminalFormat)
|
|
||||||
# pygments terminal formatter may add a newline when there wasn't one.
|
|
||||||
# We don't want this, remove.
|
|
||||||
if highlighted[-1] == "\n" and source[-1] != "\n":
|
|
||||||
highlighted = highlighted[:-1]
|
|
||||||
|
|
||||||
# Some lexers will not set the initial color explicitly
|
|
||||||
# which may lead to the previous color being propagated to the
|
|
||||||
# start of the expression, so reset first.
|
|
||||||
return "\x1b[0m" + highlighted
|
|
||||||
except pygments.util.ClassNotFound as e:
|
|
||||||
raise UsageError(
|
|
||||||
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
|
||||||
"Only valid pygment styles are allowed.".format(
|
|
||||||
os.getenv("PYTEST_THEME")
|
|
||||||
)
|
|
||||||
) from e
|
|
||||||
except pygments.util.OptionError as e:
|
|
||||||
raise UsageError(
|
|
||||||
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
|
|
||||||
"The only allowed values are 'dark' and 'light'.".format(
|
|
||||||
os.getenv("PYTEST_THEME_MODE")
|
|
||||||
)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
|
|
||||||
class DarkModeStyle(Style):
|
|
||||||
# The default dark mode style class from TerminalFormatter recreated to work with
|
|
||||||
# both TerminalTrueColorFormatter and Terminal256Formatter
|
|
||||||
|
|
||||||
from pygments.token import Comment
|
|
||||||
from pygments.token import Error
|
|
||||||
from pygments.token import Generic
|
|
||||||
from pygments.token import Keyword
|
|
||||||
from pygments.token import Name
|
|
||||||
from pygments.token import Number
|
|
||||||
from pygments.token import Operator
|
|
||||||
from pygments.token import String
|
|
||||||
from pygments.token import Token
|
|
||||||
from pygments.token import Whitespace
|
|
||||||
|
|
||||||
styles = {
|
|
||||||
Token: "",
|
|
||||||
Whitespace: "ansibrightblack",
|
|
||||||
Comment: "ansibrightblack",
|
|
||||||
Comment.Preproc: "ansibrightcyan",
|
|
||||||
Keyword: "ansibrightblue",
|
|
||||||
Keyword.Type: "ansibrightcyan",
|
|
||||||
Operator.Word: "ansibrightmagenta",
|
|
||||||
Name.Builtin: "ansibrightcyan",
|
|
||||||
Name.Function: "ansibrightgreen",
|
|
||||||
Name.Namespace: "ansibrightcyan",
|
|
||||||
Name.Class: "ansibrightgreen",
|
|
||||||
Name.Exception: "ansibrightcyan",
|
|
||||||
Name.Decorator: "ansigray",
|
|
||||||
Name.Variable: "ansibrightred",
|
|
||||||
Name.Constant: "ansibrightred",
|
|
||||||
Name.Attribute: "ansibrightcyan",
|
|
||||||
Name.Tag: "ansibrightblue",
|
|
||||||
String: "ansiyellow",
|
|
||||||
Number: "ansibrightblue",
|
|
||||||
Generic.Deleted: "ansibrightred",
|
|
||||||
Generic.Inserted: "ansigreen",
|
|
||||||
Generic.Subheading: "ansimagenta",
|
|
||||||
Generic.Error: "ansibrightred",
|
|
||||||
Error: "ansibrightred",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LightModeStyle(Style):
|
|
||||||
# The default light mode style class from TerminalFormatter recreated to work with
|
|
||||||
# both TerminalTrueColorFormatter and Terminal256Formatter
|
|
||||||
|
|
||||||
from pygments.token import Comment
|
|
||||||
from pygments.token import Error
|
|
||||||
from pygments.token import Generic
|
|
||||||
from pygments.token import Keyword
|
|
||||||
from pygments.token import Name
|
|
||||||
from pygments.token import Number
|
|
||||||
from pygments.token import Operator
|
|
||||||
from pygments.token import String
|
|
||||||
from pygments.token import Token
|
|
||||||
from pygments.token import Whitespace
|
|
||||||
|
|
||||||
styles = {
|
|
||||||
Token: "",
|
|
||||||
Whitespace: "ansigray",
|
|
||||||
Comment: "ansigray",
|
|
||||||
Comment.Preproc: "ansicyan",
|
|
||||||
Keyword: "ansiblue",
|
|
||||||
Keyword.Type: "ansicyan",
|
|
||||||
Operator.Word: "ansimagenta",
|
|
||||||
Name.Builtin: "ansicyan",
|
|
||||||
Name.Function: "ansigreen",
|
|
||||||
Name.Namespace: "ansicyan",
|
|
||||||
Name.Class: "ansigreen",
|
|
||||||
Name.Exception: "ansicyan",
|
|
||||||
Name.Decorator: "ansibrightblack",
|
|
||||||
Name.Variable: "ansired",
|
|
||||||
Name.Constant: "ansired",
|
|
||||||
Name.Attribute: "ansicyan",
|
|
||||||
Name.Tag: "ansibrightblue",
|
|
||||||
String: "ansiyellow",
|
|
||||||
Number: "ansiblue",
|
|
||||||
Generic.Deleted: "ansibrightred",
|
|
||||||
Generic.Inserted: "ansigreen",
|
|
||||||
Generic.Subheading: "ansimagenta",
|
|
||||||
Generic.Error: "ansibrightred",
|
|
||||||
Error: "ansibrightred",
|
|
||||||
}
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ _winerrnomap = {
|
||||||
3: errno.ENOENT,
|
3: errno.ENOENT,
|
||||||
17: errno.EEXIST,
|
17: errno.EEXIST,
|
||||||
18: errno.EXDEV,
|
18: errno.EXDEV,
|
||||||
13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable
|
13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable
|
||||||
22: errno.ENOTDIR,
|
22: errno.ENOTDIR,
|
||||||
20: errno.ENOTDIR,
|
20: errno.ENOTDIR,
|
||||||
267: errno.ENOTDIR,
|
267: errno.ENOTDIR,
|
||||||
|
|
|
@ -161,15 +161,13 @@ class Visitor:
|
||||||
)
|
)
|
||||||
if not self.breadthfirst:
|
if not self.breadthfirst:
|
||||||
for subdir in dirs:
|
for subdir in dirs:
|
||||||
for p in self.gen(subdir):
|
yield from self.gen(subdir)
|
||||||
yield p
|
|
||||||
for p in self.optsort(entries):
|
for p in self.optsort(entries):
|
||||||
if self.fil is None or self.fil(p):
|
if self.fil is None or self.fil(p):
|
||||||
yield p
|
yield p
|
||||||
if self.breadthfirst:
|
if self.breadthfirst:
|
||||||
for subdir in dirs:
|
for subdir in dirs:
|
||||||
for p in self.gen(subdir):
|
yield from self.gen(subdir)
|
||||||
yield p
|
|
||||||
|
|
||||||
|
|
||||||
class FNMatcher:
|
class FNMatcher:
|
||||||
|
@ -836,7 +834,7 @@ class LocalPath:
|
||||||
def copy(self, target, mode=False, stat=False):
|
def copy(self, target, mode=False, stat=False):
|
||||||
"""Copy path to target.
|
"""Copy path to target.
|
||||||
|
|
||||||
If mode is True, will copy copy permission from path to target.
|
If mode is True, will copy permission from path to target.
|
||||||
If stat is True, copy permission, last modification
|
If stat is True, copy permission, last modification
|
||||||
time, last access time, and flags from path to target.
|
time, last access time, and flags from path to target.
|
||||||
"""
|
"""
|
||||||
|
@ -1047,7 +1045,7 @@ class LocalPath:
|
||||||
def pypkgpath(self):
|
def pypkgpath(self):
|
||||||
"""Return the Python package path by looking for the last
|
"""Return the Python package path by looking for the last
|
||||||
directory upwards which still contains an __init__.py.
|
directory upwards which still contains an __init__.py.
|
||||||
Return None if a pkgpath can not be determined.
|
Return None if a pkgpath cannot be determined.
|
||||||
"""
|
"""
|
||||||
pkgpath = None
|
pkgpath = None
|
||||||
for parent in self.parts(reverse=True):
|
for parent in self.parts(reverse=True):
|
||||||
|
|
|
@ -584,7 +584,7 @@ def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
|
||||||
# multi-line assert with message
|
# multi-line assert with message
|
||||||
elif lineno in seen_lines:
|
elif lineno in seen_lines:
|
||||||
lines[-1] = lines[-1][:offset]
|
lines[-1] = lines[-1][:offset]
|
||||||
# multi line assert with escapd newline before message
|
# multi line assert with escaped newline before message
|
||||||
else:
|
else:
|
||||||
lines.append(line[:offset])
|
lines.append(line[:offset])
|
||||||
_write_and_reset()
|
_write_and_reset()
|
||||||
|
@ -835,7 +835,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
current = self.stack.pop()
|
current = self.stack.pop()
|
||||||
if self.stack:
|
if self.stack:
|
||||||
self.explanation_specifiers = self.stack[-1]
|
self.explanation_specifiers = self.stack[-1]
|
||||||
keys = [ast.Constant(key) for key in current.keys()]
|
keys: List[Optional[ast.expr]] = [ast.Constant(key) for key in current.keys()]
|
||||||
format_dict = ast.Dict(keys, list(current.values()))
|
format_dict = ast.Dict(keys, list(current.values()))
|
||||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||||
name = "@py_format" + str(next(self.variable_counter))
|
name = "@py_format" + str(next(self.variable_counter))
|
||||||
|
@ -926,13 +926,13 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
[*self.expl_stmts, hook_call_pass],
|
[*self.expl_stmts, hook_call_pass],
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
statements_pass = [hook_impl_test]
|
statements_pass: List[ast.stmt] = [hook_impl_test]
|
||||||
|
|
||||||
# Test for assertion condition
|
# Test for assertion condition
|
||||||
main_test = ast.If(negation, statements_fail, statements_pass)
|
main_test = ast.If(negation, statements_fail, statements_pass)
|
||||||
self.statements.append(main_test)
|
self.statements.append(main_test)
|
||||||
if self.format_variables:
|
if self.format_variables:
|
||||||
variables = [
|
variables: List[ast.expr] = [
|
||||||
ast.Name(name, ast.Store()) for name in self.format_variables
|
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||||
]
|
]
|
||||||
clear_format = ast.Assign(variables, ast.Constant(None))
|
clear_format = ast.Assign(variables, ast.Constant(None))
|
||||||
|
@ -1114,11 +1114,11 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||||
left_expl = f"({left_expl})"
|
left_expl = f"({left_expl})"
|
||||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
load_names: List[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||||
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
|
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
|
||||||
it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
|
it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
|
||||||
expls = []
|
expls: List[ast.expr] = []
|
||||||
syms = []
|
syms: List[ast.expr] = []
|
||||||
results = [left_res]
|
results = [left_res]
|
||||||
for i, op, next_operand in it:
|
for i, op, next_operand in it:
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -325,7 +325,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||||
def _compare_eq_iterable(
|
def _compare_eq_iterable(
|
||||||
left: Iterable[Any],
|
left: Iterable[Any],
|
||||||
right: Iterable[Any],
|
right: Iterable[Any],
|
||||||
highligher: _HighlightFunc,
|
highlighter: _HighlightFunc,
|
||||||
verbose: int = 0,
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
if verbose <= 0 and not running_on_ci():
|
if verbose <= 0 and not running_on_ci():
|
||||||
|
@ -340,7 +340,7 @@ def _compare_eq_iterable(
|
||||||
# "right" is the expected base against which we compare "left",
|
# "right" is the expected base against which we compare "left",
|
||||||
# see https://github.com/pytest-dev/pytest/issues/3333
|
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||||
explanation.extend(
|
explanation.extend(
|
||||||
highligher(
|
highlighter(
|
||||||
"\n".join(
|
"\n".join(
|
||||||
line.rstrip()
|
line.rstrip()
|
||||||
for line in difflib.ndiff(right_formatting, left_formatting)
|
for line in difflib.ndiff(right_formatting, left_formatting)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
# This plugin was not named "cache" to avoid conflicts with the external
|
# This plugin was not named "cache" to avoid conflicts with the external
|
||||||
# pytest-cache version.
|
# pytest-cache version.
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import errno
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -213,21 +214,38 @@ class Cache:
|
||||||
dir=self._cachedir.parent,
|
dir=self._cachedir.parent,
|
||||||
) as newpath:
|
) as newpath:
|
||||||
path = Path(newpath)
|
path = Path(newpath)
|
||||||
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
|
|
||||||
|
# Reset permissions to the default, see #12308.
|
||||||
|
# Note: there's no way to get the current umask atomically, eek.
|
||||||
|
umask = os.umask(0o022)
|
||||||
|
os.umask(umask)
|
||||||
|
path.chmod(0o777 - umask)
|
||||||
|
|
||||||
|
with open(path.joinpath("README.md"), "x", encoding="UTF-8") as f:
|
||||||
f.write(README_CONTENT)
|
f.write(README_CONTENT)
|
||||||
with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f:
|
with open(path.joinpath(".gitignore"), "x", encoding="UTF-8") as f:
|
||||||
f.write("# Created by pytest automatically.\n*\n")
|
f.write("# Created by pytest automatically.\n*\n")
|
||||||
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
|
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
|
||||||
f.write(CACHEDIR_TAG_CONTENT)
|
f.write(CACHEDIR_TAG_CONTENT)
|
||||||
|
|
||||||
path.rename(self._cachedir)
|
try:
|
||||||
# Create a directory in place of the one we just moved so that `TemporaryDirectory`'s
|
path.rename(self._cachedir)
|
||||||
# cleanup doesn't complain.
|
except OSError as e:
|
||||||
#
|
# If 2 concurrent pytests both race to the rename, the loser
|
||||||
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See
|
# gets "Directory not empty" from the rename. In this case,
|
||||||
# https://github.com/python/cpython/issues/74168. Note that passing delete=False would
|
# everything is handled so just continue (while letting the
|
||||||
# do the wrong thing in case of errors and isn't supported until python 3.12.
|
# temporary directory be cleaned up).
|
||||||
path.mkdir()
|
if e.errno != errno.ENOTEMPTY:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# Create a directory in place of the one we just moved so that
|
||||||
|
# `TemporaryDirectory`'s cleanup doesn't complain.
|
||||||
|
#
|
||||||
|
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10.
|
||||||
|
# See https://github.com/python/cpython/issues/74168. Note that passing
|
||||||
|
# delete=False would do the wrong thing in case of errors and isn't supported
|
||||||
|
# until python 3.12.
|
||||||
|
path.mkdir()
|
||||||
|
|
||||||
|
|
||||||
class LFPluginCollWrapper:
|
class LFPluginCollWrapper:
|
||||||
|
@ -244,7 +262,7 @@ class LFPluginCollWrapper:
|
||||||
# Sort any lf-paths to the beginning.
|
# Sort any lf-paths to the beginning.
|
||||||
lf_paths = self.lfplugin._last_failed_paths
|
lf_paths = self.lfplugin._last_failed_paths
|
||||||
|
|
||||||
# Use stable sort to priorize last failed.
|
# Use stable sort to prioritize last failed.
|
||||||
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
|
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
|
||||||
return node.path in lf_paths
|
return node.path in lf_paths
|
||||||
|
|
||||||
|
|
|
@ -983,6 +983,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_output(capsys):
|
def test_output(capsys):
|
||||||
|
@ -1010,6 +1011,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
||||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_output(capsysbinary):
|
def test_output(capsysbinary):
|
||||||
|
@ -1037,6 +1039,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_system_echo(capfd):
|
def test_system_echo(capfd):
|
||||||
|
@ -1064,6 +1067,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
|
||||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_system_echo(capfdbinary):
|
def test_system_echo(capfdbinary):
|
||||||
|
|
|
@ -53,7 +53,7 @@ def iscoroutinefunction(func: object) -> bool:
|
||||||
def syntax, and doesn't contain yield), or a function decorated with
|
def syntax, and doesn't contain yield), or a function decorated with
|
||||||
@asyncio.coroutine.
|
@asyncio.coroutine.
|
||||||
|
|
||||||
Note: copied and modified from Python 3.5's builtin couroutines.py to avoid
|
Note: copied and modified from Python 3.5's builtin coroutines.py to avoid
|
||||||
importing asyncio directly, which in turns also initializes the "logging"
|
importing asyncio directly, which in turns also initializes the "logging"
|
||||||
module as a side-effect (see issue #8).
|
module as a side-effect (see issue #8).
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -54,7 +54,10 @@ from _pytest import __version__
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest._code import ExceptionInfo
|
from _pytest._code import ExceptionInfo
|
||||||
from _pytest._code import filter_traceback
|
from _pytest._code import filter_traceback
|
||||||
|
from _pytest._code.code import TracebackStyle
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
|
from _pytest.config.argparsing import Argument
|
||||||
|
from _pytest.config.argparsing import Parser
|
||||||
import _pytest.deprecated
|
import _pytest.deprecated
|
||||||
import _pytest.hookspec
|
import _pytest.hookspec
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
|
@ -71,9 +74,7 @@ from _pytest.warning_types import warn_explicit_for
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .argparsing import Argument
|
from _pytest.cacheprovider import Cache
|
||||||
from .argparsing import Parser
|
|
||||||
from _pytest._code.code import _TracebackStyle
|
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
|
|
||||||
|
@ -462,7 +463,7 @@ class PytestPluginManager(PluginManager):
|
||||||
# (see issue #1073).
|
# (see issue #1073).
|
||||||
if not name.startswith("pytest_"):
|
if not name.startswith("pytest_"):
|
||||||
return None
|
return None
|
||||||
# Ignore names which can not be hooks.
|
# Ignore names which cannot be hooks.
|
||||||
if name == "pytest_plugins":
|
if name == "pytest_plugins":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -574,8 +575,8 @@ class PytestPluginManager(PluginManager):
|
||||||
self._noconftest = noconftest
|
self._noconftest = noconftest
|
||||||
self._using_pyargs = pyargs
|
self._using_pyargs = pyargs
|
||||||
foundanchor = False
|
foundanchor = False
|
||||||
for intitial_path in args:
|
for initial_path in args:
|
||||||
path = str(intitial_path)
|
path = str(initial_path)
|
||||||
# remove node-id syntax
|
# remove node-id syntax
|
||||||
i = path.find("::")
|
i = path.find("::")
|
||||||
if i != -1:
|
if i != -1:
|
||||||
|
@ -1030,6 +1031,9 @@ class Config:
|
||||||
#: 'testpaths' configuration value.
|
#: 'testpaths' configuration value.
|
||||||
TESTPATHS = enum.auto()
|
TESTPATHS = enum.auto()
|
||||||
|
|
||||||
|
# Set by cacheprovider plugin.
|
||||||
|
cache: Optional["Cache"]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pluginmanager: PytestPluginManager,
|
pluginmanager: PytestPluginManager,
|
||||||
|
@ -1091,11 +1095,6 @@ class Config:
|
||||||
self.args_source = Config.ArgsSource.ARGS
|
self.args_source = Config.ArgsSource.ARGS
|
||||||
self.args: List[str] = []
|
self.args: List[str] = []
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from _pytest.cacheprovider import Cache
|
|
||||||
|
|
||||||
self.cache: Optional[Cache] = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rootpath(self) -> Path:
|
def rootpath(self) -> Path:
|
||||||
"""The path to the :ref:`rootdir <rootdir>`.
|
"""The path to the :ref:`rootdir <rootdir>`.
|
||||||
|
@ -1175,7 +1174,7 @@ class Config:
|
||||||
option: Optional[argparse.Namespace] = None,
|
option: Optional[argparse.Namespace] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if option and getattr(option, "fulltrace", False):
|
if option and getattr(option, "fulltrace", False):
|
||||||
style: _TracebackStyle = "long"
|
style: TracebackStyle = "long"
|
||||||
else:
|
else:
|
||||||
style = "native"
|
style = "native"
|
||||||
excrepr = excinfo.getrepr(
|
excrepr = excinfo.getrepr(
|
||||||
|
@ -1736,6 +1735,7 @@ class Config:
|
||||||
can be used to explicitly use the global verbosity level.
|
can be used to explicitly use the global verbosity level.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
# content of pytest.ini
|
# content of pytest.ini
|
||||||
|
@ -1913,7 +1913,7 @@ def parse_warning_filter(
|
||||||
parts.append("")
|
parts.append("")
|
||||||
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
|
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
|
||||||
try:
|
try:
|
||||||
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
|
action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||||
except warnings._OptionError as e:
|
except warnings._OptionError as e:
|
||||||
raise UsageError(error_template.format(error=str(e))) from None
|
raise UsageError(error_template.format(error=str(e))) from None
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -13,12 +13,12 @@ from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest._code import ExceptionInfo
|
from _pytest._code import ExceptionInfo
|
||||||
|
from _pytest.capture import CaptureManager
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ConftestImportFailure
|
from _pytest.config import ConftestImportFailure
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
|
@ -27,11 +27,7 @@ from _pytest.config.argparsing import Parser
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
from _pytest.nodes import Node
|
from _pytest.nodes import Node
|
||||||
from _pytest.reports import BaseReport
|
from _pytest.reports import BaseReport
|
||||||
|
from _pytest.runner import CallInfo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from _pytest.capture import CaptureManager
|
|
||||||
from _pytest.runner import CallInfo
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
|
def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
|
||||||
|
@ -310,7 +306,7 @@ class PdbTrace:
|
||||||
return (yield)
|
return (yield)
|
||||||
|
|
||||||
|
|
||||||
def wrap_pytest_function_for_tracing(pyfuncitem):
|
def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
|
||||||
"""Change the Python function object of the given Function item by a
|
"""Change the Python function object of the given Function item by a
|
||||||
wrapper which actually enters pdb before calling the python function
|
wrapper which actually enters pdb before calling the python function
|
||||||
itself, effectively leaving the user in the pdb prompt in the first
|
itself, effectively leaving the user in the pdb prompt in the first
|
||||||
|
@ -322,14 +318,14 @@ def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||||
# python < 3.7.4) runcall's first param is `func`, which means we'd get
|
# python < 3.7.4) runcall's first param is `func`, which means we'd get
|
||||||
# an exception if one of the kwargs to testfunction was called `func`.
|
# an exception if one of the kwargs to testfunction was called `func`.
|
||||||
@functools.wraps(testfunction)
|
@functools.wraps(testfunction)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs) -> None:
|
||||||
func = functools.partial(testfunction, *args, **kwargs)
|
func = functools.partial(testfunction, *args, **kwargs)
|
||||||
_pdb.runcall(func)
|
_pdb.runcall(func)
|
||||||
|
|
||||||
pyfuncitem.obj = wrapper
|
pyfuncitem.obj = wrapper
|
||||||
|
|
||||||
|
|
||||||
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
|
def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
|
||||||
"""Wrap the given pytestfunct item for tracing support if --trace was given in
|
"""Wrap the given pytestfunct item for tracing support if --trace was given in
|
||||||
the command line."""
|
the command line."""
|
||||||
if pyfuncitem.config.getvalue("trace"):
|
if pyfuncitem.config.getvalue("trace"):
|
||||||
|
|
|
@ -298,7 +298,7 @@ class DoctestItem(Item):
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
_check_all_skipped(self.dtest)
|
_check_all_skipped(self.dtest)
|
||||||
self._disable_output_capturing_for_darwin()
|
self._disable_output_capturing_for_darwin()
|
||||||
failures: List["doctest.DocTestFailure"] = []
|
failures: List[doctest.DocTestFailure] = []
|
||||||
# Type ignored because we change the type of `out` from what
|
# Type ignored because we change the type of `out` from what
|
||||||
# doctest expects.
|
# doctest expects.
|
||||||
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
|
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
|
||||||
|
@ -505,43 +505,46 @@ class DoctestModule(Module):
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
class MockAwareDocTestFinder(doctest.DocTestFinder):
|
class MockAwareDocTestFinder(doctest.DocTestFinder):
|
||||||
"""A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
|
if sys.version_info < (3, 11):
|
||||||
|
|
||||||
https://github.com/pytest-dev/pytest/issues/3456
|
def _find_lineno(self, obj, source_lines):
|
||||||
https://bugs.python.org/issue25532
|
"""On older Pythons, doctest code does not take into account
|
||||||
"""
|
`@property`. https://github.com/python/cpython/issues/61648
|
||||||
|
|
||||||
def _find_lineno(self, obj, source_lines):
|
Moreover, wrapped Doctests need to be unwrapped so the correct
|
||||||
"""Doctest code does not take into account `@property`, this
|
line number is returned. #8796
|
||||||
is a hackish way to fix it. https://bugs.python.org/issue17446
|
"""
|
||||||
|
if isinstance(obj, property):
|
||||||
|
obj = getattr(obj, "fget", obj)
|
||||||
|
|
||||||
Wrapped Doctests will need to be unwrapped so the correct
|
if hasattr(obj, "__wrapped__"):
|
||||||
line number is returned. This will be reported upstream. #8796
|
# Get the main obj in case of it being wrapped
|
||||||
"""
|
obj = inspect.unwrap(obj)
|
||||||
if isinstance(obj, property):
|
|
||||||
obj = getattr(obj, "fget", obj)
|
|
||||||
|
|
||||||
if hasattr(obj, "__wrapped__"):
|
|
||||||
# Get the main obj in case of it being wrapped
|
|
||||||
obj = inspect.unwrap(obj)
|
|
||||||
|
|
||||||
# Type ignored because this is a private function.
|
|
||||||
return super()._find_lineno( # type:ignore[misc]
|
|
||||||
obj,
|
|
||||||
source_lines,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _find(
|
|
||||||
self, tests, obj, name, module, source_lines, globs, seen
|
|
||||||
) -> None:
|
|
||||||
if _is_mocked(obj):
|
|
||||||
return
|
|
||||||
with _patch_unwrap_mock_aware():
|
|
||||||
# Type ignored because this is a private function.
|
# Type ignored because this is a private function.
|
||||||
super()._find( # type:ignore[misc]
|
return super()._find_lineno( # type:ignore[misc]
|
||||||
tests, obj, name, module, source_lines, globs, seen
|
obj,
|
||||||
|
source_lines,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
|
||||||
|
def _find(
|
||||||
|
self, tests, obj, name, module, source_lines, globs, seen
|
||||||
|
) -> None:
|
||||||
|
"""Override _find to work around issue in stdlib.
|
||||||
|
|
||||||
|
https://github.com/pytest-dev/pytest/issues/3456
|
||||||
|
https://github.com/python/cpython/issues/69718
|
||||||
|
"""
|
||||||
|
if _is_mocked(obj):
|
||||||
|
return # pragma: no cover
|
||||||
|
with _patch_unwrap_mock_aware():
|
||||||
|
# Type ignored because this is a private function.
|
||||||
|
super()._find( # type:ignore[misc]
|
||||||
|
tests, obj, name, module, source_lines, globs, seen
|
||||||
|
)
|
||||||
|
|
||||||
if sys.version_info < (3, 13):
|
if sys.version_info < (3, 13):
|
||||||
|
|
||||||
def _from_module(self, module, object):
|
def _from_module(self, module, object):
|
||||||
|
@ -556,9 +559,6 @@ class DoctestModule(Module):
|
||||||
# Type ignored because this is a private function.
|
# Type ignored because this is a private function.
|
||||||
return super()._from_module(module, object) # type: ignore[misc]
|
return super()._from_module(module, object) # type: ignore[misc]
|
||||||
|
|
||||||
else: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
module = self.obj
|
module = self.obj
|
||||||
except Collector.CollectError:
|
except Collector.CollectError:
|
||||||
|
|
|
@ -21,9 +21,11 @@ from typing import Generic
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
from typing import MutableMapping
|
from typing import MutableMapping
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import OrderedDict
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
@ -58,6 +60,7 @@ from _pytest.config.argparsing import Parser
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.deprecated import MARKED_FIXTURE
|
from _pytest.deprecated import MARKED_FIXTURE
|
||||||
from _pytest.deprecated import YIELD_FIXTURE
|
from _pytest.deprecated import YIELD_FIXTURE
|
||||||
|
from _pytest.main import Session
|
||||||
from _pytest.mark import Mark
|
from _pytest.mark import Mark
|
||||||
from _pytest.mark import ParameterSet
|
from _pytest.mark import ParameterSet
|
||||||
from _pytest.mark.structures import MarkDecorator
|
from _pytest.mark.structures import MarkDecorator
|
||||||
|
@ -76,9 +79,6 @@ if sys.version_info < (3, 11):
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Deque
|
|
||||||
|
|
||||||
from _pytest.main import Session
|
|
||||||
from _pytest.python import CallSpec2
|
from _pytest.python import CallSpec2
|
||||||
from _pytest.python import Function
|
from _pytest.python import Function
|
||||||
from _pytest.python import Metafunc
|
from _pytest.python import Metafunc
|
||||||
|
@ -161,6 +161,12 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Algorithm for sorting on a per-parametrized resource setup basis.
|
||||||
|
# It is called for Session scope first and performs sorting
|
||||||
|
# down to the lower scopes such as to minimize number of "high scope"
|
||||||
|
# setups and teardowns.
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class FixtureArgKey:
|
class FixtureArgKey:
|
||||||
argname: str
|
argname: str
|
||||||
|
@ -169,96 +175,95 @@ class FixtureArgKey:
|
||||||
item_cls: Optional[type]
|
item_cls: Optional[type]
|
||||||
|
|
||||||
|
|
||||||
def get_parametrized_fixture_keys(
|
_V = TypeVar("_V")
|
||||||
|
OrderedSet = Dict[_V, None]
|
||||||
|
|
||||||
|
|
||||||
|
def get_parametrized_fixture_argkeys(
|
||||||
item: nodes.Item, scope: Scope
|
item: nodes.Item, scope: Scope
|
||||||
) -> Iterator[FixtureArgKey]:
|
) -> Iterator[FixtureArgKey]:
|
||||||
"""Return list of keys for all parametrized arguments which match
|
"""Return list of keys for all parametrized arguments which match
|
||||||
the specified scope."""
|
the specified scope."""
|
||||||
assert scope is not Scope.Function
|
assert scope is not Scope.Function
|
||||||
|
|
||||||
try:
|
try:
|
||||||
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
|
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
item_cls = None
|
||||||
|
if scope is Scope.Session:
|
||||||
|
scoped_item_path = None
|
||||||
|
elif scope is Scope.Package:
|
||||||
|
# Package key = module's directory.
|
||||||
|
scoped_item_path = item.path.parent
|
||||||
|
elif scope is Scope.Module:
|
||||||
|
scoped_item_path = item.path
|
||||||
|
elif scope is Scope.Class:
|
||||||
|
scoped_item_path = item.path
|
||||||
|
item_cls = item.cls # type: ignore[attr-defined]
|
||||||
|
else:
|
||||||
|
assert_never(scope)
|
||||||
|
|
||||||
for argname in callspec.indices:
|
for argname in callspec.indices:
|
||||||
if callspec._arg2scope[argname] != scope:
|
if callspec._arg2scope[argname] != scope:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item_cls = None
|
|
||||||
if scope is Scope.Session:
|
|
||||||
scoped_item_path = None
|
|
||||||
elif scope is Scope.Package:
|
|
||||||
scoped_item_path = item.path
|
|
||||||
elif scope is Scope.Module:
|
|
||||||
scoped_item_path = item.path
|
|
||||||
elif scope is Scope.Class:
|
|
||||||
scoped_item_path = item.path
|
|
||||||
item_cls = item.cls # type: ignore[attr-defined]
|
|
||||||
else:
|
|
||||||
assert_never(scope)
|
|
||||||
|
|
||||||
param_index = callspec.indices[argname]
|
param_index = callspec.indices[argname]
|
||||||
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
||||||
|
|
||||||
|
|
||||||
# Algorithm for sorting on a per-parametrized resource setup basis.
|
|
||||||
# It is called for Session scope first and performs sorting
|
|
||||||
# down to the lower scopes such as to minimize number of "high scope"
|
|
||||||
# setups and teardowns.
|
|
||||||
|
|
||||||
|
|
||||||
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
argkeys_by_item: Dict[Scope, Dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
|
||||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
items_by_argkey: Dict[
|
||||||
|
Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
|
||||||
|
] = {}
|
||||||
for scope in HIGH_SCOPES:
|
for scope in HIGH_SCOPES:
|
||||||
scoped_argkeys_cache = argkeys_cache[scope] = {}
|
scoped_argkeys_by_item = argkeys_by_item[scope] = {}
|
||||||
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
|
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
|
||||||
for item in items:
|
for item in items:
|
||||||
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope))
|
||||||
if keys:
|
if argkeys:
|
||||||
scoped_argkeys_cache[item] = keys
|
scoped_argkeys_by_item[item] = argkeys
|
||||||
for key in keys:
|
for argkey in argkeys:
|
||||||
scoped_items_by_argkey[key].append(item)
|
scoped_items_by_argkey[argkey][item] = None
|
||||||
items_dict = dict.fromkeys(items, None)
|
|
||||||
|
items_set = dict.fromkeys(items)
|
||||||
return list(
|
return list(
|
||||||
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
reorder_items_atscope(
|
||||||
|
items_set, argkeys_by_item, items_by_argkey, Scope.Session
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def fix_cache_order(
|
|
||||||
item: nodes.Item,
|
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
|
||||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
|
|
||||||
) -> None:
|
|
||||||
for scope in HIGH_SCOPES:
|
|
||||||
for key in argkeys_cache[scope].get(item, []):
|
|
||||||
items_by_argkey[scope][key].appendleft(item)
|
|
||||||
|
|
||||||
|
|
||||||
def reorder_items_atscope(
|
def reorder_items_atscope(
|
||||||
items: Dict[nodes.Item, None],
|
items: OrderedSet[nodes.Item],
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]],
|
||||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
|
items_by_argkey: Mapping[
|
||||||
|
Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]]
|
||||||
|
],
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
) -> Dict[nodes.Item, None]:
|
) -> OrderedSet[nodes.Item]:
|
||||||
if scope is Scope.Function or len(items) < 3:
|
if scope is Scope.Function or len(items) < 3:
|
||||||
return items
|
return items
|
||||||
ignore: Set[Optional[FixtureArgKey]] = set()
|
|
||||||
items_deque = deque(items)
|
|
||||||
items_done: Dict[nodes.Item, None] = {}
|
|
||||||
scoped_items_by_argkey = items_by_argkey[scope]
|
scoped_items_by_argkey = items_by_argkey[scope]
|
||||||
scoped_argkeys_cache = argkeys_cache[scope]
|
scoped_argkeys_by_item = argkeys_by_item[scope]
|
||||||
|
|
||||||
|
ignore: Set[FixtureArgKey] = set()
|
||||||
|
items_deque = deque(items)
|
||||||
|
items_done: OrderedSet[nodes.Item] = {}
|
||||||
while items_deque:
|
while items_deque:
|
||||||
no_argkey_group: Dict[nodes.Item, None] = {}
|
no_argkey_items: OrderedSet[nodes.Item] = {}
|
||||||
slicing_argkey = None
|
slicing_argkey = None
|
||||||
while items_deque:
|
while items_deque:
|
||||||
item = items_deque.popleft()
|
item = items_deque.popleft()
|
||||||
if item in items_done or item in no_argkey_group:
|
if item in items_done or item in no_argkey_items:
|
||||||
continue
|
continue
|
||||||
argkeys = dict.fromkeys(
|
argkeys = dict.fromkeys(
|
||||||
(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None
|
k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore
|
||||||
)
|
)
|
||||||
if not argkeys:
|
if not argkeys:
|
||||||
no_argkey_group[item] = None
|
no_argkey_items[item] = None
|
||||||
else:
|
else:
|
||||||
slicing_argkey, _ = argkeys.popitem()
|
slicing_argkey, _ = argkeys.popitem()
|
||||||
# We don't have to remove relevant items from later in the
|
# We don't have to remove relevant items from later in the
|
||||||
|
@ -267,16 +272,23 @@ def reorder_items_atscope(
|
||||||
i for i in scoped_items_by_argkey[slicing_argkey] if i in items
|
i for i in scoped_items_by_argkey[slicing_argkey] if i in items
|
||||||
]
|
]
|
||||||
for i in reversed(matching_items):
|
for i in reversed(matching_items):
|
||||||
fix_cache_order(i, argkeys_cache, items_by_argkey)
|
|
||||||
items_deque.appendleft(i)
|
items_deque.appendleft(i)
|
||||||
|
# Fix items_by_argkey order.
|
||||||
|
for other_scope in HIGH_SCOPES:
|
||||||
|
other_scoped_items_by_argkey = items_by_argkey[other_scope]
|
||||||
|
for argkey in argkeys_by_item[other_scope].get(i, ()):
|
||||||
|
other_scoped_items_by_argkey[argkey][i] = None
|
||||||
|
other_scoped_items_by_argkey[argkey].move_to_end(
|
||||||
|
i, last=False
|
||||||
|
)
|
||||||
break
|
break
|
||||||
if no_argkey_group:
|
if no_argkey_items:
|
||||||
no_argkey_group = reorder_items_atscope(
|
reordered_no_argkey_items = reorder_items_atscope(
|
||||||
no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower()
|
no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower()
|
||||||
)
|
)
|
||||||
for item in no_argkey_group:
|
items_done.update(reordered_no_argkey_items)
|
||||||
items_done[item] = None
|
if slicing_argkey is not None:
|
||||||
ignore.add(slicing_argkey)
|
ignore.add(slicing_argkey)
|
||||||
return items_done
|
return items_done
|
||||||
|
|
||||||
|
|
||||||
|
@ -326,7 +338,7 @@ class FuncFixtureInfo:
|
||||||
working_set = set(self.initialnames)
|
working_set = set(self.initialnames)
|
||||||
while working_set:
|
while working_set:
|
||||||
argname = working_set.pop()
|
argname = working_set.pop()
|
||||||
# Argname may be smth not included in the original names_closure,
|
# Argname may be something not included in the original names_closure,
|
||||||
# in which case we ignore it. This currently happens with pseudo
|
# in which case we ignore it. This currently happens with pseudo
|
||||||
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
|
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
|
||||||
# So they introduce the new dependency 'request' which might have
|
# So they introduce the new dependency 'request' which might have
|
||||||
|
@ -1624,7 +1636,7 @@ class FixtureManager:
|
||||||
func: "_FixtureFunc[object]",
|
func: "_FixtureFunc[object]",
|
||||||
nodeid: Optional[str],
|
nodeid: Optional[str],
|
||||||
scope: Union[
|
scope: Union[
|
||||||
Scope, _ScopeName, Callable[[str, Config], _ScopeName], None
|
Scope, _ScopeName, Callable[[str, Config], _ScopeName]
|
||||||
] = "function",
|
] = "function",
|
||||||
params: Optional[Sequence[object]] = None,
|
params: Optional[Sequence[object]] = None,
|
||||||
ids: Optional[
|
ids: Optional[
|
||||||
|
@ -1702,7 +1714,7 @@ class FixtureManager:
|
||||||
|
|
||||||
If `node_or_object` is a collection node (with an underlying Python
|
If `node_or_object` is a collection node (with an underlying Python
|
||||||
object), the node's object is traversed and the node's nodeid is used to
|
object), the node's object is traversed and the node's nodeid is used to
|
||||||
determine the fixtures' visibilty. `nodeid` must not be specified in
|
determine the fixtures' visibility. `nodeid` must not be specified in
|
||||||
this case.
|
this case.
|
||||||
|
|
||||||
If `node_or_object` is an object (e.g. a plugin), the object is
|
If `node_or_object` is an object (e.g. a plugin), the object is
|
||||||
|
@ -1820,7 +1832,10 @@ def _show_fixtures_per_test(config: Config, session: "Session") -> None:
|
||||||
fixture_doc = inspect.getdoc(fixture_def.func)
|
fixture_doc = inspect.getdoc(fixture_def.func)
|
||||||
if fixture_doc:
|
if fixture_doc:
|
||||||
write_docstring(
|
write_docstring(
|
||||||
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
|
tw,
|
||||||
|
fixture_doc.split("\n\n", maxsplit=1)[0]
|
||||||
|
if verbose <= 0
|
||||||
|
else fixture_doc,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tw.line(" no docstring available", red=True)
|
tw.line(" no docstring available", red=True)
|
||||||
|
@ -1902,7 +1917,9 @@ def _showfixtures_main(config: Config, session: "Session") -> None:
|
||||||
tw.write("\n")
|
tw.write("\n")
|
||||||
doc = inspect.getdoc(fixturedef.func)
|
doc = inspect.getdoc(fixturedef.func)
|
||||||
if doc:
|
if doc:
|
||||||
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
|
write_docstring(
|
||||||
|
tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
tw.line(" no docstring available", red=True)
|
tw.line(" no docstring available", red=True)
|
||||||
tw.line()
|
tw.line()
|
||||||
|
|
|
@ -311,7 +311,12 @@ def pytest_collection_finish(session: "Session") -> None:
|
||||||
def pytest_ignore_collect(
|
def pytest_ignore_collect(
|
||||||
collection_path: Path, path: "LEGACY_PATH", config: "Config"
|
collection_path: Path, path: "LEGACY_PATH", config: "Config"
|
||||||
) -> Optional[bool]:
|
) -> Optional[bool]:
|
||||||
"""Return True to prevent considering this path for collection.
|
"""Return ``True`` to ignore this path for collection.
|
||||||
|
|
||||||
|
Return ``None`` to let other plugins ignore the path for collection.
|
||||||
|
|
||||||
|
Returning ``False`` will forcefully *not* ignore this path for collection,
|
||||||
|
without giving a chance for other plugins to ignore this path.
|
||||||
|
|
||||||
This hook is consulted for all files and directories prior to calling
|
This hook is consulted for all files and directories prior to calling
|
||||||
more specific hooks.
|
more specific hooks.
|
||||||
|
@ -651,7 +656,7 @@ def pytest_runtest_protocol(
|
||||||
- ``pytest_runtest_logreport(report)``
|
- ``pytest_runtest_logreport(report)``
|
||||||
- ``pytest_exception_interact(call, report)`` if an interactive exception occurred
|
- ``pytest_exception_interact(call, report)`` if an interactive exception occurred
|
||||||
|
|
||||||
- Call phase, if the the setup passed and the ``setuponly`` pytest option is not set:
|
- Call phase, if the setup passed and the ``setuponly`` pytest option is not set:
|
||||||
- ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``)
|
- ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``)
|
||||||
- ``report = pytest_runtest_makereport(item, call)``
|
- ``report = pytest_runtest_makereport(item, call)``
|
||||||
- ``pytest_runtest_logreport(report)``
|
- ``pytest_runtest_logreport(report)``
|
||||||
|
@ -861,7 +866,7 @@ def pytest_fixture_setup(
|
||||||
) -> Optional[object]:
|
) -> Optional[object]:
|
||||||
"""Perform fixture setup execution.
|
"""Perform fixture setup execution.
|
||||||
|
|
||||||
:param fixturdef:
|
:param fixturedef:
|
||||||
The fixture definition object.
|
The fixture definition object.
|
||||||
:param request:
|
:param request:
|
||||||
The fixture request object.
|
The fixture request object.
|
||||||
|
@ -891,7 +896,7 @@ def pytest_fixture_post_finalizer(
|
||||||
the fixture result ``fixturedef.cached_result`` is still available (not
|
the fixture result ``fixturedef.cached_result`` is still available (not
|
||||||
``None``).
|
``None``).
|
||||||
|
|
||||||
:param fixturdef:
|
:param fixturedef:
|
||||||
The fixture definition object.
|
The fixture definition object.
|
||||||
:param request:
|
:param request:
|
||||||
The fixture request object.
|
The fixture request object.
|
||||||
|
|
|
@ -384,7 +384,7 @@ def Config_inifile(self: Config) -> Optional[LEGACY_PATH]:
|
||||||
return legacy_path(str(self.inipath)) if self.inipath else None
|
return legacy_path(str(self.inipath)) if self.inipath else None
|
||||||
|
|
||||||
|
|
||||||
def Session_stardir(self: Session) -> LEGACY_PATH:
|
def Session_startdir(self: Session) -> LEGACY_PATH:
|
||||||
"""The path from which pytest was invoked.
|
"""The path from which pytest was invoked.
|
||||||
|
|
||||||
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
|
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
|
||||||
|
@ -439,7 +439,7 @@ def pytest_load_initial_conftests(early_config: Config) -> None:
|
||||||
mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
|
mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
|
||||||
|
|
||||||
# Add Session.startdir property.
|
# Add Session.startdir property.
|
||||||
mp.setattr(Session, "startdir", property(Session_stardir), raising=False)
|
mp.setattr(Session, "startdir", property(Session_startdir), raising=False)
|
||||||
|
|
||||||
# Add pathlist configuration type.
|
# Add pathlist configuration type.
|
||||||
mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)
|
mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)
|
||||||
|
|
|
@ -401,7 +401,7 @@ class LogCaptureHandler(logging_StreamHandler):
|
||||||
# The default behavior of logging is to print "Logging error"
|
# The default behavior of logging is to print "Logging error"
|
||||||
# to stderr with the call stack and some extra details.
|
# to stderr with the call stack and some extra details.
|
||||||
# pytest wants to make such mistakes visible during testing.
|
# pytest wants to make such mistakes visible during testing.
|
||||||
raise
|
raise # pylint: disable=misplaced-bare-raise
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
|
|
@ -38,7 +38,6 @@ from _pytest.config import PytestPluginManager
|
||||||
from _pytest.config import UsageError
|
from _pytest.config import UsageError
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.config.compat import PathAwareHookProxy
|
from _pytest.config.compat import PathAwareHookProxy
|
||||||
from _pytest.fixtures import FixtureManager
|
|
||||||
from _pytest.outcomes import exit
|
from _pytest.outcomes import exit
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
|
@ -55,6 +54,8 @@ from _pytest.warning_types import PytestWarning
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
|
from _pytest.fixtures import FixtureManager
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
parser.addini(
|
parser.addini(
|
||||||
|
@ -551,7 +552,7 @@ class Session(nodes.Collector):
|
||||||
# Set on the session by runner.pytest_sessionstart.
|
# Set on the session by runner.pytest_sessionstart.
|
||||||
_setupstate: SetupState
|
_setupstate: SetupState
|
||||||
# Set on the session by fixtures.pytest_sessionstart.
|
# Set on the session by fixtures.pytest_sessionstart.
|
||||||
_fixturemanager: FixtureManager
|
_fixturemanager: "FixtureManager"
|
||||||
exitstatus: Union[int, ExitCode]
|
exitstatus: Union[int, ExitCode]
|
||||||
|
|
||||||
def __init__(self, config: Config) -> None:
|
def __init__(self, config: Config) -> None:
|
||||||
|
|
|
@ -78,7 +78,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
default="",
|
default="",
|
||||||
metavar="EXPRESSION",
|
metavar="EXPRESSION",
|
||||||
help="Only run tests which match the given substring expression. "
|
help="Only run tests which match the given substring expression. "
|
||||||
"An expression is a Python evaluatable expression "
|
"An expression is a Python evaluable expression "
|
||||||
"where all names are substring-matched against test names "
|
"where all names are substring-matched against test names "
|
||||||
"and their parent classes. Example: -k 'test_method or test_"
|
"and their parent classes. Example: -k 'test_method or test_"
|
||||||
"other' matches all test functions and classes whose name "
|
"other' matches all test functions and classes whose name "
|
||||||
|
|
|
@ -31,6 +31,7 @@ from _pytest.config import Config
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.deprecated import MARKED_FIXTURE
|
from _pytest.deprecated import MARKED_FIXTURE
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
|
from _pytest.scope import _ScopeName
|
||||||
from _pytest.warning_types import PytestUnknownMarkWarning
|
from _pytest.warning_types import PytestUnknownMarkWarning
|
||||||
|
|
||||||
|
|
||||||
|
@ -430,7 +431,6 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
|
||||||
# Typing for builtin pytest marks. This is cheating; it gives builtin marks
|
# Typing for builtin pytest marks. This is cheating; it gives builtin marks
|
||||||
# special privilege, and breaks modularity. But practicality beats purity...
|
# special privilege, and breaks modularity. But practicality beats purity...
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.scope import _ScopeName
|
|
||||||
|
|
||||||
class _SkipMarkDecorator(MarkDecorator):
|
class _SkipMarkDecorator(MarkDecorator):
|
||||||
@overload # type: ignore[override,no-overload-impl]
|
@overload # type: ignore[override,no-overload-impl]
|
||||||
|
|
|
@ -142,6 +142,7 @@ class MonkeyPatch:
|
||||||
which undoes any patching done inside the ``with`` block upon exit.
|
which undoes any patching done inside the ``with`` block upon exit.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
|
|
@ -30,6 +30,7 @@ from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest._code.code import Traceback
|
from _pytest._code.code import Traceback
|
||||||
|
from _pytest._code.code import TracebackStyle
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ConftestImportFailure
|
from _pytest.config import ConftestImportFailure
|
||||||
|
@ -49,7 +50,6 @@ if TYPE_CHECKING:
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
# Imported here due to circular import.
|
# Imported here due to circular import.
|
||||||
from _pytest._code.code import _TracebackStyle
|
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
|
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
def _repr_failure_py(
|
def _repr_failure_py(
|
||||||
self,
|
self,
|
||||||
excinfo: ExceptionInfo[BaseException],
|
excinfo: ExceptionInfo[BaseException],
|
||||||
style: "Optional[_TracebackStyle]" = None,
|
style: "Optional[TracebackStyle]" = None,
|
||||||
) -> TerminalRepr:
|
) -> TerminalRepr:
|
||||||
from _pytest.fixtures import FixtureLookupError
|
from _pytest.fixtures import FixtureLookupError
|
||||||
|
|
||||||
|
@ -448,6 +448,8 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
else:
|
else:
|
||||||
truncate_locals = True
|
truncate_locals = True
|
||||||
|
|
||||||
|
truncate_args = False if self.config.getoption("verbose", 0) > 2 else True
|
||||||
|
|
||||||
# excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
|
# excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
|
||||||
# It is possible for a fixture/test to change the CWD while this code runs, which
|
# It is possible for a fixture/test to change the CWD while this code runs, which
|
||||||
# would then result in the user seeing confusing paths in the failure message.
|
# would then result in the user seeing confusing paths in the failure message.
|
||||||
|
@ -466,12 +468,13 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
style=style,
|
style=style,
|
||||||
tbfilter=tbfilter,
|
tbfilter=tbfilter,
|
||||||
truncate_locals=truncate_locals,
|
truncate_locals=truncate_locals,
|
||||||
|
truncate_args=truncate_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
def repr_failure(
|
def repr_failure(
|
||||||
self,
|
self,
|
||||||
excinfo: ExceptionInfo[BaseException],
|
excinfo: ExceptionInfo[BaseException],
|
||||||
style: "Optional[_TracebackStyle]" = None,
|
style: "Optional[TracebackStyle]" = None,
|
||||||
) -> Union[str, TerminalRepr]:
|
) -> Union[str, TerminalRepr]:
|
||||||
"""Return a representation of a collection or test failure.
|
"""Return a representation of a collection or test failure.
|
||||||
|
|
||||||
|
|
|
@ -173,7 +173,7 @@ def rm_rf(path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]:
|
def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]:
|
||||||
"""Find all elements in root that begin with the prefix, case insensitive."""
|
"""Find all elements in root that begin with the prefix, case-insensitive."""
|
||||||
l_prefix = prefix.lower()
|
l_prefix = prefix.lower()
|
||||||
for x in os.scandir(root):
|
for x in os.scandir(root):
|
||||||
if x.name.lower().startswith(l_prefix):
|
if x.name.lower().startswith(l_prefix):
|
||||||
|
@ -776,7 +776,7 @@ def resolve_package_path(path: Path) -> Optional[Path]:
|
||||||
"""Return the Python package path by looking for the last
|
"""Return the Python package path by looking for the last
|
||||||
directory upwards which still contains an __init__.py.
|
directory upwards which still contains an __init__.py.
|
||||||
|
|
||||||
Returns None if it can not be determined.
|
Returns None if it cannot be determined.
|
||||||
"""
|
"""
|
||||||
result = None
|
result = None
|
||||||
for parent in itertools.chain((path,), path.parents):
|
for parent in itertools.chain((path,), path.parents):
|
||||||
|
|
|
@ -289,7 +289,8 @@ class HookRecorder:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
i = 0
|
i = 0
|
||||||
entries = list(entries)
|
entries = list(entries)
|
||||||
backlocals = sys._getframe(1).f_locals
|
# Since Python 3.13, f_locals is not a dict, but eval requires a dict.
|
||||||
|
backlocals = dict(sys._getframe(1).f_locals)
|
||||||
while entries:
|
while entries:
|
||||||
name, check = entries.pop(0)
|
name, check = entries.pop(0)
|
||||||
for ind, call in enumerate(self.calls[i:]):
|
for ind, call in enumerate(self.calls[i:]):
|
||||||
|
@ -760,6 +761,9 @@ class Pytester:
|
||||||
) -> Path:
|
) -> Path:
|
||||||
items = list(files.items())
|
items = list(files.items())
|
||||||
|
|
||||||
|
if ext is None:
|
||||||
|
raise TypeError("ext must not be None")
|
||||||
|
|
||||||
if ext and not ext.startswith("."):
|
if ext and not ext.startswith("."):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
|
f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
|
||||||
|
@ -801,6 +805,7 @@ class Pytester:
|
||||||
The first created file.
|
The first created file.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
pytester.makefile(".txt", "line1", "line2")
|
pytester.makefile(".txt", "line1", "line2")
|
||||||
|
@ -854,6 +859,7 @@ class Pytester:
|
||||||
existing files.
|
existing files.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_something(pytester):
|
def test_something(pytester):
|
||||||
|
@ -873,6 +879,7 @@ class Pytester:
|
||||||
existing files.
|
existing files.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_something(pytester):
|
def test_something(pytester):
|
||||||
|
|
|
@ -176,7 +176,12 @@ def pytest_collect_directory(
|
||||||
path: Path, parent: nodes.Collector
|
path: Path, parent: nodes.Collector
|
||||||
) -> Optional[nodes.Collector]:
|
) -> Optional[nodes.Collector]:
|
||||||
pkginit = path / "__init__.py"
|
pkginit = path / "__init__.py"
|
||||||
if pkginit.is_file():
|
try:
|
||||||
|
has_pkginit = pkginit.is_file()
|
||||||
|
except PermissionError:
|
||||||
|
# See https://github.com/pytest-dev/pytest/issues/12120#issuecomment-2106349096.
|
||||||
|
return None
|
||||||
|
if has_pkginit:
|
||||||
return Package.from_parent(parent, path=path)
|
return Package.from_parent(parent, path=path)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -368,7 +373,11 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def istestclass(self, obj: object, name: str) -> bool:
|
def istestclass(self, obj: object, name: str) -> bool:
|
||||||
return self.classnamefilter(name) or self.isnosetest(obj)
|
if not (self.classnamefilter(name) or self.isnosetest(obj)):
|
||||||
|
return False
|
||||||
|
if inspect.isabstract(obj):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
|
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
|
||||||
"""Check if the given name matches the prefix or glob-pattern defined
|
"""Check if the given name matches the prefix or glob-pattern defined
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
|
from _pytest.cacheprovider import Cache
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from _pytest.cacheprovider import Cache
|
|
||||||
|
|
||||||
STEPWISE_CACHE_DIR = "cache/stepwise"
|
STEPWISE_CACHE_DIR = "cache/stepwise"
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,10 +33,9 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl
|
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
if config.option.stepwise_skip:
|
if config.option.stepwise_skip:
|
||||||
# allow --stepwise-skip to work on it's own merits.
|
# allow --stepwise-skip to work on its own merits.
|
||||||
config.option.stepwise = True
|
config.option.stepwise = True
|
||||||
if config.getoption("stepwise"):
|
if config.getoption("stepwise"):
|
||||||
config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
|
config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
|
||||||
|
|
|
@ -34,8 +34,8 @@ class catch_threading_exception:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.args: Optional["threading.ExceptHookArgs"] = None
|
self.args: Optional[threading.ExceptHookArgs] = None
|
||||||
self._old_hook: Optional[Callable[["threading.ExceptHookArgs"], Any]] = None
|
self._old_hook: Optional[Callable[[threading.ExceptHookArgs], Any]] = None
|
||||||
|
|
||||||
def _hook(self, args: "threading.ExceptHookArgs") -> None:
|
def _hook(self, args: "threading.ExceptHookArgs") -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Discover and run std-library "unittest" style tests."""
|
"""Discover and run std-library "unittest" style tests."""
|
||||||
|
|
||||||
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
|
@ -40,23 +41,29 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
import twisted.trial.unittest
|
import twisted.trial.unittest
|
||||||
|
|
||||||
_SysExcInfoType = Union[
|
|
||||||
Tuple[Type[BaseException], BaseException, types.TracebackType],
|
_SysExcInfoType = Union[
|
||||||
Tuple[None, None, None],
|
Tuple[Type[BaseException], BaseException, types.TracebackType],
|
||||||
]
|
Tuple[None, None, None],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def pytest_pycollect_makeitem(
|
def pytest_pycollect_makeitem(
|
||||||
collector: Union[Module, Class], name: str, obj: object
|
collector: Union[Module, Class], name: str, obj: object
|
||||||
) -> Optional["UnitTestCase"]:
|
) -> Optional["UnitTestCase"]:
|
||||||
# Has unittest been imported and is obj a subclass of its TestCase?
|
|
||||||
try:
|
try:
|
||||||
|
# Has unittest been imported?
|
||||||
ut = sys.modules["unittest"]
|
ut = sys.modules["unittest"]
|
||||||
|
# Is obj a subclass of unittest.TestCase?
|
||||||
# Type ignored because `ut` is an opaque module.
|
# Type ignored because `ut` is an opaque module.
|
||||||
if not issubclass(obj, ut.TestCase): # type: ignore
|
if not issubclass(obj, ut.TestCase): # type: ignore
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
# Is obj a concrete class?
|
||||||
|
# Abstract classes can't be instantiated so no point collecting them.
|
||||||
|
if inspect.isabstract(obj):
|
||||||
|
return None
|
||||||
# Yes, so let's collect it.
|
# Yes, so let's collect it.
|
||||||
return UnitTestCase.from_parent(collector, name=name, obj=obj)
|
return UnitTestCase.from_parent(collector, name=name, obj=obj)
|
||||||
|
|
||||||
|
@ -212,16 +219,17 @@ class TestCaseFunction(Function):
|
||||||
super().setup()
|
super().setup()
|
||||||
|
|
||||||
def teardown(self) -> None:
|
def teardown(self) -> None:
|
||||||
super().teardown()
|
|
||||||
if self._explicit_tearDown is not None:
|
if self._explicit_tearDown is not None:
|
||||||
self._explicit_tearDown()
|
self._explicit_tearDown()
|
||||||
self._explicit_tearDown = None
|
self._explicit_tearDown = None
|
||||||
self._obj = None
|
self._obj = None
|
||||||
|
del self._instance
|
||||||
|
super().teardown()
|
||||||
|
|
||||||
def startTest(self, testcase: "unittest.TestCase") -> None:
|
def startTest(self, testcase: "unittest.TestCase") -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
|
def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None:
|
||||||
# Unwrap potential exception info (see twisted trial support below).
|
# Unwrap potential exception info (see twisted trial support below).
|
||||||
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
|
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
|
||||||
try:
|
try:
|
||||||
|
@ -257,7 +265,7 @@ class TestCaseFunction(Function):
|
||||||
self.__dict__.setdefault("_excinfo", []).append(excinfo)
|
self.__dict__.setdefault("_excinfo", []).append(excinfo)
|
||||||
|
|
||||||
def addError(
|
def addError(
|
||||||
self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
|
self, testcase: "unittest.TestCase", rawexcinfo: _SysExcInfoType
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
if isinstance(rawexcinfo[1], exit.Exception):
|
if isinstance(rawexcinfo[1], exit.Exception):
|
||||||
|
@ -267,7 +275,7 @@ class TestCaseFunction(Function):
|
||||||
self._addexcinfo(rawexcinfo)
|
self._addexcinfo(rawexcinfo)
|
||||||
|
|
||||||
def addFailure(
|
def addFailure(
|
||||||
self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
|
self, testcase: "unittest.TestCase", rawexcinfo: _SysExcInfoType
|
||||||
) -> None:
|
) -> None:
|
||||||
self._addexcinfo(rawexcinfo)
|
self._addexcinfo(rawexcinfo)
|
||||||
|
|
||||||
|
@ -280,7 +288,7 @@ class TestCaseFunction(Function):
|
||||||
def addExpectedFailure(
|
def addExpectedFailure(
|
||||||
self,
|
self,
|
||||||
testcase: "unittest.TestCase",
|
testcase: "unittest.TestCase",
|
||||||
rawexcinfo: "_SysExcInfoType",
|
rawexcinfo: _SysExcInfoType,
|
||||||
reason: str = "",
|
reason: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -34,8 +34,8 @@ class catch_unraisable_exception:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.unraisable: Optional["sys.UnraisableHookArgs"] = None
|
self.unraisable: Optional[sys.UnraisableHookArgs] = None
|
||||||
self._old_hook: Optional[Callable[["sys.UnraisableHookArgs"], Any]] = None
|
self._old_hook: Optional[Callable[[sys.UnraisableHookArgs], Any]] = None
|
||||||
|
|
||||||
def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None:
|
def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None:
|
||||||
# Storing unraisable.object can resurrect an object which is being
|
# Storing unraisable.object can resurrect an object which is being
|
||||||
|
|
|
@ -207,7 +207,7 @@ class CommonFSTests:
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"fil",
|
"fil",
|
||||||
["*dir", "*dir", pytest.mark.skip("sys.version_info <" " (3,6)")(b"*dir")],
|
["*dir", "*dir", pytest.mark.skip("sys.version_info < (3,6)")(b"*dir")],
|
||||||
)
|
)
|
||||||
def test_visit_filterfunc_is_string(self, path1, fil):
|
def test_visit_filterfunc_is_string(self, path1, fil):
|
||||||
lst = []
|
lst = []
|
||||||
|
@ -667,7 +667,7 @@ class TestLocalPath(CommonFSTests):
|
||||||
assert p == os.path.expanduser("~")
|
assert p == os.path.expanduser("~")
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
not sys.platform.startswith("win32"), reason="case insensitive only on windows"
|
not sys.platform.startswith("win32"), reason="case-insensitive only on windows"
|
||||||
)
|
)
|
||||||
def test_eq_hash_are_case_insensitive_on_windows(self):
|
def test_eq_hash_are_case_insensitive_on_windows(self):
|
||||||
a = local("/some/path")
|
a = local("/some/path")
|
||||||
|
@ -898,7 +898,7 @@ class TestExecutionOnWindows:
|
||||||
class TestExecution:
|
class TestExecution:
|
||||||
pytestmark = skiponwin32
|
pytestmark = skiponwin32
|
||||||
|
|
||||||
def test_sysfind_no_permisson_ignored(self, monkeypatch, tmpdir):
|
def test_sysfind_no_permission_ignored(self, monkeypatch, tmpdir):
|
||||||
noperm = tmpdir.ensure("noperm", dir=True)
|
noperm = tmpdir.ensure("noperm", dir=True)
|
||||||
monkeypatch.setenv("PATH", str(noperm), prepend=":")
|
monkeypatch.setenv("PATH", str(noperm), prepend=":")
|
||||||
noperm.chmod(0)
|
noperm.chmod(0)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
import importlib
|
import importlib
|
||||||
import io
|
import io
|
||||||
import operator
|
import operator
|
||||||
|
@ -10,6 +11,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import cast
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
|
@ -26,7 +28,7 @@ import pytest
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest._code.code import _TracebackStyle
|
from _pytest._code.code import TracebackStyle
|
||||||
|
|
||||||
if sys.version_info < (3, 11):
|
if sys.version_info < (3, 11):
|
||||||
from exceptiongroup import ExceptionGroup
|
from exceptiongroup import ExceptionGroup
|
||||||
|
@ -237,7 +239,7 @@ class TestTraceback_f_g_h:
|
||||||
n += 1
|
n += 1
|
||||||
f(n)
|
f(n)
|
||||||
|
|
||||||
excinfo = pytest.raises(RuntimeError, f, 8)
|
excinfo = pytest.raises(RecursionError, f, 8)
|
||||||
traceback = excinfo.traceback
|
traceback = excinfo.traceback
|
||||||
recindex = traceback.recursionindex()
|
recindex = traceback.recursionindex()
|
||||||
assert recindex == 3
|
assert recindex == 3
|
||||||
|
@ -373,7 +375,10 @@ def test_excinfo_no_sourcecode():
|
||||||
except ValueError:
|
except ValueError:
|
||||||
excinfo = _pytest._code.ExceptionInfo.from_current()
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
||||||
s = str(excinfo.traceback[-1])
|
s = str(excinfo.traceback[-1])
|
||||||
assert s == " File '<string>':1 in <module>\n ???\n"
|
# TODO: Since Python 3.13b1 under pytest-xdist, the * is `import
|
||||||
|
# sys;exec(eval(sys.stdin.readline()))` (execnet bootstrap code)
|
||||||
|
# instead of `???` like before. Is this OK?
|
||||||
|
fnmatch.fnmatch(s, " File '<string>':1 in <module>\n *\n")
|
||||||
|
|
||||||
|
|
||||||
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
|
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
|
||||||
|
@ -708,6 +713,29 @@ raise ValueError()
|
||||||
assert full_reprlocals.lines
|
assert full_reprlocals.lines
|
||||||
assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
||||||
|
|
||||||
|
def test_repr_args_not_truncated(self, importasmod) -> None:
|
||||||
|
mod = importasmod(
|
||||||
|
"""
|
||||||
|
def func1(m):
|
||||||
|
raise ValueError("hello\\nworld")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
excinfo = pytest.raises(ValueError, mod.func1, "m" * 500)
|
||||||
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||||
|
entry = excinfo.traceback[-1]
|
||||||
|
p = FormattedExcinfo(funcargs=True, truncate_args=True)
|
||||||
|
reprfuncargs = p.repr_args(entry)
|
||||||
|
assert reprfuncargs is not None
|
||||||
|
arg1 = cast(str, reprfuncargs.args[0][1])
|
||||||
|
assert len(arg1) < 500
|
||||||
|
assert "..." in arg1
|
||||||
|
# again without truncate
|
||||||
|
p = FormattedExcinfo(funcargs=True, truncate_args=False)
|
||||||
|
reprfuncargs = p.repr_args(entry)
|
||||||
|
assert reprfuncargs is not None
|
||||||
|
assert reprfuncargs.args[0] == ("m", repr("m" * 500))
|
||||||
|
assert "..." not in cast(str, reprfuncargs.args[0][1])
|
||||||
|
|
||||||
def test_repr_tracebackentry_lines(self, importasmod) -> None:
|
def test_repr_tracebackentry_lines(self, importasmod) -> None:
|
||||||
mod = importasmod(
|
mod = importasmod(
|
||||||
"""
|
"""
|
||||||
|
@ -897,7 +925,7 @@ raise ValueError()
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.entry)
|
excinfo = pytest.raises(ValueError, mod.entry)
|
||||||
|
|
||||||
styles: tuple[_TracebackStyle, ...] = ("long", "short")
|
styles: tuple[TracebackStyle, ...] = ("long", "short")
|
||||||
for style in styles:
|
for style in styles:
|
||||||
p = FormattedExcinfo(style=style)
|
p = FormattedExcinfo(style=style)
|
||||||
reprtb = p.repr_traceback(excinfo)
|
reprtb = p.repr_traceback(excinfo)
|
||||||
|
@ -1024,7 +1052,7 @@ raise ValueError()
|
||||||
)
|
)
|
||||||
excinfo = pytest.raises(ValueError, mod.entry)
|
excinfo = pytest.raises(ValueError, mod.entry)
|
||||||
|
|
||||||
styles: tuple[_TracebackStyle, ...] = ("short", "long", "no")
|
styles: tuple[TracebackStyle, ...] = ("short", "long", "no")
|
||||||
for style in styles:
|
for style in styles:
|
||||||
for showlocals in (True, False):
|
for showlocals in (True, False):
|
||||||
repr = excinfo.getrepr(style=style, showlocals=showlocals)
|
repr = excinfo.getrepr(style=style, showlocals=showlocals)
|
||||||
|
@ -1515,7 +1543,7 @@ def test_cwd_deleted(pytester: Pytester) -> None:
|
||||||
result.stderr.no_fnmatch_line("*INTERNALERROR*")
|
result.stderr.no_fnmatch_line("*INTERNALERROR*")
|
||||||
|
|
||||||
|
|
||||||
def test_regression_nagative_line_index(pytester: Pytester) -> None:
|
def test_regression_negative_line_index(pytester: Pytester) -> None:
|
||||||
"""
|
"""
|
||||||
With Python 3.10 alphas, there was an INTERNALERROR reported in
|
With Python 3.10 alphas, there was an INTERNALERROR reported in
|
||||||
https://github.com/pytest-dev/pytest/pull/8227
|
https://github.com/pytest-dev/pytest/pull/8227
|
||||||
|
|
|
@ -255,7 +255,7 @@ def test_getfuncsource_dynamic() -> None:
|
||||||
assert str(g_source).strip() == "def g():\n pass # pragma: no cover"
|
assert str(g_source).strip() == "def g():\n pass # pragma: no cover"
|
||||||
|
|
||||||
|
|
||||||
def test_getfuncsource_with_multine_string() -> None:
|
def test_getfuncsource_with_multiline_string() -> None:
|
||||||
def f():
|
def f():
|
||||||
c = """while True:
|
c = """while True:
|
||||||
pass
|
pass
|
||||||
|
@ -370,7 +370,11 @@ def test_getfslineno() -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
B.__name__ = B.__qualname__ = "B2"
|
B.__name__ = B.__qualname__ = "B2"
|
||||||
assert getfslineno(B)[1] == -1
|
# Since Python 3.13 this started working.
|
||||||
|
if sys.version_info >= (3, 13):
|
||||||
|
assert getfslineno(B)[1] != -1
|
||||||
|
else:
|
||||||
|
assert getfslineno(B)[1] == -1
|
||||||
|
|
||||||
|
|
||||||
def test_code_of_object_instance_with_call() -> None:
|
def test_code_of_object_instance_with_call() -> None:
|
||||||
|
|
|
@ -117,7 +117,7 @@ def test_change_disabled_level_undo(pytester: Pytester) -> None:
|
||||||
result.stdout.no_fnmatch_line("*log from test2*")
|
result.stdout.no_fnmatch_line("*log from test2*")
|
||||||
|
|
||||||
|
|
||||||
def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
def test_change_level_undoes_handler_level(pytester: Pytester) -> None:
|
||||||
"""Ensure that 'set_level' is undone after the end of the test (handler).
|
"""Ensure that 'set_level' is undone after the end of the test (handler).
|
||||||
|
|
||||||
Issue #7569. Tests the handler level specifically.
|
Issue #7569. Tests the handler level specifically.
|
||||||
|
@ -302,7 +302,15 @@ def logging_during_setup_and_teardown(
|
||||||
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
|
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
|
||||||
|
|
||||||
|
|
||||||
def test_caplog_captures_for_all_stages(
|
def private_assert_caplog_records_is_setup_call(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
# This reaches into private API, don't use this type of thing in real tests!
|
||||||
|
caplog_records = caplog._item.stash[caplog_records_key]
|
||||||
|
assert set(caplog_records) == {"setup", "call"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_captures_for_all_stages(
|
||||||
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
|
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
|
||||||
) -> None:
|
) -> None:
|
||||||
assert not caplog.records
|
assert not caplog.records
|
||||||
|
@ -312,9 +320,7 @@ def test_caplog_captures_for_all_stages(
|
||||||
|
|
||||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||||
|
|
||||||
# This reaches into private API, don't use this type of thing in real tests!
|
private_assert_caplog_records_is_setup_call(caplog)
|
||||||
caplog_records = caplog._item.stash[caplog_records_key]
|
|
||||||
assert set(caplog_records) == {"setup", "call"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_clear_for_call_stage(
|
def test_clear_for_call_stage(
|
||||||
|
@ -323,21 +329,18 @@ def test_clear_for_call_stage(
|
||||||
logger.info("a_call_log")
|
logger.info("a_call_log")
|
||||||
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
|
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
|
||||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||||
caplog_records = caplog._item.stash[caplog_records_key]
|
private_assert_caplog_records_is_setup_call(caplog)
|
||||||
assert set(caplog_records) == {"setup", "call"}
|
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
assert caplog.get_records("call") == []
|
assert caplog.get_records("call") == []
|
||||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||||
caplog_records = caplog._item.stash[caplog_records_key]
|
private_assert_caplog_records_is_setup_call(caplog)
|
||||||
assert set(caplog_records) == {"setup", "call"}
|
|
||||||
|
|
||||||
logging.info("a_call_log_after_clear")
|
logging.info("a_call_log_after_clear")
|
||||||
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
|
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
|
||||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||||
caplog_records = caplog._item.stash[caplog_records_key]
|
private_assert_caplog_records_is_setup_call(caplog)
|
||||||
assert set(caplog_records) == {"setup", "call"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_ini_controls_global_log_level(pytester: Pytester) -> None:
|
def test_ini_controls_global_log_level(pytester: Pytester) -> None:
|
||||||
|
@ -363,11 +366,11 @@ def test_ini_controls_global_log_level(pytester: Pytester) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
# make sure that that we get a '0' exit code for the testsuite
|
# make sure that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
def test_caplog_can_override_global_log_level(pytester: Pytester) -> None:
|
def test_can_override_global_log_level(pytester: Pytester) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -406,7 +409,7 @@ def test_caplog_can_override_global_log_level(pytester: Pytester) -> None:
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
def test_caplog_captures_despite_exception(pytester: Pytester) -> None:
|
def test_captures_despite_exception(pytester: Pytester) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
first_time = True
|
||||||
|
|
||||||
|
def test_fail_the_first_time(self) -> None:
|
||||||
|
"""Regression test for issue #12424."""
|
||||||
|
if self.first_time:
|
||||||
|
type(self).first_time = False
|
||||||
|
self.fail()
|
|
@ -1,7 +1,7 @@
|
||||||
anyio[curio,trio]==4.3.0
|
anyio[curio,trio]==4.4.0
|
||||||
django==5.0.4
|
django==5.0.6
|
||||||
pytest-asyncio==0.23.6
|
pytest-asyncio==0.23.7
|
||||||
pytest-bdd==7.1.2
|
pytest-bdd==7.2.0
|
||||||
pytest-cov==5.0.0
|
pytest-cov==5.0.0
|
||||||
pytest-django==4.8.0
|
pytest-django==4.8.0
|
||||||
pytest-flakes==4.0.5
|
pytest-flakes==4.0.5
|
||||||
|
@ -9,7 +9,7 @@ pytest-html==4.1.1
|
||||||
pytest-mock==3.14.0
|
pytest-mock==3.14.0
|
||||||
pytest-rerunfailures==14.0
|
pytest-rerunfailures==14.0
|
||||||
pytest-sugar==1.0.0
|
pytest-sugar==1.0.0
|
||||||
pytest-trio==0.7.0
|
pytest-trio==0.8.0
|
||||||
pytest-twisted==1.14.1
|
pytest-twisted==1.14.1
|
||||||
twisted==24.3.0
|
twisted==24.3.0
|
||||||
pytest-xvfb==3.0.0
|
pytest-xvfb==3.0.0
|
||||||
|
|
|
@ -262,6 +262,32 @@ class TestClass:
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
|
|
||||||
|
def test_abstract_class_is_not_collected(self, pytester: Pytester) -> None:
|
||||||
|
"""Regression test for #12275 (non-unittest version)."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import abc
|
||||||
|
|
||||||
|
class TestBase(abc.ABC):
|
||||||
|
@abc.abstractmethod
|
||||||
|
def abstract1(self): pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def abstract2(self): pass
|
||||||
|
|
||||||
|
def test_it(self): pass
|
||||||
|
|
||||||
|
class TestPartial(TestBase):
|
||||||
|
def abstract1(self): pass
|
||||||
|
|
||||||
|
class TestConcrete(TestPartial):
|
||||||
|
def abstract2(self): pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
||||||
|
|
||||||
class TestFunction:
|
class TestFunction:
|
||||||
def test_getmodulecollector(self, pytester: Pytester) -> None:
|
def test_getmodulecollector(self, pytester: Pytester) -> None:
|
||||||
|
|
|
@ -933,7 +933,7 @@ class TestRequestBasic:
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure exceptions raised during teardown by finalizers are suppressed
|
Ensure exceptions raised during teardown by finalizers are suppressed
|
||||||
until all finalizers are called, then re-reaised together in an
|
until all finalizers are called, then re-raised together in an
|
||||||
exception group (#2440)
|
exception group (#2440)
|
||||||
"""
|
"""
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
|
@ -2219,6 +2219,25 @@ class TestAutouseManagement:
|
||||||
reprec = pytester.inline_run("-s")
|
reprec = pytester.inline_run("-s")
|
||||||
reprec.assertoutcome(passed=2)
|
reprec.assertoutcome(passed=2)
|
||||||
|
|
||||||
|
def test_reordering_catastrophic_performance(self, pytester: Pytester) -> None:
|
||||||
|
"""Check that a certain high-scope parametrization pattern doesn't cause
|
||||||
|
a catasrophic slowdown.
|
||||||
|
|
||||||
|
Regression test for #12355.
|
||||||
|
"""
|
||||||
|
pytester.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
params = tuple("abcdefghijklmnopqrstuvwxyz")
|
||||||
|
@pytest.mark.parametrize(params, [range(len(params))] * 3, scope="module")
|
||||||
|
def test_parametrize(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z):
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
|
||||||
|
result = pytester.runpytest()
|
||||||
|
|
||||||
|
result.assert_outcomes(passed=3)
|
||||||
|
|
||||||
|
|
||||||
class TestFixtureMarker:
|
class TestFixtureMarker:
|
||||||
def test_parametrize(self, pytester: Pytester) -> None:
|
def test_parametrize(self, pytester: Pytester) -> None:
|
||||||
|
@ -4274,6 +4293,39 @@ class TestScopeOrdering:
|
||||||
request = TopRequest(items[0], _ispytest=True)
|
request = TopRequest(items[0], _ispytest=True)
|
||||||
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
|
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
|
||||||
|
|
||||||
|
def test_parametrized_package_scope_reordering(self, pytester: Pytester) -> None:
|
||||||
|
"""A paramaterized package-scoped fixture correctly reorders items to
|
||||||
|
minimize setups & teardowns.
|
||||||
|
|
||||||
|
Regression test for #12328.
|
||||||
|
"""
|
||||||
|
pytester.makepyfile(
|
||||||
|
__init__="",
|
||||||
|
conftest="""
|
||||||
|
import pytest
|
||||||
|
@pytest.fixture(scope="package", params=["a", "b"])
|
||||||
|
def fix(request):
|
||||||
|
return request.param
|
||||||
|
""",
|
||||||
|
test_1="def test1(fix): pass",
|
||||||
|
test_2="def test2(fix): pass",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = pytester.runpytest("--setup-plan")
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
" SETUP P fix['a']",
|
||||||
|
" test_1.py::test1[a] (fixtures used: fix, request)",
|
||||||
|
" test_2.py::test2[a] (fixtures used: fix, request)",
|
||||||
|
" TEARDOWN P fix['a']",
|
||||||
|
" SETUP P fix['b']",
|
||||||
|
" test_1.py::test1[b] (fixtures used: fix, request)",
|
||||||
|
" test_2.py::test2[b] (fixtures used: fix, request)",
|
||||||
|
" TEARDOWN P fix['b']",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_multiple_packages(self, pytester: Pytester) -> None:
|
def test_multiple_packages(self, pytester: Pytester) -> None:
|
||||||
"""Complex test involving multiple package fixtures. Make sure teardowns
|
"""Complex test involving multiple package fixtures. Make sure teardowns
|
||||||
are executed in order.
|
are executed in order.
|
||||||
|
|
|
@ -163,7 +163,7 @@ class TestMockDecoration:
|
||||||
@mock.patch("os.path.abspath")
|
@mock.patch("os.path.abspath")
|
||||||
@mock.patch("os.path.normpath")
|
@mock.patch("os.path.normpath")
|
||||||
@mock.patch("os.path.basename", new=mock_basename)
|
@mock.patch("os.path.basename", new=mock_basename)
|
||||||
def test_someting(normpath, abspath, tmp_path):
|
def test_something(normpath, abspath, tmp_path):
|
||||||
abspath.return_value = "this"
|
abspath.return_value = "this"
|
||||||
os.path.normpath(os.path.abspath("hello"))
|
os.path.normpath(os.path.abspath("hello"))
|
||||||
normpath.assert_any_call("this")
|
normpath.assert_any_call("this")
|
||||||
|
@ -176,7 +176,7 @@ class TestMockDecoration:
|
||||||
funcnames = [
|
funcnames = [
|
||||||
call.report.location[2] for call in calls if call.report.when == "call"
|
call.report.location[2] for call in calls if call.report.when == "call"
|
||||||
]
|
]
|
||||||
assert funcnames == ["T.test_hello", "test_someting"]
|
assert funcnames == ["T.test_hello", "test_something"]
|
||||||
|
|
||||||
def test_mock_sorting(self, pytester: Pytester) -> None:
|
def test_mock_sorting(self, pytester: Pytester) -> None:
|
||||||
pytest.importorskip("mock", "1.0.1")
|
pytest.importorskip("mock", "1.0.1")
|
||||||
|
|
|
@ -2045,3 +2045,36 @@ def test_fine_grained_assertion_verbosity(pytester: Pytester):
|
||||||
f"E AssertionError: assert 'hello world' in '{long_text}'",
|
f"E AssertionError: assert 'hello world' in '{long_text}'",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_output_vvv(pytester: Pytester) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
r"""
|
||||||
|
def crash_helper(m):
|
||||||
|
assert 1 == 2
|
||||||
|
def test_vvv():
|
||||||
|
crash_helper(500 * "a")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("")
|
||||||
|
# without -vvv, the passed args are truncated
|
||||||
|
expected_non_vvv_arg_line = "m = 'aaaaaaaaaaaaaaa*..aaaaaaaaaaaa*"
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
expected_non_vvv_arg_line,
|
||||||
|
"test_full_output_vvv.py:2: AssertionError",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
# double check that the untruncated part is not in the output
|
||||||
|
expected_vvv_arg_line = "m = '{}'".format(500 * "a")
|
||||||
|
result.stdout.no_fnmatch_line(expected_vvv_arg_line)
|
||||||
|
|
||||||
|
# but with "-vvv" the args are not truncated
|
||||||
|
result = pytester.runpytest("-vvv")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
expected_vvv_arg_line,
|
||||||
|
"test_full_output_vvv.py:2: AssertionError",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
result.stdout.no_fnmatch_line(expected_non_vvv_arg_line)
|
||||||
|
|
|
@ -130,6 +130,7 @@ class TestAssertionRewrite:
|
||||||
if isinstance(node, ast.Import):
|
if isinstance(node, ast.Import):
|
||||||
continue
|
continue
|
||||||
for n in [node, *ast.iter_child_nodes(node)]:
|
for n in [node, *ast.iter_child_nodes(node)]:
|
||||||
|
assert isinstance(n, (ast.stmt, ast.expr))
|
||||||
assert n.lineno == 3
|
assert n.lineno == 3
|
||||||
assert n.col_offset == 0
|
assert n.col_offset == 0
|
||||||
assert n.end_lineno == 6
|
assert n.end_lineno == 6
|
||||||
|
|
|
@ -31,6 +31,21 @@ class TestNewAPI:
|
||||||
p = config.cache.mkdir("name")
|
p = config.cache.mkdir("name")
|
||||||
assert p.is_dir()
|
assert p.is_dir()
|
||||||
|
|
||||||
|
def test_cache_dir_permissions(self, pytester: Pytester) -> None:
|
||||||
|
"""The .pytest_cache directory should have world-readable permissions
|
||||||
|
(depending on umask).
|
||||||
|
|
||||||
|
Regression test for #12308.
|
||||||
|
"""
|
||||||
|
pytester.makeini("[pytest]")
|
||||||
|
config = pytester.parseconfigure()
|
||||||
|
assert config.cache is not None
|
||||||
|
p = config.cache.mkdir("name")
|
||||||
|
assert p.is_dir()
|
||||||
|
# Instead of messing with umask, make sure .pytest_cache has the same
|
||||||
|
# permissions as the default that `mkdir` gives `p`.
|
||||||
|
assert (p.parent.stat().st_mode & 0o777) == (p.stat().st_mode & 0o777)
|
||||||
|
|
||||||
def test_config_cache_dataerror(self, pytester: Pytester) -> None:
|
def test_config_cache_dataerror(self, pytester: Pytester) -> None:
|
||||||
pytester.makeini("[pytest]")
|
pytester.makeini("[pytest]")
|
||||||
config = pytester.parseconfigure()
|
config = pytester.parseconfigure()
|
||||||
|
@ -43,7 +58,7 @@ class TestNewAPI:
|
||||||
assert val == -2
|
assert val == -2
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:could not create cache path")
|
@pytest.mark.filterwarnings("ignore:could not create cache path")
|
||||||
def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None:
|
def test_cache_writefail_cachefile_silent(self, pytester: Pytester) -> None:
|
||||||
pytester.makeini("[pytest]")
|
pytester.makeini("[pytest]")
|
||||||
pytester.path.joinpath(".pytest_cache").write_text(
|
pytester.path.joinpath(".pytest_cache").write_text(
|
||||||
"gone wrong", encoding="utf-8"
|
"gone wrong", encoding="utf-8"
|
||||||
|
@ -179,7 +194,7 @@ class TestNewAPI:
|
||||||
assert pytester.path.joinpath("custom_cache_dir").is_dir()
|
assert pytester.path.joinpath("custom_cache_dir").is_dir()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir")))
|
@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "mydir/tox-env")))
|
||||||
def test_cache_reportheader(
|
def test_cache_reportheader(
|
||||||
env: Sequence[str], pytester: Pytester, monkeypatch: MonkeyPatch
|
env: Sequence[str], pytester: Pytester, monkeypatch: MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1148,7 +1163,7 @@ class TestNewFirst:
|
||||||
)
|
)
|
||||||
|
|
||||||
p1.write_text(
|
p1.write_text(
|
||||||
"def test_1(): assert 1\n" "def test_2(): assert 1\n", encoding="utf-8"
|
"def test_1(): assert 1\ndef test_2(): assert 1\n", encoding="utf-8"
|
||||||
)
|
)
|
||||||
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
|
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
|
||||||
|
|
||||||
|
|
|
@ -285,6 +285,23 @@ class TestCollectFS:
|
||||||
items, reprec = pytester.inline_genitems()
|
items, reprec = pytester.inline_genitems()
|
||||||
assert [x.name for x in items] == [f"test_{dirname}"]
|
assert [x.name for x in items] == [f"test_{dirname}"]
|
||||||
|
|
||||||
|
def test_missing_permissions_on_unselected_directory_doesnt_crash(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
"""Regression test for #12120."""
|
||||||
|
test = pytester.makepyfile(test="def test(): pass")
|
||||||
|
bad = pytester.mkdir("bad")
|
||||||
|
try:
|
||||||
|
bad.chmod(0)
|
||||||
|
|
||||||
|
result = pytester.runpytest(test)
|
||||||
|
finally:
|
||||||
|
bad.chmod(750)
|
||||||
|
bad.rmdir()
|
||||||
|
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
||||||
|
|
||||||
class TestCollectPluginHookRelay:
|
class TestCollectPluginHookRelay:
|
||||||
def test_pytest_collect_file(self, pytester: Pytester) -> None:
|
def test_pytest_collect_file(self, pytester: Pytester) -> None:
|
||||||
|
|
|
@ -94,7 +94,7 @@ def test_get_real_func_partial() -> None:
|
||||||
assert get_real_func(partial(foo)) is foo
|
assert get_real_func(partial(foo)) is foo
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info >= (3, 11), reason="couroutine removed")
|
@pytest.mark.skipif(sys.version_info >= (3, 11), reason="coroutine removed")
|
||||||
def test_is_generator_asyncio(pytester: Pytester) -> None:
|
def test_is_generator_asyncio(pytester: Pytester) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -216,7 +216,7 @@ class TestParseIni:
|
||||||
|
|
||||||
def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None:
|
def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None:
|
||||||
# If --confcutdir is not specified, and there is no configfile, default
|
# If --confcutdir is not specified, and there is no configfile, default
|
||||||
# to the roothpath.
|
# to the rootpath.
|
||||||
sub = pytester.mkdir("sub")
|
sub = pytester.mkdir("sub")
|
||||||
os.chdir(sub)
|
os.chdir(sub)
|
||||||
config = pytester.parseconfigure()
|
config = pytester.parseconfigure()
|
||||||
|
@ -1738,7 +1738,7 @@ class TestOverrideIniArgs:
|
||||||
)
|
)
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
r"""
|
r"""
|
||||||
def test_overriden(pytestconfig):
|
def test_overridden(pytestconfig):
|
||||||
config_paths = pytestconfig.getini("paths")
|
config_paths = pytestconfig.getini("paths")
|
||||||
print(config_paths)
|
print(config_paths)
|
||||||
for cpf in config_paths:
|
for cpf in config_paths:
|
||||||
|
|
|
@ -70,7 +70,7 @@ class TestConftestValueAccessGlobal:
|
||||||
)
|
)
|
||||||
assert conftest._rget_with_confmod("a", p)[1] == 1
|
assert conftest._rget_with_confmod("a", p)[1] == 1
|
||||||
|
|
||||||
def test_immediate_initialiation_and_incremental_are_the_same(
|
def test_immediate_initialization_and_incremental_are_the_same(
|
||||||
self, basedir: Path
|
self, basedir: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
conftest = PytestPluginManager()
|
conftest = PytestPluginManager()
|
||||||
|
@ -396,7 +396,7 @@ def test_conftest_symlink_files(pytester: Pytester) -> None:
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
os.path.normcase("x") != os.path.normcase("X"),
|
os.path.normcase("x") != os.path.normcase("X"),
|
||||||
reason="only relevant for case insensitive file systems",
|
reason="only relevant for case-insensitive file systems",
|
||||||
)
|
)
|
||||||
def test_conftest_badcase(pytester: Pytester) -> None:
|
def test_conftest_badcase(pytester: Pytester) -> None:
|
||||||
"""Check conftest.py loading when directory casing is wrong (#5792)."""
|
"""Check conftest.py loading when directory casing is wrong (#5792)."""
|
||||||
|
|
|
@ -1122,7 +1122,7 @@ class TestTraceOption:
|
||||||
|
|
||||||
|
|
||||||
def test_trace_after_runpytest(pytester: Pytester) -> None:
|
def test_trace_after_runpytest(pytester: Pytester) -> None:
|
||||||
"""Test that debugging's pytest_configure is re-entrant."""
|
"""Test that debugging's pytest_configure is reentrant."""
|
||||||
p1 = pytester.makepyfile(
|
p1 = pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
from _pytest.debugging import pytestPDB
|
from _pytest.debugging import pytestPDB
|
||||||
|
@ -1153,7 +1153,7 @@ def test_trace_after_runpytest(pytester: Pytester) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_quit_with_swallowed_SystemExit(pytester: Pytester) -> None:
|
def test_quit_with_swallowed_SystemExit(pytester: Pytester) -> None:
|
||||||
"""Test that debugging's pytest_configure is re-entrant."""
|
"""Test that debugging's pytest_configure is reentrant."""
|
||||||
p1 = pytester.makepyfile(
|
p1 = pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
def call_pdb_set_trace():
|
def call_pdb_set_trace():
|
||||||
|
|
|
@ -224,11 +224,7 @@ class TestDoctests:
|
||||||
"Traceback (most recent call last):",
|
"Traceback (most recent call last):",
|
||||||
' File "*/doctest.py", line *, in __run',
|
' File "*/doctest.py", line *, in __run',
|
||||||
" *",
|
" *",
|
||||||
*(
|
*((" *^^^^*", " *", " *") if sys.version_info >= (3, 13) else ()),
|
||||||
(" *^^^^*",)
|
|
||||||
if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
|
|
||||||
else ()
|
|
||||||
),
|
|
||||||
' File "<doctest test_doctest_unexpected_exception.txt[1]>", line 1, in <module>',
|
' File "<doctest test_doctest_unexpected_exception.txt[1]>", line 1, in <module>',
|
||||||
"ZeroDivisionError: division by zero",
|
"ZeroDivisionError: division by zero",
|
||||||
"*/test_doctest_unexpected_exception.txt:2: UnexpectedException",
|
"*/test_doctest_unexpected_exception.txt:2: UnexpectedException",
|
||||||
|
@ -385,7 +381,7 @@ class TestDoctests:
|
||||||
"*= FAILURES =*",
|
"*= FAILURES =*",
|
||||||
"*_ [[]doctest[]] test_doctest_linedata_on_property.Sample.some_property _*",
|
"*_ [[]doctest[]] test_doctest_linedata_on_property.Sample.some_property _*",
|
||||||
"004 ",
|
"004 ",
|
||||||
"005 >>> Sample().some_property",
|
"005 *>>> Sample().some_property",
|
||||||
"Expected:",
|
"Expected:",
|
||||||
" 'another thing'",
|
" 'another thing'",
|
||||||
"Got:",
|
"Got:",
|
||||||
|
@ -396,7 +392,7 @@ class TestDoctests:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_doctest_no_linedata_on_overriden_property(self, pytester: Pytester):
|
def test_doctest_no_linedata_on_overridden_property(self, pytester: Pytester):
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
class Sample(object):
|
class Sample(object):
|
||||||
|
@ -414,7 +410,7 @@ class TestDoctests:
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*= FAILURES =*",
|
"*= FAILURES =*",
|
||||||
"*_ [[]doctest[]] test_doctest_no_linedata_on_overriden_property.Sample.some_property _*",
|
"*_ [[]doctest[]] test_doctest_no_linedata_on_overridden_property.Sample.some_property _*",
|
||||||
"EXAMPLE LOCATION UNKNOWN, not showing all tests of that example",
|
"EXAMPLE LOCATION UNKNOWN, not showing all tests of that example",
|
||||||
"[?][?][?] >>> Sample().some_property",
|
"[?][?][?] >>> Sample().some_property",
|
||||||
"Expected:",
|
"Expected:",
|
||||||
|
@ -422,7 +418,7 @@ class TestDoctests:
|
||||||
"Got:",
|
"Got:",
|
||||||
" 'something'",
|
" 'something'",
|
||||||
"",
|
"",
|
||||||
"*/test_doctest_no_linedata_on_overriden_property.py:None: DocTestFailure",
|
"*/test_doctest_no_linedata_on_overridden_property.py:None: DocTestFailure",
|
||||||
"*= 1 failed in *",
|
"*= 1 failed in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1001,7 +1001,7 @@ class TestNonPython:
|
||||||
|
|
||||||
@pytest.mark.parametrize("junit_logging", ["no", "system-out"])
|
@pytest.mark.parametrize("junit_logging", ["no", "system-out"])
|
||||||
def test_nullbyte(pytester: Pytester, junit_logging: str) -> None:
|
def test_nullbyte(pytester: Pytester, junit_logging: str) -> None:
|
||||||
# A null byte can not occur in XML (see section 2.2 of the spec)
|
# A null byte cannot occur in XML (see section 2.2 of the spec)
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
|
|
@ -155,7 +155,7 @@ def test_override_ini_paths(pytester: pytest.Pytester) -> None:
|
||||||
)
|
)
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
r"""
|
r"""
|
||||||
def test_overriden(pytestconfig):
|
def test_overridden(pytestconfig):
|
||||||
config_paths = pytestconfig.getini("paths")
|
config_paths = pytestconfig.getini("paths")
|
||||||
print(config_paths)
|
print(config_paths)
|
||||||
for cpf in config_paths:
|
for cpf in config_paths:
|
||||||
|
|
|
@ -3,7 +3,6 @@ import argparse
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -45,32 +44,18 @@ def test_wrap_session_notify_exception(ret_exc, pytester: Pytester) -> None:
|
||||||
assert result.ret == ExitCode.INTERNAL_ERROR
|
assert result.ret == ExitCode.INTERNAL_ERROR
|
||||||
assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):"
|
assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):"
|
||||||
|
|
||||||
end_lines = (
|
end_lines = result.stdout.lines[-3:]
|
||||||
result.stdout.lines[-4:]
|
|
||||||
if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
|
|
||||||
else result.stdout.lines[-3:]
|
|
||||||
)
|
|
||||||
|
|
||||||
if exc == SystemExit:
|
if exc == SystemExit:
|
||||||
assert end_lines == [
|
assert end_lines == [
|
||||||
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
|
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
|
||||||
'INTERNALERROR> raise SystemExit("boom")',
|
'INTERNALERROR> raise SystemExit("boom")',
|
||||||
*(
|
|
||||||
("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",)
|
|
||||||
if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
|
|
||||||
else ()
|
|
||||||
),
|
|
||||||
"INTERNALERROR> SystemExit: boom",
|
"INTERNALERROR> SystemExit: boom",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
assert end_lines == [
|
assert end_lines == [
|
||||||
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
|
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
|
||||||
'INTERNALERROR> raise ValueError("boom")',
|
'INTERNALERROR> raise ValueError("boom")',
|
||||||
*(
|
|
||||||
("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",)
|
|
||||||
if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
|
|
||||||
else ()
|
|
||||||
),
|
|
||||||
"INTERNALERROR> ValueError: boom",
|
"INTERNALERROR> ValueError: boom",
|
||||||
]
|
]
|
||||||
if returncode is False:
|
if returncode is False:
|
||||||
|
|
|
@ -61,7 +61,7 @@ def test_basic(expr: str, expected: bool) -> None:
|
||||||
("not not not not not true", False),
|
("not not not not not true", False),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_syntax_oddeties(expr: str, expected: bool) -> None:
|
def test_syntax_oddities(expr: str, expected: bool) -> None:
|
||||||
matcher = {"true": True, "false": False}.__getitem__
|
matcher = {"true": True, "false": False}.__getitem__
|
||||||
assert evaluate(expr, matcher) is expected
|
assert evaluate(expr, matcher) is expected
|
||||||
|
|
||||||
|
|
|
@ -420,7 +420,7 @@ class TestPytestPluginManager:
|
||||||
pytestpm.consider_conftest(mod, registration_name="unused")
|
pytestpm.consider_conftest(mod, registration_name="unused")
|
||||||
|
|
||||||
|
|
||||||
class TestPytestPluginManagerBootstrapming:
|
class TestPytestPluginManagerBootstrapping:
|
||||||
def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
|
def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
|
||||||
pytest.raises(
|
pytest.raises(
|
||||||
ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
|
ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
|
||||||
|
@ -446,7 +446,7 @@ class TestPytestPluginManagerBootstrapming:
|
||||||
assert len(l2) == len(l1)
|
assert len(l2) == len(l1)
|
||||||
assert 42 not in l2
|
assert 42 not in l2
|
||||||
|
|
||||||
def test_plugin_prevent_register_unregistered_alredy_registered(
|
def test_plugin_prevent_register_unregistered_already_registered(
|
||||||
self, pytestpm: PytestPluginManager
|
self, pytestpm: PytestPluginManager
|
||||||
) -> None:
|
) -> None:
|
||||||
pytestpm.register(42, name="abc")
|
pytestpm.register(42, name="abc")
|
||||||
|
|
|
@ -100,14 +100,13 @@ class TestReportSerialization:
|
||||||
|
|
||||||
rep_entries = rep.longrepr.reprtraceback.reprentries
|
rep_entries = rep.longrepr.reprtraceback.reprentries
|
||||||
a_entries = a.longrepr.reprtraceback.reprentries
|
a_entries = a.longrepr.reprtraceback.reprentries
|
||||||
for i in range(len(a_entries)):
|
assert len(rep_entries) == len(a_entries) # python < 3.10 zip(strict=True)
|
||||||
rep_entry = rep_entries[i]
|
for a_entry, rep_entry in zip(a_entries, rep_entries):
|
||||||
assert isinstance(rep_entry, ReprEntry)
|
assert isinstance(rep_entry, ReprEntry)
|
||||||
assert rep_entry.reprfileloc is not None
|
assert rep_entry.reprfileloc is not None
|
||||||
assert rep_entry.reprfuncargs is not None
|
assert rep_entry.reprfuncargs is not None
|
||||||
assert rep_entry.reprlocals is not None
|
assert rep_entry.reprlocals is not None
|
||||||
|
|
||||||
a_entry = a_entries[i]
|
|
||||||
assert isinstance(a_entry, ReprEntry)
|
assert isinstance(a_entry, ReprEntry)
|
||||||
assert a_entry.reprfileloc is not None
|
assert a_entry.reprfileloc is not None
|
||||||
assert a_entry.reprfuncargs is not None
|
assert a_entry.reprfuncargs is not None
|
||||||
|
@ -146,9 +145,10 @@ class TestReportSerialization:
|
||||||
|
|
||||||
rep_entries = rep.longrepr.reprtraceback.reprentries
|
rep_entries = rep.longrepr.reprtraceback.reprentries
|
||||||
a_entries = a.longrepr.reprtraceback.reprentries
|
a_entries = a.longrepr.reprtraceback.reprentries
|
||||||
for i in range(len(a_entries)):
|
assert len(rep_entries) == len(a_entries) # python < 3.10 zip(strict=True)
|
||||||
assert isinstance(rep_entries[i], ReprEntryNative)
|
for rep_entry, a_entry in zip(rep_entries, a_entries):
|
||||||
assert rep_entries[i].lines == a_entries[i].lines
|
assert isinstance(rep_entry, ReprEntryNative)
|
||||||
|
assert rep_entry.lines == a_entry.lines
|
||||||
|
|
||||||
def test_itemreport_outcomes(self, pytester: Pytester) -> None:
|
def test_itemreport_outcomes(self, pytester: Pytester) -> None:
|
||||||
# This test came originally from test_remote.py in xdist (ca03269).
|
# This test came originally from test_remote.py in xdist (ca03269).
|
||||||
|
|
|
@ -446,7 +446,7 @@ class BaseFunctionalTests:
|
||||||
# assert rep.outcome.when == "setup"
|
# assert rep.outcome.when == "setup"
|
||||||
# assert rep.outcome.where.lineno == 3
|
# assert rep.outcome.where.lineno == 3
|
||||||
# assert rep.outcome.where.path.basename == "test_func.py"
|
# assert rep.outcome.where.path.basename == "test_func.py"
|
||||||
# assert instanace(rep.failed.failurerepr, PythonFailureRepr)
|
# assert isinstance(rep.failed.failurerepr, PythonFailureRepr)
|
||||||
|
|
||||||
def test_systemexit_does_not_bail_out(self, pytester: Pytester) -> None:
|
def test_systemexit_does_not_bail_out(self, pytester: Pytester) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -926,7 +926,7 @@ class TestTerminalFunctional:
|
||||||
def test_header_absolute_testpath(
|
def test_header_absolute_testpath(
|
||||||
self, pytester: Pytester, monkeypatch: MonkeyPatch
|
self, pytester: Pytester, monkeypatch: MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Regresstion test for #7814."""
|
"""Regression test for #7814."""
|
||||||
tests = pytester.path.joinpath("tests")
|
tests = pytester.path.joinpath("tests")
|
||||||
tests.mkdir()
|
tests.mkdir()
|
||||||
pytester.makepyprojecttoml(
|
pytester.makepyprojecttoml(
|
||||||
|
@ -2609,8 +2609,8 @@ class TestCodeHighlight:
|
||||||
monkeypatch.setenv("PYTEST_THEME", "invalid")
|
monkeypatch.setenv("PYTEST_THEME", "invalid")
|
||||||
result = pytester.runpytest_subprocess("--color=yes")
|
result = pytester.runpytest_subprocess("--color=yes")
|
||||||
result.stderr.fnmatch_lines(
|
result.stderr.fnmatch_lines(
|
||||||
"ERROR: PYTEST_THEME environment variable had an invalid value: 'invalid'. "
|
"ERROR: PYTEST_THEME environment variable has an invalid value: 'invalid'. "
|
||||||
"Only valid pygment styles are allowed."
|
"Hint: See available pygments styles with `pygmentize -L styles`."
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_code_highlight_invalid_theme_mode(
|
def test_code_highlight_invalid_theme_mode(
|
||||||
|
@ -2625,8 +2625,8 @@ class TestCodeHighlight:
|
||||||
monkeypatch.setenv("PYTEST_THEME_MODE", "invalid")
|
monkeypatch.setenv("PYTEST_THEME_MODE", "invalid")
|
||||||
result = pytester.runpytest_subprocess("--color=yes")
|
result = pytester.runpytest_subprocess("--color=yes")
|
||||||
result.stderr.fnmatch_lines(
|
result.stderr.fnmatch_lines(
|
||||||
"ERROR: PYTEST_THEME_MODE environment variable had an invalid value: 'invalid'. "
|
"ERROR: PYTEST_THEME_MODE environment variable has an invalid value: 'invalid'. "
|
||||||
"The only allowed values are 'dark' and 'light'."
|
"The allowed values are 'dark' (default) and 'light'."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue