Compare commits
89 Commits
Author | SHA1 | Date |
---|---|---|
|
8e3b8dc7e7 | |
|
2e01db1fc1 | |
|
c45812ed93 | |
|
77e93c248a | |
|
60c92473af | |
|
f484eaf1fd | |
|
d4d4ca3213 | |
|
1dc3a28805 | |
|
3980206012 | |
|
09888fa263 | |
|
2d5b20ac6c | |
|
90e70c3071 | |
|
64be38da80 | |
|
d68e0e4602 | |
|
f8277264de | |
|
8985f0475c | |
|
e6884c80c3 | |
|
7f3469f7af | |
|
f48f31291b | |
|
8effd29884 | |
|
a177ba7905 | |
|
68ea72b89c | |
|
d3dacc74c9 | |
|
a22b8e196d | |
|
29987a4e12 | |
|
7a0cb6b37d | |
|
b19cbf3e36 | |
|
0f6118596d | |
|
481aa764eb | |
|
780d0c10cd | |
|
277951fab9 | |
|
76065e5028 | |
|
cffcbdfbba | |
|
4f7ada5b6a | |
|
e60afd5af2 | |
|
2b16e7b138 | |
|
fc6ba92c30 | |
|
a36b74ead8 | |
|
69936d32a5 | |
|
d51fac48d6 | |
|
8e495e32c6 | |
|
2a75bab290 | |
|
0fce6d4ddb | |
|
f839730d22 | |
|
4716ba546f | |
|
c87707122c | |
|
31e07cf151 | |
|
3931fe2fbf | |
|
4f4bcc2e24 | |
|
65a8e4a744 | |
|
fdc53750f9 | |
|
a64fb18305 | |
|
53d2d4717d | |
|
56c0b1db3a | |
|
eb8721d387 | |
|
36f0ea10b4 | |
|
f3a494cca3 | |
|
329d371214 | |
|
214d098fcc | |
|
153a436bc4 | |
|
b41d5a52bb | |
|
9bb73d734f | |
|
4569a01e3d | |
|
1d103e5cdc | |
|
240a252d34 | |
|
a5ee3c4126 | |
|
f7358aec28 | |
|
558e4fa71a | |
|
f0e7a07667 | |
|
22e885f109 | |
|
bddbeba74b | |
|
66ff8dffdf | |
|
3ffcfd122c | |
|
0b28313b46 | |
|
f3dd93ad8d | |
|
bb5a1257b0 | |
|
f179bf252f | |
|
2b671b5f92 | |
|
65ab7cb96c | |
|
4d5fb7d71c | |
|
cbe5996cc6 | |
|
c9e9315725 | |
|
328001eab1 | |
|
8fdb72947e | |
|
5c1c73b961 | |
|
a152c2cee4 | |
|
69c3bcea36 | |
|
6bd3f31344 | |
|
9b6219b5e8 |
|
@ -31,5 +31,3 @@ c9df77cbd6a365dcb73c39618e4842711817e871
|
||||||
4546d5445aaefe6a03957db028c263521dfb5c4b
|
4546d5445aaefe6a03957db028c263521dfb5c4b
|
||||||
# Migration to ruff / ruff format
|
# Migration to ruff / ruff format
|
||||||
4588653b2497ed25976b7aaff225b889fb476756
|
4588653b2497ed25976b7aaff225b889fb476756
|
||||||
# Use format specifiers instead of percent format
|
|
||||||
4788165e69d08e10fc6b9c0124083fb358e2e9b0
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ 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.6.0
|
uses: hynek/build-and-inspect-python-package@v2.5.0
|
||||||
with:
|
with:
|
||||||
attest-build-provenance-github: 'true'
|
attest-build-provenance-github: 'true'
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ jobs:
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Publish package to PyPI
|
- name: Publish package to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.9.0
|
uses: pypa/gh-action-pypi-publish@v1.8.14
|
||||||
|
|
||||||
- name: Push tag
|
- name: Push tag
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -14,11 +14,6 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- "[0-9]+.[0-9]+.x"
|
- "[0-9]+.[0-9]+.x"
|
||||||
types:
|
|
||||||
- opened # default
|
|
||||||
- synchronize # default
|
|
||||||
- reopened # default
|
|
||||||
- ready_for_review # used in PRs created from the release workflow
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTEST_ADDOPTS: "--color=yes"
|
PYTEST_ADDOPTS: "--color=yes"
|
||||||
|
@ -40,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.6.0
|
uses: hynek/build-and-inspect-python-package@v2.5.0
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [package]
|
needs: [package]
|
||||||
|
|
|
@ -46,8 +46,7 @@ jobs:
|
||||||
run: python scripts/update-plugin-list.py
|
run: python scripts/update-plugin-list.py
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: pr
|
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c
|
|
||||||
with:
|
with:
|
||||||
commit-message: '[automated] Update plugin list'
|
commit-message: '[automated] Update plugin list'
|
||||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||||
|
@ -56,13 +55,3 @@ jobs:
|
||||||
branch-suffix: short-commit-hash
|
branch-suffix: short-commit-hash
|
||||||
title: '[automated] Update plugin list'
|
title: '[automated] Update plugin list'
|
||||||
body: '[automated] Update plugin list'
|
body: '[automated] Update plugin list'
|
||||||
draft: true
|
|
||||||
|
|
||||||
- name: Instruct the maintainers to trigger CI by undrafting the PR
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: >-
|
|
||||||
gh pr comment
|
|
||||||
--body 'Please mark the PR as ready for review to trigger PR checks.'
|
|
||||||
--repo '${{ github.repository }}'
|
|
||||||
'${{ steps.pr.outputs.pull-request-number }}'
|
|
||||||
|
|
|
@ -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.10"
|
rev: "v0.4.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: ["--fix"]
|
args: ["--fix"]
|
||||||
|
@ -10,7 +10,12 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
|
- id: fix-encoding-pragma
|
||||||
|
args: [--remove]
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
- id: debug-statements
|
||||||
|
exclude: _pytest/(debugging|hookspec).py
|
||||||
|
language_version: python3
|
||||||
- repo: https://github.com/adamchainz/blacken-docs
|
- repo: https://github.com/adamchainz/blacken-docs
|
||||||
rev: 1.16.0
|
rev: 1.16.0
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -21,7 +26,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-use-type-annotations
|
- id: python-use-type-annotations
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.10.0
|
rev: v1.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/|scripts/)
|
files: ^(src/|testing/|scripts/)
|
||||||
|
@ -38,25 +43,20 @@ 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: "2.1.3"
|
rev: "1.8.0"
|
||||||
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
|
||||||
name: pylint
|
name: pylint
|
||||||
entry: pylint
|
entry: pylint
|
||||||
language: system
|
language: system
|
||||||
types: [python]
|
types: [python]
|
||||||
args: ["-rn", "-sn", "--fail-on=I"]
|
args: ["-rn", "-sn", "--fail-on=I"]
|
||||||
stages: [manual]
|
stages: [manual]
|
||||||
- id: rst
|
- id: rst
|
||||||
name: rst
|
name: rst
|
||||||
entry: rst-lint --encoding utf-8
|
entry: rst-lint --encoding utf-8
|
||||||
|
|
6
AUTHORS
6
AUTHORS
|
@ -149,7 +149,6 @@ Evgeny Seliverstov
|
||||||
Fabian Sturm
|
Fabian Sturm
|
||||||
Fabien Zarifian
|
Fabien Zarifian
|
||||||
Fabio Zadrozny
|
Fabio Zadrozny
|
||||||
Farbod Ahmadian
|
|
||||||
faph
|
faph
|
||||||
Felix Hofstätter
|
Felix Hofstätter
|
||||||
Felix Nieuwenhuizen
|
Felix Nieuwenhuizen
|
||||||
|
@ -213,7 +212,6 @@ Jordan Guymon
|
||||||
Jordan Moldow
|
Jordan Moldow
|
||||||
Jordan Speicher
|
Jordan Speicher
|
||||||
Joseph Hunkeler
|
Joseph Hunkeler
|
||||||
Joseph Sawaya
|
|
||||||
Josh Karpel
|
Josh Karpel
|
||||||
Joshua Bronson
|
Joshua Bronson
|
||||||
Jurko Gospodnetić
|
Jurko Gospodnetić
|
||||||
|
@ -246,7 +244,6 @@ Levon Saldamli
|
||||||
Lewis Cowles
|
Lewis Cowles
|
||||||
Llandy Riveron Del Risco
|
Llandy Riveron Del Risco
|
||||||
Loic Esteve
|
Loic Esteve
|
||||||
lovetheguitar
|
|
||||||
Lukas Bednar
|
Lukas Bednar
|
||||||
Luke Murphy
|
Luke Murphy
|
||||||
Maciek Fijalkowski
|
Maciek Fijalkowski
|
||||||
|
@ -283,7 +280,6 @@ 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
|
||||||
|
@ -430,7 +426,6 @@ Victor Rodriguez
|
||||||
Victor Uriarte
|
Victor Uriarte
|
||||||
Vidar T. Fauske
|
Vidar T. Fauske
|
||||||
Vijay Arora
|
Vijay Arora
|
||||||
Virendra Patil
|
|
||||||
Virgil Dupras
|
Virgil Dupras
|
||||||
Vitaly Lashmanov
|
Vitaly Lashmanov
|
||||||
Vivaan Verma
|
Vivaan Verma
|
||||||
|
@ -455,7 +450,6 @@ Yusuke Kadowaki
|
||||||
Yutian Li
|
Yutian Li
|
||||||
Yuval Shimon
|
Yuval Shimon
|
||||||
Zac Hatfield-Dodds
|
Zac Hatfield-Dodds
|
||||||
Zach Snicker
|
|
||||||
Zachary Kneupper
|
Zachary Kneupper
|
||||||
Zachary OBrien
|
Zachary OBrien
|
||||||
Zhouxin Qiu
|
Zhouxin Qiu
|
||||||
|
|
|
@ -10,7 +10,7 @@ if __name__ == "__main__":
|
||||||
import pytest # noqa: F401
|
import pytest # noqa: F401
|
||||||
|
|
||||||
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
|
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
|
||||||
cProfile.run(f"pytest.cmdline.main({script!r})", "prof")
|
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
|
||||||
p = pstats.Stats("prof")
|
p = pstats.Stats("prof")
|
||||||
p.strip_dirs()
|
p.strip_dirs()
|
||||||
p.sort_stats("cumulative")
|
p.sort_stats("cumulative")
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
|
|
||||||
|
|
||||||
Originally added in pytest 8.0.0, but reverted in 8.0.2 due to a regression in pytest-xdist.
|
|
||||||
This regression was fixed in pytest-xdist 3.6.1.
|
|
|
@ -1 +0,0 @@
|
||||||
:func:`pytest.approx` now correctly handles :class:`Sequence <collections.abc.Sequence>`-like objects.
|
|
|
@ -1 +0,0 @@
|
||||||
Documented using :envvar:`PYTEST_VERSION` to detect if code is running from within a pytest run.
|
|
|
@ -1,11 +0,0 @@
|
||||||
Fixed a regression in pytest 8.0 where tracebacks get longer and longer when multiple
|
|
||||||
tests fail due to a shared higher-scope fixture which raised -- by :user:`bluetech`.
|
|
||||||
|
|
||||||
Also fixed a similar regression in pytest 5.4 for collectors which raise during setup.
|
|
||||||
|
|
||||||
The fix necessitated internal changes which may affect some plugins:
|
|
||||||
|
|
||||||
* ``FixtureDef.cached_result[2]`` is now a tuple ``(exc, tb)``
|
|
||||||
instead of ``exc``.
|
|
||||||
* ``SetupState.stack`` failures are now a tuple ``(exc, tb)``
|
|
||||||
instead of ``exc``.
|
|
|
@ -1,11 +0,0 @@
|
||||||
Added `--xfail-tb` flag, which turns on traceback output for XFAIL results.
|
|
||||||
|
|
||||||
* If the `--xfail-tb` flag is not sent, tracebacks for XFAIL results are NOT shown.
|
|
||||||
* The style of traceback for XFAIL is set with `--tb`, and can be `auto|long|short|line|native|no`.
|
|
||||||
* Note: Even if you have `--xfail-tb` set, you won't see them if `--tb=no`.
|
|
||||||
|
|
||||||
Some history:
|
|
||||||
|
|
||||||
With pytest 8.0, `-rx` or `-ra` would not only turn on summary reports for xfail, but also report the tracebacks for xfail results. This caused issues with some projects that utilize xfail, but don't want to see all of the xfail tracebacks.
|
|
||||||
|
|
||||||
This change detaches xfail tracebacks from `-rx`, and now we turn on xfail tracebacks with `--xfail-tb`. With this, the default `-rx`/ `-ra` behavior is identical to pre-8.0 with respect to xfail tracebacks. While this is a behavior change, it brings default behavior back to pre-8.0.0 behavior, which ultimately was considered the better course of action.
|
|
|
@ -1 +0,0 @@
|
||||||
12204.bugfix.rst
|
|
|
@ -1 +0,0 @@
|
||||||
Fix collection error upon encountering an :mod:`abstract <abc>` class, including abstract `unittest.TestCase` subclasses.
|
|
|
@ -1,8 +0,0 @@
|
||||||
Added support for keyword matching in marker expressions.
|
|
||||||
|
|
||||||
Now tests can be selected by marker keyword arguments.
|
|
||||||
Supported values are :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None`.
|
|
||||||
|
|
||||||
See :ref:`marker examples <marker_keyword_expression_example>` for more information.
|
|
||||||
|
|
||||||
-- by :user:`lovetheguitar`
|
|
|
@ -1 +0,0 @@
|
||||||
Fix a regression in pytest 8.0.0 where package-scoped parameterized items were not correctly reordered to minimize setups/teardowns in some cases.
|
|
|
@ -1,6 +0,0 @@
|
||||||
The external plugin mentions in the documentation now avoid mentioning
|
|
||||||
:std:doc:`setuptools entry-points <setuptools:index>` as the concept is
|
|
||||||
much more generic nowadays. Instead, the terminology of "external",
|
|
||||||
"installed", or "third-party" plugins (or packages) replaces that.
|
|
||||||
|
|
||||||
-- by :user:`webknjaz`
|
|
|
@ -1,4 +0,0 @@
|
||||||
The console output now uses the "third-party plugins" terminology,
|
|
||||||
replacing the previously established but confusing and outdated
|
|
||||||
reference to :std:doc:`setuptools <setuptools:index>`
|
|
||||||
-- by :user:`webknjaz`.
|
|
|
@ -1 +0,0 @@
|
||||||
Fixed a crash when returning category ``"error"`` or ``"failed"`` with a custom test status from :hook:`pytest_report_teststatus` hook -- :user:`pbrezina`.
|
|
|
@ -1,7 +0,0 @@
|
||||||
The UX of the GitHub automation making pull requests to update the
|
|
||||||
plugin list has been updated. Previously, the maintainers had to close
|
|
||||||
the automatically created pull requests and re-open them to trigger the
|
|
||||||
CI runs. From now on, they only need to click the `Ready for review`
|
|
||||||
button instead.
|
|
||||||
|
|
||||||
-- by :user:`webknjaz`
|
|
|
@ -1 +0,0 @@
|
||||||
Improve handling of invalid regex patterns in :func:`pytest.raises(match=r'...') <pytest.raises>` by providing a clear error message.
|
|
|
@ -1,3 +0,0 @@
|
||||||
The ``_in_venv()`` function now detects Python virtual environments by
|
|
||||||
checking for a :file:`pyvenv.cfg` file, ensuring reliable detection on
|
|
||||||
various platforms -- by :user:`zachsnickers`.
|
|
|
@ -1 +0,0 @@
|
||||||
12544.improvement.rst
|
|
|
@ -1 +0,0 @@
|
||||||
Do not truncate arguments to functions in output when running with `-vvv`.
|
|
|
@ -1,38 +0,0 @@
|
||||||
The readability of assertion introspection of bound methods has been enhanced
|
|
||||||
-- by :user:`farbodahm`, :user:`webknjaz`, :user:`obestwalter`, :user:`flub`
|
|
||||||
and :user:`glyphack`.
|
|
||||||
|
|
||||||
Earlier, it was like:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
=================================== FAILURES ===================================
|
|
||||||
_____________________________________ test _____________________________________
|
|
||||||
|
|
||||||
def test():
|
|
||||||
> assert Help().fun() == 2
|
|
||||||
E assert 1 == 2
|
|
||||||
E + where 1 = <bound method Help.fun of <example.Help instance at 0x256a830>>()
|
|
||||||
E + where <bound method Help.fun of <example.Help instance at 0x256a830>> = <example.Help instance at 0x256a830>.fun
|
|
||||||
E + where <example.Help instance at 0x256a830> = Help()
|
|
||||||
|
|
||||||
example.py:7: AssertionError
|
|
||||||
=========================== 1 failed in 0.03 seconds ===========================
|
|
||||||
|
|
||||||
|
|
||||||
And now it's like:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
=================================== FAILURES ===================================
|
|
||||||
_____________________________________ test _____________________________________
|
|
||||||
|
|
||||||
def test():
|
|
||||||
> assert Help().fun() == 2
|
|
||||||
E assert 1 == 2
|
|
||||||
E + where 1 = fun()
|
|
||||||
E + where fun = <test_local.Help object at 0x1074be230>.fun
|
|
||||||
E + where <test_local.Help object at 0x1074be230> = Help()
|
|
||||||
|
|
||||||
test_local.py:13: AssertionError
|
|
||||||
=========================== 1 failed in 0.03 seconds ===========================
|
|
|
@ -1 +0,0 @@
|
||||||
Fixed progress percentages (the ``[ 87%]`` at the edge of the screen) sometimes not aligning correctly when running with pytest-xdist ``-n``.
|
|
|
@ -1 +0,0 @@
|
||||||
Added timezone information to the testsuite timestamp in the JUnit XML report.
|
|
|
@ -237,7 +237,7 @@ html_theme = "furo"
|
||||||
html_title = "pytest documentation"
|
html_title = "pytest documentation"
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
html_short_title = f"pytest-{release}"
|
html_short_title = "pytest-%s" % release
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
# of the sidebar.
|
# of the sidebar.
|
||||||
|
|
|
@ -25,12 +25,10 @@ You can "mark" a test function with custom metadata like this:
|
||||||
pass # perform some webtest test for your app
|
pass # perform some webtest test for your app
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.device(serial="123")
|
|
||||||
def test_something_quick():
|
def test_something_quick():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.device(serial="abc")
|
|
||||||
def test_another():
|
def test_another():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -73,28 +71,6 @@ Or the inverse, running all tests except the webtest ones:
|
||||||
|
|
||||||
===================== 3 passed, 1 deselected in 0.12s ======================
|
===================== 3 passed, 1 deselected in 0.12s ======================
|
||||||
|
|
||||||
.. _`marker_keyword_expression_example`:
|
|
||||||
|
|
||||||
Additionally, you can restrict a test run to only run tests matching one or multiple marker
|
|
||||||
keyword arguments, e.g. to run only tests marked with ``device`` and the specific ``serial="123"``:
|
|
||||||
|
|
||||||
.. code-block:: pytest
|
|
||||||
|
|
||||||
$ pytest -v -m "device(serial='123')"
|
|
||||||
=========================== test session starts ============================
|
|
||||||
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
|
|
||||||
cachedir: .pytest_cache
|
|
||||||
rootdir: /home/sweet/project
|
|
||||||
collecting ... collected 4 items / 3 deselected / 1 selected
|
|
||||||
|
|
||||||
test_server.py::test_something_quick PASSED [100%]
|
|
||||||
|
|
||||||
===================== 1 passed, 3 deselected in 0.12s ======================
|
|
||||||
|
|
||||||
.. note:: Only keyword argument matching is supported in marker expressions.
|
|
||||||
|
|
||||||
.. note:: Only :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None` values are supported in marker expressions.
|
|
||||||
|
|
||||||
Selecting tests based on their node ID
|
Selecting tests based on their node ID
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -212,7 +212,7 @@ the command line arguments before they get processed:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
# installable external plugin
|
# setuptools plugin
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
@ -405,20 +405,35 @@ Detect if running from within a pytest run
|
||||||
Usually it is a bad idea to make application code
|
Usually it is a bad idea to make application code
|
||||||
behave differently if called from a test. But if you
|
behave differently if called from a test. But if you
|
||||||
absolutely must find out if your application code is
|
absolutely must find out if your application code is
|
||||||
running from a test you can do this:
|
running from a test you can do something like this:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import os
|
# content of your_module.py
|
||||||
|
|
||||||
|
|
||||||
if os.environ.get("PYTEST_VERSION") is not None:
|
_called_from_test = False
|
||||||
# Things you want to to do if your code is called by pytest.
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# content of conftest.py
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
your_module._called_from_test = True
|
||||||
|
|
||||||
|
and then check for the ``your_module._called_from_test`` flag:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if your_module._called_from_test:
|
||||||
|
# called from within a test run
|
||||||
...
|
...
|
||||||
else:
|
else:
|
||||||
# Things you want to to do if your code is not called by pytest.
|
# called "normally"
|
||||||
...
|
...
|
||||||
|
|
||||||
|
accordingly in your application.
|
||||||
|
|
||||||
Adding info to test report header
|
Adding info to test report header
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
|
@ -1073,8 +1088,8 @@ Instead of freezing the pytest runner as a separate executable, you can make
|
||||||
your frozen program work as the pytest runner by some clever
|
your frozen program work as the pytest runner by some clever
|
||||||
argument handling during program startup. This allows you to
|
argument handling during program startup. This allows you to
|
||||||
have a single executable, which is usually more convenient.
|
have a single executable, which is usually more convenient.
|
||||||
Please note that the mechanism for plugin discovery used by pytest (:ref:`entry
|
Please note that the mechanism for plugin discovery used by pytest
|
||||||
points <pip-installable plugins>`) doesn't work with frozen executables so pytest
|
(setuptools entry points) doesn't work with frozen executables so pytest
|
||||||
can't find any third party plugins automatically. To include third party plugins
|
can't find any third party plugins automatically. To include third party plugins
|
||||||
like ``pytest-timeout`` they must be imported explicitly and passed on to pytest.main.
|
like ``pytest-timeout`` they must be imported explicitly and passed on to pytest.main.
|
||||||
|
|
||||||
|
|
|
@ -1931,7 +1931,7 @@ The same applies for the test folder level obviously.
|
||||||
Using fixtures from other projects
|
Using fixtures from other projects
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
Usually projects that provide pytest support will use :ref:`entry points <pip-installable plugins>`,
|
Usually projects that provide pytest support will use :ref:`entry points <setuptools entry points>`,
|
||||||
so just installing those projects into an environment will make those fixtures available for use.
|
so just installing those projects into an environment will make those fixtures available for use.
|
||||||
|
|
||||||
In case you want to use fixtures from a project that does not use entry points, you can
|
In case you want to use fixtures from a project that does not use entry points, you can
|
||||||
|
|
|
@ -76,19 +76,11 @@ Specifying a specific parametrization of a test:
|
||||||
|
|
||||||
**Run tests by marker expressions**
|
**Run tests by marker expressions**
|
||||||
|
|
||||||
To run all tests which are decorated with the ``@pytest.mark.slow`` decorator:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest -m slow
|
pytest -m slow
|
||||||
|
|
||||||
|
Will run all tests which are decorated with the ``@pytest.mark.slow`` decorator.
|
||||||
To run all tests which are decorated with the annotated ``@pytest.mark.slow(phase=1)`` decorator,
|
|
||||||
with the ``phase`` keyword argument set to ``1``:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
pytest -m "slow(phase=1)"
|
|
||||||
|
|
||||||
For more information see :ref:`marks <mark>`.
|
For more information see :ref:`marks <mark>`.
|
||||||
|
|
||||||
|
@ -162,7 +154,7 @@ You can early-load plugins (internal and external) explicitly in the command-lin
|
||||||
The option receives a ``name`` parameter, which can be:
|
The option receives a ``name`` parameter, which can be:
|
||||||
|
|
||||||
* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable.
|
* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable.
|
||||||
* The entry-point name of a plugin. This is the name passed to ``importlib`` when the plugin is
|
* The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is
|
||||||
registered. For example to early-load the :pypi:`pytest-cov` plugin you can use::
|
registered. For example to early-load the :pypi:`pytest-cov` plugin you can use::
|
||||||
|
|
||||||
pytest -p pytest_cov
|
pytest -p pytest_cov
|
||||||
|
|
|
@ -16,8 +16,8 @@ reporting by calling :ref:`well specified hooks <hook-reference>` of the followi
|
||||||
|
|
||||||
* builtin plugins: loaded from pytest's internal ``_pytest`` directory.
|
* builtin plugins: loaded from pytest's internal ``_pytest`` directory.
|
||||||
|
|
||||||
* :ref:`external plugins <extplugins>`: installed third-party modules discovered
|
* :ref:`external plugins <extplugins>`: modules discovered through
|
||||||
through :ref:`entry points <pip-installable plugins>` in their packaging metadata
|
`setuptools entry points`_
|
||||||
|
|
||||||
* `conftest.py plugins`_: modules auto-discovered in test directories
|
* `conftest.py plugins`_: modules auto-discovered in test directories
|
||||||
|
|
||||||
|
@ -42,8 +42,7 @@ Plugin discovery order at tool startup
|
||||||
3. by scanning the command line for the ``-p name`` option
|
3. by scanning the command line for the ``-p name`` option
|
||||||
and loading the specified plugin. This happens before normal command-line parsing.
|
and loading the specified plugin. This happens before normal command-line parsing.
|
||||||
|
|
||||||
4. by loading all plugins registered through installed third-party package
|
4. by loading all plugins registered through `setuptools entry points`_.
|
||||||
:ref:`entry points <pip-installable plugins>`.
|
|
||||||
|
|
||||||
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
|
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
|
||||||
|
|
||||||
|
@ -143,8 +142,7 @@ Making your plugin installable by others
|
||||||
If you want to make your plugin externally available, you
|
If you want to make your plugin externally available, you
|
||||||
may define a so-called entry point for your distribution so
|
may define a so-called entry point for your distribution so
|
||||||
that ``pytest`` finds your plugin module. Entry points are
|
that ``pytest`` finds your plugin module. Entry points are
|
||||||
a feature that is provided by :std:doc:`packaging tools
|
a feature that is provided by :std:doc:`setuptools <setuptools:index>`.
|
||||||
<packaging:specifications/entry-points>`.
|
|
||||||
|
|
||||||
pytest looks up the ``pytest11`` entrypoint to discover its
|
pytest looks up the ``pytest11`` entrypoint to discover its
|
||||||
plugins, thus you can make your plugin available by defining
|
plugins, thus you can make your plugin available by defining
|
||||||
|
@ -267,9 +265,8 @@ of the variable will also be loaded as plugins, and so on.
|
||||||
tests root directory is deprecated, and will raise a warning.
|
tests root directory is deprecated, and will raise a warning.
|
||||||
|
|
||||||
This mechanism makes it easy to share fixtures within applications or even
|
This mechanism makes it easy to share fixtures within applications or even
|
||||||
external applications without the need to create external plugins using the
|
external applications without the need to create external plugins using
|
||||||
:std:doc:`entry point packaging metadata
|
the ``setuptools``'s entry point technique.
|
||||||
<packaging:guides/creating-and-discovering-plugins>` technique.
|
|
||||||
|
|
||||||
Plugins imported by :globalvar:`pytest_plugins` will also automatically be marked
|
Plugins imported by :globalvar:`pytest_plugins` will also automatically be marked
|
||||||
for assertion rewriting (see :func:`pytest.register_assert_rewrite`).
|
for assertion rewriting (see :func:`pytest.register_assert_rewrite`).
|
||||||
|
|
|
@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
|
||||||
setup.cfg
|
setup.cfg
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and :std:doc:`setuptools <setuptools:userguide/declarative_config>`, and can also be used to hold pytest configuration
|
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
|
||||||
if they have a ``[tool:pytest]`` section.
|
if they have a ``[tool:pytest]`` section.
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -650,7 +650,7 @@ Reference to all hooks which can be implemented by :ref:`conftest.py files <loca
|
||||||
Bootstrapping hooks
|
Bootstrapping hooks
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Bootstrapping hooks called for plugins registered early enough (internal and third-party plugins).
|
Bootstrapping hooks called for plugins registered early enough (internal and setuptools plugins).
|
||||||
|
|
||||||
.. hook:: pytest_load_initial_conftests
|
.. hook:: pytest_load_initial_conftests
|
||||||
.. autofunction:: pytest_load_initial_conftests
|
.. autofunction:: pytest_load_initial_conftests
|
||||||
|
@ -1147,9 +1147,8 @@ When set, pytest will print tracing and debug information.
|
||||||
|
|
||||||
.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD
|
.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD
|
||||||
|
|
||||||
When set, disables plugin auto-loading through :std:doc:`entry point packaging
|
When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be
|
||||||
metadata <packaging:guides/creating-and-discovering-plugins>`. Only explicitly
|
loaded.
|
||||||
specified plugins will be loaded.
|
|
||||||
|
|
||||||
.. envvar:: PYTEST_PLUGINS
|
.. envvar:: PYTEST_PLUGINS
|
||||||
|
|
||||||
|
@ -1702,13 +1701,13 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
This would tell ``pytest`` to not look into typical subversion or
|
This would tell ``pytest`` to not look into typical subversion or
|
||||||
sphinx-build directories or into any ``tmp`` prefixed directory.
|
sphinx-build directories or into any ``tmp`` prefixed directory.
|
||||||
|
|
||||||
Additionally, ``pytest`` will attempt to intelligently identify and ignore
|
Additionally, ``pytest`` will attempt to intelligently identify and ignore a
|
||||||
a virtualenv. Any directory deemed to be the root of a virtual environment
|
virtualenv by the presence of an activation script. Any directory deemed to
|
||||||
will not be considered during test collection unless
|
be the root of a virtual environment will not be considered during test
|
||||||
``--collect-in-virtualenv`` is given. Note also that ``norecursedirs``
|
collection unless ``--collect-in-virtualenv`` is given. Note also that
|
||||||
takes precedence over ``--collect-in-virtualenv``; e.g. if you intend to
|
``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if
|
||||||
run tests in a virtualenv with a base directory that matches ``'.*'`` you
|
you intend to run tests in a virtualenv with a base directory that matches
|
||||||
*must* override ``norecursedirs`` in addition to using the
|
``'.*'`` you *must* override ``norecursedirs`` in addition to using the
|
||||||
``--collect-in-virtualenv`` flag.
|
``--collect-in-virtualenv`` flag.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
@ -20,7 +19,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"])
|
||||||
sys.exit(1)
|
exit(1)
|
||||||
issues.extend(data)
|
issues.extend(data)
|
||||||
|
|
||||||
# Look for next page
|
# Look for next page
|
||||||
|
@ -63,7 +62,7 @@ def report(issues):
|
||||||
kind = _get_kind(issue)
|
kind = _get_kind(issue)
|
||||||
status = issue["state"]
|
status = issue["state"]
|
||||||
number = issue["number"]
|
number = issue["number"]
|
||||||
link = f"https://github.com/pytest-dev/pytest/issues/{number}/"
|
link = "https://github.com/pytest-dev/pytest/issues/%s/" % number
|
||||||
print("----")
|
print("----")
|
||||||
print(status, kind, link)
|
print(status, kind, link)
|
||||||
print(title)
|
print(title)
|
||||||
|
@ -72,7 +71,7 @@ def report(issues):
|
||||||
# print("\n".join(lines[:3]))
|
# print("\n".join(lines[:3]))
|
||||||
# if len(lines) > 3 or len(body) > 240:
|
# if len(lines) > 3 or len(body) > 240:
|
||||||
# print("...")
|
# print("...")
|
||||||
print(f"\n\nFound {len(issues)} open issues")
|
print("\n\nFound %s open issues" % len(issues))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
214
pyproject.toml
214
pyproject.toml
|
@ -1,10 +1,3 @@
|
||||||
[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"
|
||||||
|
@ -38,6 +31,7 @@ classifiers = [
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
"Topic :: Software Development :: Libraries",
|
"Topic :: Software Development :: Libraries",
|
||||||
"Topic :: Software Development :: Testing",
|
"Topic :: Software Development :: Testing",
|
||||||
"Topic :: Utilities",
|
"Topic :: Utilities",
|
||||||
|
@ -46,14 +40,15 @@ 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,>=1.5",
|
"pluggy<2.0,>=1.5",
|
||||||
"tomli>=1; python_version<'3.11'",
|
'tomli>=1; python_version < "3.11"',
|
||||||
]
|
]
|
||||||
optional-dependencies.dev = [
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
"argcomplete",
|
"argcomplete",
|
||||||
"attrs>=19.2",
|
"attrs>=19.2",
|
||||||
"hypothesis>=3.56",
|
"hypothesis>=3.56",
|
||||||
|
@ -63,55 +58,59 @@ optional-dependencies.dev = [
|
||||||
"setuptools",
|
"setuptools",
|
||||||
"xmlschema",
|
"xmlschema",
|
||||||
]
|
]
|
||||||
urls.Changelog = "https://docs.pytest.org/en/stable/changelog.html"
|
[project.urls]
|
||||||
urls.Homepage = "https://docs.pytest.org/en/latest/"
|
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
|
||||||
urls.Source = "https://github.com/pytest-dev/pytest"
|
Homepage = "https://docs.pytest.org/en/latest/"
|
||||||
urls.Tracker = "https://github.com/pytest-dev/pytest/issues"
|
Source = "https://github.com/pytest-dev/pytest"
|
||||||
urls.Twitter = "https://twitter.com/pytestdotorg"
|
Tracker = "https://github.com/pytest-dev/pytest/issues"
|
||||||
scripts."py.test" = "pytest:console_main"
|
Twitter = "https://twitter.com/pytestdotorg"
|
||||||
scripts.pytest = "pytest:console_main"
|
[project.scripts]
|
||||||
|
"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" = [
|
"_pytest" = ["py.typed"]
|
||||||
"py.typed",
|
"pytest" = ["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 = [
|
target-version = ['py38']
|
||||||
'py38',
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
src = ["src"]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
src = [
|
|
||||||
"src",
|
[tool.ruff.format]
|
||||||
]
|
docstring-code-format = true
|
||||||
format.docstring-code-format = true
|
|
||||||
lint.select = [
|
[tool.ruff.lint]
|
||||||
"B", # bugbear
|
select = [
|
||||||
"D", # pydocstyle
|
"B", # bugbear
|
||||||
"E", # pycodestyle
|
"D", # pydocstyle
|
||||||
"F", # pyflakes
|
"E", # pycodestyle
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
"FA100", # add future annotations
|
"FA100", # add future annotations
|
||||||
"I", # isort
|
"PYI", # flake8-pyi
|
||||||
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
|
"UP", # pyupgrade
|
||||||
"PIE", # flake8-pie
|
"RUF", # ruff
|
||||||
"PLE", # pylint error
|
"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
|
"PLR1714", # Consider merging multiple comparisons
|
||||||
"PLW", # pylint warning
|
|
||||||
"PYI", # flake8-pyi
|
|
||||||
"RUF", # ruff
|
|
||||||
"T100", # flake8-debugger
|
|
||||||
"UP", # pyupgrade
|
|
||||||
"W", # pycodestyle
|
|
||||||
]
|
]
|
||||||
lint.ignore = [
|
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
|
||||||
|
@ -119,6 +118,10 @@ lint.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
|
||||||
|
@ -128,54 +131,41 @@ lint.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__`
|
||||||
"D205", # 1 blank line required between summary line and description
|
|
||||||
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
|
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
|
||||||
|
"D205", # 1 blank line required between summary line and description
|
||||||
"D400", # First line should end with a period
|
"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" = [
|
|
||||||
"B",
|
[tool.ruff.lint.pycodestyle]
|
||||||
"PYI",
|
|
||||||
]
|
|
||||||
lint.per-file-ignores."src/_pytest/_version.py" = [
|
|
||||||
"I001",
|
|
||||||
]
|
|
||||||
lint.per-file-ignores."testing/python/approx.py" = [
|
|
||||||
"B015",
|
|
||||||
]
|
|
||||||
lint.extend-safe-fixes = [
|
|
||||||
"UP006",
|
|
||||||
"UP007",
|
|
||||||
]
|
|
||||||
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
|
|
||||||
lint.isort.required-imports = [
|
|
||||||
"from __future__ import annotations",
|
|
||||||
]
|
|
||||||
# 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
|
||||||
lint.pycodestyle.max-line-length = 120
|
max-line-length = 120
|
||||||
lint.pydocstyle.convention = "pep257"
|
|
||||||
lint.pyupgrade.keep-runtime-typing = false
|
[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.
|
||||||
|
@ -190,25 +180,28 @@ 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", # B023 from ruff / flake8-bugbear
|
"cell-var-from-loop",
|
||||||
"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", # foo / bar are used often in tests
|
"disallowed-name",
|
||||||
"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",
|
||||||
|
@ -219,9 +212,10 @@ 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-on-one-line-colon (E701) from ruff
|
"multiple-statements",
|
||||||
"no-else-break",
|
"no-else-break",
|
||||||
"no-else-continue",
|
"no-else-continue",
|
||||||
"no-else-raise",
|
"no-else-raise",
|
||||||
|
@ -234,7 +228,6 @@ 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",
|
||||||
|
@ -282,6 +275,7 @@ 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",
|
||||||
]
|
]
|
||||||
|
@ -297,27 +291,16 @@ 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 = [
|
python_files = ["test_*.py", "*_test.py", "testing/python/*.py"]
|
||||||
"test_*.py",
|
python_classes = ["Test", "Acceptance"]
|
||||||
"*_test.py",
|
python_functions = ["test"]
|
||||||
"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 = [
|
testpaths = ["testing"]
|
||||||
"testing",
|
|
||||||
]
|
|
||||||
norecursedirs = [
|
norecursedirs = [
|
||||||
"testing/example_scripts",
|
"testing/example_scripts",
|
||||||
".*",
|
".*",
|
||||||
"build",
|
"build",
|
||||||
"dist",
|
"dist",
|
||||||
]
|
]
|
||||||
xfail_strict = true
|
xfail_strict = true
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
|
@ -358,9 +341,6 @@ markers = [
|
||||||
"foo",
|
"foo",
|
||||||
"bar",
|
"bar",
|
||||||
"baz",
|
"baz",
|
||||||
"number_mark",
|
|
||||||
"builtin_matchers_mark",
|
|
||||||
"str_mark",
|
|
||||||
# conftest.py reorders tests moving slow ones to the end of the list
|
# conftest.py reorders tests moving slow ones to the end of the list
|
||||||
"slow",
|
"slow",
|
||||||
# experimental mark for all tests using pexpect
|
# experimental mark for all tests using pexpect
|
||||||
|
@ -446,14 +426,8 @@ name = "Miscellaneous internal changes"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
files = [
|
files = ["src", "testing", "scripts"]
|
||||||
"src",
|
mypy_path = ["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
|
||||||
|
|
|
@ -54,7 +54,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"]
|
||||||
|
|
||||||
EXCEPTION_OR_MORE = Union[Type[Exception], Tuple[Type[Exception], ...]]
|
EXCEPTION_OR_MORE = Union[Type[Exception], Tuple[Type[Exception], ...]]
|
||||||
|
|
||||||
|
@ -618,13 +618,12 @@ 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: bool
|
tbfilter: bool
|
||||||
| Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True,
|
| Callable[[ExceptionInfo[BaseException]], _pytest._code.code.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,
|
||||||
) -> ReprExceptionInfo | ExceptionChainRepr:
|
) -> ReprExceptionInfo | ExceptionChainRepr:
|
||||||
"""Return str()able representation of this exception info.
|
"""Return str()able representation of this exception info.
|
||||||
|
@ -655,9 +654,6 @@ 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.
|
||||||
|
|
||||||
|
@ -684,7 +680,6 @@ 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)
|
||||||
|
@ -798,12 +793,11 @@ 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: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True
|
tbfilter: 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[str | Path, ast.AST] = dataclasses.field(
|
astcache: dict[str | Path, ast.AST] = dataclasses.field(
|
||||||
default_factory=dict, init=False, repr=False
|
default_factory=dict, init=False, repr=False
|
||||||
|
@ -834,11 +828,7 @@ 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):
|
||||||
if self.truncate_args:
|
args.append((argname, saferepr(argvalue)))
|
||||||
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
|
||||||
|
|
||||||
|
@ -938,7 +928,7 @@ class FormattedExcinfo:
|
||||||
s = self.get_source(source, line_index, excinfo, short=short)
|
s = self.get_source(source, line_index, excinfo, short=short)
|
||||||
lines.extend(s)
|
lines.extend(s)
|
||||||
if short:
|
if short:
|
||||||
message = f"in {entry.name}"
|
message = "in %s" % (entry.name)
|
||||||
else:
|
else:
|
||||||
message = excinfo and excinfo.typename or ""
|
message = excinfo and excinfo.typename or ""
|
||||||
entry_path = entry.path
|
entry_path = entry.path
|
||||||
|
@ -1157,7 +1147,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||||
class ReprTraceback(TerminalRepr):
|
class ReprTraceback(TerminalRepr):
|
||||||
reprentries: Sequence[ReprEntry | ReprEntryNative]
|
reprentries: Sequence[ReprEntry | ReprEntryNative]
|
||||||
extraline: str | None
|
extraline: str | None
|
||||||
style: TracebackStyle
|
style: _TracebackStyle
|
||||||
|
|
||||||
entrysep: ClassVar = "_ "
|
entrysep: ClassVar = "_ "
|
||||||
|
|
||||||
|
@ -1191,7 +1181,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))
|
||||||
|
@ -1203,7 +1193,7 @@ class ReprEntry(TerminalRepr):
|
||||||
reprfuncargs: ReprFuncArgs | None
|
reprfuncargs: ReprFuncArgs | None
|
||||||
reprlocals: ReprLocals | None
|
reprlocals: ReprLocals | None
|
||||||
reprfileloc: ReprFileLocation | None
|
reprfileloc: ReprFileLocation | None
|
||||||
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.
|
||||||
|
|
|
@ -613,7 +613,7 @@ class PrettyPrinter:
|
||||||
vrepr = self._safe_repr(v, context, maxlevels, level)
|
vrepr = self._safe_repr(v, context, maxlevels, level)
|
||||||
append(f"{krepr}: {vrepr}")
|
append(f"{krepr}: {vrepr}")
|
||||||
context.remove(objid)
|
context.remove(objid)
|
||||||
return "{{{}}}".format(", ".join(components))
|
return "{%s}" % ", ".join(components)
|
||||||
|
|
||||||
if (issubclass(typ, list) and r is list.__repr__) or (
|
if (issubclass(typ, list) and r is list.__repr__) or (
|
||||||
issubclass(typ, tuple) and r is tuple.__repr__
|
issubclass(typ, tuple) and r is tuple.__repr__
|
||||||
|
|
|
@ -60,6 +60,7 @@ class SafeRepr(reprlib.Repr):
|
||||||
s = ascii(x)
|
s = ascii(x)
|
||||||
else:
|
else:
|
||||||
s = super().repr(x)
|
s = super().repr(x)
|
||||||
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
raise
|
raise
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
|
|
|
@ -9,17 +9,11 @@ from typing import final
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,7 +105,7 @@ class TerminalWriter:
|
||||||
if self.hasmarkup:
|
if self.hasmarkup:
|
||||||
esc = [self._esctable[name] for name, on in markup.items() if on]
|
esc = [self._esctable[name] for name, on in markup.items() if on]
|
||||||
if esc:
|
if esc:
|
||||||
text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m"
|
text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m"
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def sep(
|
def sep(
|
||||||
|
@ -201,74 +195,58 @@ 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 _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer | None:
|
|
||||||
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) -> Formatter | None:
|
|
||||||
try:
|
|
||||||
import pygments.util
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
from _pytest.config.exceptions import UsageError
|
|
||||||
|
|
||||||
theme = os.getenv("PYTEST_THEME")
|
|
||||||
theme_mode = os.getenv("PYTEST_THEME_MODE", "dark")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from pygments.formatters.terminal import TerminalFormatter
|
|
||||||
|
|
||||||
return TerminalFormatter(bg=theme_mode, style=theme)
|
|
||||||
|
|
||||||
except pygments.util.ClassNotFound as e:
|
|
||||||
raise UsageError(
|
|
||||||
f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
|
|
||||||
"Hint: See available pygments styles with `pygmentize -L styles`."
|
|
||||||
) from e
|
|
||||||
except pygments.util.OptionError as e:
|
|
||||||
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(
|
def _highlight(
|
||||||
self, source: str, lexer: Literal["diff", "python"] = "python"
|
self, source: str, lexer: Literal["diff", "python"] = "python"
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Highlight the given source if we have markup support."""
|
"""Highlight the given source if we have markup support."""
|
||||||
|
from _pytest.config.exceptions import UsageError
|
||||||
|
|
||||||
if not source or not self.hasmarkup or not self.code_highlight:
|
if not source or not self.hasmarkup or not self.code_highlight:
|
||||||
return source
|
return source
|
||||||
|
|
||||||
pygments_lexer = self._get_pygments_lexer(lexer)
|
try:
|
||||||
if pygments_lexer is None:
|
from pygments.formatters.terminal import TerminalFormatter
|
||||||
|
|
||||||
|
if lexer == "python":
|
||||||
|
from pygments.lexers.python import PythonLexer as Lexer
|
||||||
|
elif lexer == "diff":
|
||||||
|
from pygments.lexers.diff import DiffLexer as Lexer
|
||||||
|
else:
|
||||||
|
assert_never(lexer)
|
||||||
|
from pygments import highlight
|
||||||
|
import pygments.util
|
||||||
|
except ImportError:
|
||||||
return source
|
return source
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
highlighted: str = highlight(
|
||||||
|
source,
|
||||||
|
Lexer(),
|
||||||
|
TerminalFormatter(
|
||||||
|
bg=os.getenv("PYTEST_THEME_MODE", "dark"),
|
||||||
|
style=os.getenv("PYTEST_THEME"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# 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]
|
||||||
|
|
||||||
pygments_formatter = self._get_pygments_formatter()
|
# Some lexers will not set the initial color explicitly
|
||||||
if pygments_formatter is None:
|
# which may lead to the previous color being propagated to the
|
||||||
return source
|
# start of the expression, so reset first.
|
||||||
|
return "\x1b[0m" + highlighted
|
||||||
from pygments import highlight
|
except pygments.util.ClassNotFound as e:
|
||||||
|
raise UsageError(
|
||||||
highlighted: str = highlight(source, pygments_lexer, pygments_formatter)
|
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
||||||
# pygments terminal formatter may add a newline when there wasn't one.
|
"Only valid pygment styles are allowed.".format(
|
||||||
# We don't want this, remove.
|
os.getenv("PYTEST_THEME")
|
||||||
if highlighted[-1] == "\n" and source[-1] != "\n":
|
)
|
||||||
highlighted = highlighted[:-1]
|
) from e
|
||||||
|
except pygments.util.OptionError as e:
|
||||||
# Some lexers will not set the initial color explicitly
|
raise UsageError(
|
||||||
# which may lead to the previous color being propagated to the
|
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
|
||||||
# start of the expression, so reset first.
|
"The only allowed values are 'dark' and 'light'.".format(
|
||||||
highlighted = "\x1b[0m" + highlighted
|
os.getenv("PYTEST_THEME_MODE")
|
||||||
|
)
|
||||||
return highlighted
|
) from e
|
||||||
|
|
|
@ -161,13 +161,15 @@ class Visitor:
|
||||||
)
|
)
|
||||||
if not self.breadthfirst:
|
if not self.breadthfirst:
|
||||||
for subdir in dirs:
|
for subdir in dirs:
|
||||||
yield from self.gen(subdir)
|
for p in 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:
|
||||||
yield from self.gen(subdir)
|
for p in self.gen(subdir):
|
||||||
|
yield p
|
||||||
|
|
||||||
|
|
||||||
class FNMatcher:
|
class FNMatcher:
|
||||||
|
@ -657,7 +659,7 @@ class LocalPath:
|
||||||
)
|
)
|
||||||
if "basename" in kw:
|
if "basename" in kw:
|
||||||
if "purebasename" in kw or "ext" in kw:
|
if "purebasename" in kw or "ext" in kw:
|
||||||
raise ValueError(f"invalid specification {kw!r}")
|
raise ValueError("invalid specification %r" % kw)
|
||||||
else:
|
else:
|
||||||
pb = kw.setdefault("purebasename", purebasename)
|
pb = kw.setdefault("purebasename", purebasename)
|
||||||
try:
|
try:
|
||||||
|
@ -703,7 +705,7 @@ class LocalPath:
|
||||||
elif name == "ext":
|
elif name == "ext":
|
||||||
res.append(ext)
|
res.append(ext)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"invalid part specification {name!r}")
|
raise ValueError("invalid part specification %r" % name)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def dirpath(self, *args, **kwargs):
|
def dirpath(self, *args, **kwargs):
|
||||||
|
@ -1024,7 +1026,7 @@ class LocalPath:
|
||||||
return self.stat().atime
|
return self.stat().atime
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"local({self.strpath!r})"
|
return "local(%r)" % self.strpath
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return string representation of the Path."""
|
"""Return string representation of the Path."""
|
||||||
|
|
|
@ -97,7 +97,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||||
state = self.config.stash[assertstate_key]
|
state = self.config.stash[assertstate_key]
|
||||||
if self._early_rewrite_bailout(name, state):
|
if self._early_rewrite_bailout(name, state):
|
||||||
return None
|
return None
|
||||||
state.trace(f"find_module called for: {name}")
|
state.trace("find_module called for: %s" % name)
|
||||||
|
|
||||||
# Type ignored because mypy is confused about the `self` binding here.
|
# Type ignored because mypy is confused about the `self` binding here.
|
||||||
spec = self._find_spec(name, path) # type: ignore
|
spec = self._find_spec(name, path) # type: ignore
|
||||||
|
@ -269,7 +269,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||||
|
|
||||||
self.config.issue_config_time_warning(
|
self.config.issue_config_time_warning(
|
||||||
PytestAssertRewriteWarning(
|
PytestAssertRewriteWarning(
|
||||||
f"Module already imported so cannot be rewritten: {name}"
|
"Module already imported so cannot be rewritten: %s" % name
|
||||||
),
|
),
|
||||||
stacklevel=5,
|
stacklevel=5,
|
||||||
)
|
)
|
||||||
|
@ -370,21 +370,21 @@ def _read_pyc(
|
||||||
return None
|
return None
|
||||||
# Check for invalid or out of date pyc file.
|
# Check for invalid or out of date pyc file.
|
||||||
if len(data) != (16):
|
if len(data) != (16):
|
||||||
trace(f"_read_pyc({source}): invalid pyc (too short)")
|
trace("_read_pyc(%s): invalid pyc (too short)" % source)
|
||||||
return None
|
return None
|
||||||
if data[:4] != importlib.util.MAGIC_NUMBER:
|
if data[:4] != importlib.util.MAGIC_NUMBER:
|
||||||
trace(f"_read_pyc({source}): invalid pyc (bad magic number)")
|
trace("_read_pyc(%s): invalid pyc (bad magic number)" % source)
|
||||||
return None
|
return None
|
||||||
if data[4:8] != b"\x00\x00\x00\x00":
|
if data[4:8] != b"\x00\x00\x00\x00":
|
||||||
trace(f"_read_pyc({source}): invalid pyc (unsupported flags)")
|
trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source)
|
||||||
return None
|
return None
|
||||||
mtime_data = data[8:12]
|
mtime_data = data[8:12]
|
||||||
if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
|
if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
|
||||||
trace(f"_read_pyc({source}): out of date")
|
trace("_read_pyc(%s): out of date" % source)
|
||||||
return None
|
return None
|
||||||
size_data = data[12:16]
|
size_data = data[12:16]
|
||||||
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
|
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
|
||||||
trace(f"_read_pyc({source}): invalid pyc (incorrect size)")
|
trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
co = marshal.load(fp)
|
co = marshal.load(fp)
|
||||||
|
@ -392,7 +392,7 @@ def _read_pyc(
|
||||||
trace(f"_read_pyc({source}): marshal.load error {e}")
|
trace(f"_read_pyc({source}): marshal.load error {e}")
|
||||||
return None
|
return None
|
||||||
if not isinstance(co, types.CodeType):
|
if not isinstance(co, types.CodeType):
|
||||||
trace(f"_read_pyc({source}): not a code object")
|
trace("_read_pyc(%s): not a code object" % source)
|
||||||
return None
|
return None
|
||||||
return co
|
return co
|
||||||
|
|
||||||
|
@ -417,10 +417,6 @@ def _saferepr(obj: object) -> str:
|
||||||
sequences, especially '\n{' and '\n}' are likely to be present in
|
sequences, especially '\n{' and '\n}' are likely to be present in
|
||||||
JSON reprs.
|
JSON reprs.
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, types.MethodType):
|
|
||||||
# for bound methods, skip redundant <bound method ...> information
|
|
||||||
return obj.__name__
|
|
||||||
|
|
||||||
maxsize = _get_maxsize_for_saferepr(util._config)
|
maxsize = _get_maxsize_for_saferepr(util._config)
|
||||||
return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
|
return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
|
||||||
|
|
||||||
|
|
|
@ -292,7 +292,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]:
|
||||||
if i > 42:
|
if i > 42:
|
||||||
i -= 10 # Provide some context
|
i -= 10 # Provide some context
|
||||||
explanation = [
|
explanation = [
|
||||||
f"Skipping {i} identical leading characters in diff, use -v to show"
|
"Skipping %s identical leading characters in diff, use -v to show" % i
|
||||||
]
|
]
|
||||||
left = left[i:]
|
left = left[i:]
|
||||||
right = right[i:]
|
right = right[i:]
|
||||||
|
@ -493,7 +493,7 @@ def _compare_eq_dict(
|
||||||
common = set_left.intersection(set_right)
|
common = set_left.intersection(set_right)
|
||||||
same = {k: left[k] for k in common if left[k] == right[k]}
|
same = {k: left[k] for k in common if left[k] == right[k]}
|
||||||
if same and verbose < 2:
|
if same and verbose < 2:
|
||||||
explanation += [f"Omitting {len(same)} identical items, use -vv to show"]
|
explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
|
||||||
elif same:
|
elif same:
|
||||||
explanation += ["Common items:"]
|
explanation += ["Common items:"]
|
||||||
explanation += highlighter(pprint.pformat(same)).splitlines()
|
explanation += highlighter(pprint.pformat(same)).splitlines()
|
||||||
|
@ -560,7 +560,7 @@ def _compare_eq_cls(
|
||||||
if same or diff:
|
if same or diff:
|
||||||
explanation += [""]
|
explanation += [""]
|
||||||
if same and verbose < 2:
|
if same and verbose < 2:
|
||||||
explanation.append(f"Omitting {len(same)} identical items, use -vv to show")
|
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
|
||||||
elif same:
|
elif same:
|
||||||
explanation += ["Matching attributes:"]
|
explanation += ["Matching attributes:"]
|
||||||
explanation += highlighter(pprint.pformat(same)).splitlines()
|
explanation += highlighter(pprint.pformat(same)).splitlines()
|
||||||
|
@ -590,7 +590,7 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]:
|
||||||
tail = text[index + len(term) :]
|
tail = text[index + len(term) :]
|
||||||
correct_text = head + tail
|
correct_text = head + tail
|
||||||
diff = _diff_text(text, correct_text, verbose)
|
diff = _diff_text(text, correct_text, verbose)
|
||||||
newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"]
|
newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
|
||||||
for line in diff:
|
for line in diff:
|
||||||
if line.startswith("Skipping"):
|
if line.startswith("Skipping"):
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -218,9 +218,9 @@ class Cache:
|
||||||
os.umask(umask)
|
os.umask(umask)
|
||||||
path.chmod(0o777 - umask)
|
path.chmod(0o777 - umask)
|
||||||
|
|
||||||
with open(path.joinpath("README.md"), "x", encoding="UTF-8") as f:
|
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
|
||||||
f.write(README_CONTENT)
|
f.write(README_CONTENT)
|
||||||
with open(path.joinpath(".gitignore"), "x", encoding="UTF-8") as f:
|
with open(path.joinpath(".gitignore"), "xt", 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)
|
||||||
|
@ -347,7 +347,7 @@ class LFPlugin:
|
||||||
|
|
||||||
def pytest_report_collectionfinish(self) -> str | None:
|
def pytest_report_collectionfinish(self) -> str | None:
|
||||||
if self.active and self.config.getoption("verbose") >= 0:
|
if self.active and self.config.getoption("verbose") >= 0:
|
||||||
return f"run-last-failure: {self._report_status}"
|
return "run-last-failure: %s" % self._report_status
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||||
|
@ -603,21 +603,21 @@ def cacheshow(config: Config, session: Session) -> int:
|
||||||
dummy = object()
|
dummy = object()
|
||||||
basedir = config.cache._cachedir
|
basedir = config.cache._cachedir
|
||||||
vdir = basedir / Cache._CACHE_PREFIX_VALUES
|
vdir = basedir / Cache._CACHE_PREFIX_VALUES
|
||||||
tw.sep("-", f"cache values for {glob!r}")
|
tw.sep("-", "cache values for %r" % glob)
|
||||||
for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
|
for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
|
||||||
key = str(valpath.relative_to(vdir))
|
key = str(valpath.relative_to(vdir))
|
||||||
val = config.cache.get(key, dummy)
|
val = config.cache.get(key, dummy)
|
||||||
if val is dummy:
|
if val is dummy:
|
||||||
tw.line(f"{key} contains unreadable content, will be ignored")
|
tw.line("%s contains unreadable content, will be ignored" % key)
|
||||||
else:
|
else:
|
||||||
tw.line(f"{key} contains:")
|
tw.line("%s contains:" % key)
|
||||||
for line in pformat(val).splitlines():
|
for line in pformat(val).splitlines():
|
||||||
tw.line(" " + line)
|
tw.line(" " + line)
|
||||||
|
|
||||||
ddir = basedir / Cache._CACHE_PREFIX_DIRS
|
ddir = basedir / Cache._CACHE_PREFIX_DIRS
|
||||||
if ddir.is_dir():
|
if ddir.is_dir():
|
||||||
contents = sorted(ddir.rglob(glob))
|
contents = sorted(ddir.rglob(glob))
|
||||||
tw.sep("-", f"cache directories for {glob!r}")
|
tw.sep("-", "cache directories for %r" % glob)
|
||||||
for p in contents:
|
for p in contents:
|
||||||
# if p.is_dir():
|
# if p.is_dir():
|
||||||
# print("%s/" % p.relative_to(basedir))
|
# print("%s/" % p.relative_to(basedir))
|
||||||
|
|
|
@ -739,7 +739,7 @@ class CaptureManager:
|
||||||
if self.is_globally_capturing():
|
if self.is_globally_capturing():
|
||||||
return "global"
|
return "global"
|
||||||
if self._capture_fixture:
|
if self._capture_fixture:
|
||||||
return f"fixture {self._capture_fixture.request.fixturename}"
|
return "fixture %s" % self._capture_fixture.request.fixturename
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Global capturing control
|
# Global capturing control
|
||||||
|
@ -984,7 +984,6 @@ 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):
|
||||||
|
@ -1012,7 +1011,6 @@ 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):
|
||||||
|
@ -1040,7 +1038,6 @@ 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):
|
||||||
|
@ -1068,7 +1065,6 @@ 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):
|
||||||
|
|
|
@ -50,10 +50,7 @@ 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
|
||||||
|
@ -70,6 +67,9 @@ from _pytest.warning_types import warn_explicit_for
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .argparsing import Argument
|
||||||
|
from .argparsing import Parser
|
||||||
|
from _pytest._code.code import _TracebackStyle
|
||||||
from _pytest.cacheprovider import Cache
|
from _pytest.cacheprovider import Cache
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
|
@ -791,7 +791,7 @@ class PytestPluginManager(PluginManager):
|
||||||
if arg.startswith("no:"):
|
if arg.startswith("no:"):
|
||||||
name = arg[3:]
|
name = arg[3:]
|
||||||
if name in essential_plugins:
|
if name in essential_plugins:
|
||||||
raise UsageError(f"plugin {name} cannot be disabled")
|
raise UsageError("plugin %s cannot be disabled" % name)
|
||||||
|
|
||||||
# PR #4304: remove stepwise if cacheprovider is blocked.
|
# PR #4304: remove stepwise if cacheprovider is blocked.
|
||||||
if name == "cacheprovider":
|
if name == "cacheprovider":
|
||||||
|
@ -840,9 +840,9 @@ class PytestPluginManager(PluginManager):
|
||||||
# "terminal" or "capture". Those plugins are registered under their
|
# "terminal" or "capture". Those plugins are registered under their
|
||||||
# basename for historic purposes but must be imported with the
|
# basename for historic purposes but must be imported with the
|
||||||
# _pytest prefix.
|
# _pytest prefix.
|
||||||
assert isinstance(
|
assert isinstance(modname, str), (
|
||||||
modname, str
|
"module name as text required, got %r" % modname
|
||||||
), f"module name as text required, got {modname!r}"
|
)
|
||||||
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
|
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -885,7 +885,8 @@ def _get_plugin_specs_as_list(
|
||||||
if isinstance(specs, collections.abc.Sequence):
|
if isinstance(specs, collections.abc.Sequence):
|
||||||
return list(specs)
|
return list(specs)
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}"
|
"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r"
|
||||||
|
% specs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1009,6 +1010,9 @@ class Config:
|
||||||
object.__setattr__(self, "plugins", plugins)
|
object.__setattr__(self, "plugins", plugins)
|
||||||
object.__setattr__(self, "dir", dir)
|
object.__setattr__(self, "dir", dir)
|
||||||
|
|
||||||
|
# Set by cacheprovider plugin.
|
||||||
|
cache: Cache
|
||||||
|
|
||||||
class ArgsSource(enum.Enum):
|
class ArgsSource(enum.Enum):
|
||||||
"""Indicates the source of the test arguments.
|
"""Indicates the source of the test arguments.
|
||||||
|
|
||||||
|
@ -1023,9 +1027,6 @@ class Config:
|
||||||
#: 'testpaths' configuration value.
|
#: 'testpaths' configuration value.
|
||||||
TESTPATHS = enum.auto()
|
TESTPATHS = enum.auto()
|
||||||
|
|
||||||
# Set by cacheprovider plugin.
|
|
||||||
cache: Cache
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pluginmanager: PytestPluginManager,
|
pluginmanager: PytestPluginManager,
|
||||||
|
@ -1164,7 +1165,7 @@ class Config:
|
||||||
option: argparse.Namespace | None = None,
|
option: argparse.Namespace | None = 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(
|
||||||
|
@ -1173,7 +1174,7 @@ class Config:
|
||||||
res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
|
res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
|
||||||
if not any(res):
|
if not any(res):
|
||||||
for line in str(excrepr).split("\n"):
|
for line in str(excrepr).split("\n"):
|
||||||
sys.stderr.write(f"INTERNALERROR> {line}\n")
|
sys.stderr.write("INTERNALERROR> %s\n" % line)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
def cwd_relative_nodeid(self, nodeid: str) -> str:
|
def cwd_relative_nodeid(self, nodeid: str) -> str:
|
||||||
|
@ -1280,8 +1281,7 @@ class Config:
|
||||||
self.pluginmanager.rewrite_hook = hook
|
self.pluginmanager.rewrite_hook = hook
|
||||||
|
|
||||||
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||||
# We don't autoload from distribution package entry points,
|
# We don't autoload from setuptools entry points, no need to continue.
|
||||||
# no need to continue.
|
|
||||||
return
|
return
|
||||||
|
|
||||||
package_files = (
|
package_files = (
|
||||||
|
@ -1372,8 +1372,8 @@ class Config:
|
||||||
self._consider_importhook(args)
|
self._consider_importhook(args)
|
||||||
self.pluginmanager.consider_preparse(args, exclude_only=False)
|
self.pluginmanager.consider_preparse(args, exclude_only=False)
|
||||||
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||||
# Don't autoload from distribution package entry point. Only
|
# Don't autoload from setuptools entry point. Only explicitly specified
|
||||||
# explicitly specified plugins are going to be loaded.
|
# plugins are going to be loaded.
|
||||||
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
||||||
self.pluginmanager.consider_env()
|
self.pluginmanager.consider_env()
|
||||||
|
|
||||||
|
@ -1424,7 +1424,7 @@ class Config:
|
||||||
|
|
||||||
if not isinstance(minver, str):
|
if not isinstance(minver, str):
|
||||||
raise pytest.UsageError(
|
raise pytest.UsageError(
|
||||||
f"{self.inipath}: 'minversion' must be a single value"
|
"%s: 'minversion' must be a single value" % self.inipath
|
||||||
)
|
)
|
||||||
|
|
||||||
if Version(minver) > Version(pytest.__version__):
|
if Version(minver) > Version(pytest.__version__):
|
||||||
|
@ -1728,7 +1728,6 @@ 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
|
||||||
|
|
|
@ -310,23 +310,23 @@ class Argument:
|
||||||
for opt in opts:
|
for opt in opts:
|
||||||
if len(opt) < 2:
|
if len(opt) < 2:
|
||||||
raise ArgumentError(
|
raise ArgumentError(
|
||||||
f"invalid option string {opt!r}: "
|
"invalid option string %r: "
|
||||||
"must be at least two characters long",
|
"must be at least two characters long" % opt,
|
||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
elif len(opt) == 2:
|
elif len(opt) == 2:
|
||||||
if not (opt[0] == "-" and opt[1] != "-"):
|
if not (opt[0] == "-" and opt[1] != "-"):
|
||||||
raise ArgumentError(
|
raise ArgumentError(
|
||||||
f"invalid short option string {opt!r}: "
|
"invalid short option string %r: "
|
||||||
"must be of the form -x, (x any non-dash char)",
|
"must be of the form -x, (x any non-dash char)" % opt,
|
||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
self._short_opts.append(opt)
|
self._short_opts.append(opt)
|
||||||
else:
|
else:
|
||||||
if not (opt[0:2] == "--" and opt[2] != "-"):
|
if not (opt[0:2] == "--" and opt[2] != "-"):
|
||||||
raise ArgumentError(
|
raise ArgumentError(
|
||||||
f"invalid long option string {opt!r}: "
|
"invalid long option string %r: "
|
||||||
"must start with --, followed by non-dash",
|
"must start with --, followed by non-dash" % opt,
|
||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
self._long_opts.append(opt)
|
self._long_opts.append(opt)
|
||||||
|
@ -380,7 +380,7 @@ class OptionGroup:
|
||||||
name for opt in self.options for name in opt.names()
|
name for opt in self.options for name in opt.names()
|
||||||
)
|
)
|
||||||
if conflict:
|
if conflict:
|
||||||
raise ValueError(f"option names {conflict} already added")
|
raise ValueError("option names %s already added" % conflict)
|
||||||
option = Argument(*opts, **attrs)
|
option = Argument(*opts, **attrs)
|
||||||
self._addoption_instance(option, shortupper=False)
|
self._addoption_instance(option, shortupper=False)
|
||||||
|
|
||||||
|
@ -438,9 +438,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
if unrecognized:
|
if unrecognized:
|
||||||
for arg in unrecognized:
|
for arg in unrecognized:
|
||||||
if arg and arg[0] == "-":
|
if arg and arg[0] == "-":
|
||||||
lines = [
|
lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
|
||||||
"unrecognized arguments: {}".format(" ".join(unrecognized))
|
|
||||||
]
|
|
||||||
for k, v in sorted(self.extra_info.items()):
|
for k, v in sorted(self.extra_info.items()):
|
||||||
lines.append(f" {k}: {v}")
|
lines.append(f" {k}: {v}")
|
||||||
self.error("\n".join(lines))
|
self.error("\n".join(lines))
|
||||||
|
@ -519,7 +517,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||||
continue
|
continue
|
||||||
if not option.startswith("--"):
|
if not option.startswith("--"):
|
||||||
raise ArgumentError(
|
raise ArgumentError(
|
||||||
f'long optional argument without "--": [{option}]', option
|
'long optional argument without "--": [%s]' % (option), option
|
||||||
)
|
)
|
||||||
xxoption = option[2:]
|
xxoption = option[2:]
|
||||||
shortened = xxoption.replace("-", "")
|
shortened = xxoption.replace("-", "")
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
# ruff: noqa: T100
|
|
||||||
"""Interactive debugging with PDB, the Python Debugger."""
|
"""Interactive debugging with PDB, the Python Debugger."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
@ -11,11 +10,11 @@ import types
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
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
|
||||||
|
@ -24,7 +23,11 @@ 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]:
|
||||||
|
@ -174,7 +177,8 @@ class pytestPDB:
|
||||||
else:
|
else:
|
||||||
tw.sep(
|
tw.sep(
|
||||||
">",
|
">",
|
||||||
f"PDB continue (IO-capturing resumed for {capturing})",
|
"PDB continue (IO-capturing resumed for %s)"
|
||||||
|
% capturing,
|
||||||
)
|
)
|
||||||
assert capman is not None
|
assert capman is not None
|
||||||
capman.resume()
|
capman.resume()
|
||||||
|
@ -303,7 +307,7 @@ class PdbTrace:
|
||||||
return (yield)
|
return (yield)
|
||||||
|
|
||||||
|
|
||||||
def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
|
def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||||
"""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
|
||||||
|
@ -315,14 +319,14 @@ def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
|
||||||
# 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) -> None:
|
def wrapper(*args, **kwargs):
|
||||||
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) -> None:
|
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
|
||||||
"""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"):
|
||||||
|
|
|
@ -370,7 +370,7 @@ class DoctestItem(Item):
|
||||||
).split("\n")
|
).split("\n")
|
||||||
else:
|
else:
|
||||||
inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
|
inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
|
||||||
lines += [f"UNEXPECTED EXCEPTION: {inner_excinfo.value!r}"]
|
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
|
||||||
lines += [
|
lines += [
|
||||||
x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
|
x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
|
||||||
]
|
]
|
||||||
|
@ -378,7 +378,7 @@ class DoctestItem(Item):
|
||||||
return ReprFailDoctest(reprlocation_lines)
|
return ReprFailDoctest(reprlocation_lines)
|
||||||
|
|
||||||
def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
|
def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
|
||||||
return self.path, self.dtest.lineno, f"[doctest] {self.name}"
|
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
|
||||||
|
|
||||||
|
|
||||||
def _get_flag_lookup() -> dict[str, int]:
|
def _get_flag_lookup() -> dict[str, int]:
|
||||||
|
@ -501,52 +501,43 @@ class DoctestModule(Module):
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
class MockAwareDocTestFinder(doctest.DocTestFinder):
|
class MockAwareDocTestFinder(doctest.DocTestFinder):
|
||||||
py_ver_info_minor = sys.version_info[:2]
|
"""A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
|
||||||
is_find_lineno_broken = (
|
|
||||||
py_ver_info_minor < (3, 11)
|
|
||||||
or (py_ver_info_minor == (3, 11) and sys.version_info.micro < 9)
|
|
||||||
or (py_ver_info_minor == (3, 12) and sys.version_info.micro < 3)
|
|
||||||
)
|
|
||||||
if is_find_lineno_broken:
|
|
||||||
|
|
||||||
def _find_lineno(self, obj, source_lines):
|
https://github.com/pytest-dev/pytest/issues/3456
|
||||||
"""On older Pythons, doctest code does not take into account
|
https://bugs.python.org/issue25532
|
||||||
`@property`. https://github.com/python/cpython/issues/61648
|
"""
|
||||||
|
|
||||||
Moreover, wrapped Doctests need to be unwrapped so the correct
|
def _find_lineno(self, obj, source_lines):
|
||||||
line number is returned. #8796
|
"""Doctest code does not take into account `@property`, this
|
||||||
"""
|
is a hackish way to fix it. https://bugs.python.org/issue17446
|
||||||
if isinstance(obj, property):
|
|
||||||
obj = getattr(obj, "fget", obj)
|
|
||||||
|
|
||||||
if hasattr(obj, "__wrapped__"):
|
Wrapped Doctests will need to be unwrapped so the correct
|
||||||
# Get the main obj in case of it being wrapped
|
line number is returned. This will be reported upstream. #8796
|
||||||
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.
|
||||||
return super()._find_lineno( # type:ignore[misc]
|
super()._find( # type:ignore[misc]
|
||||||
obj,
|
tests, obj, name, module, source_lines, globs, seen
|
||||||
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):
|
||||||
|
@ -561,11 +552,14 @@ 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:
|
||||||
if self.config.getvalue("doctest_ignore_import_errors"):
|
if self.config.getvalue("doctest_ignore_import_errors"):
|
||||||
skip(f"unable to import module {self.path!r}")
|
skip("unable to import module %r" % self.path)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
|
@ -10,19 +10,16 @@ import inspect
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import types
|
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
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
|
||||||
|
@ -60,7 +57,6 @@ 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
|
||||||
|
@ -79,6 +75,7 @@ if sys.version_info < (3, 11):
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
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
|
||||||
|
@ -105,8 +102,8 @@ _FixtureCachedResult = Union[
|
||||||
None,
|
None,
|
||||||
# Cache key.
|
# Cache key.
|
||||||
object,
|
object,
|
||||||
# The exception and the original traceback.
|
# Exception if raised.
|
||||||
Tuple[BaseException, Optional[types.TracebackType]],
|
BaseException,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -161,12 +158,6 @@ def getfixturemarker(obj: object) -> FixtureFunctionMarker | None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -175,95 +166,100 @@ class FixtureArgKey:
|
||||||
item_cls: type | None
|
item_cls: type | None
|
||||||
|
|
||||||
|
|
||||||
_V = TypeVar("_V")
|
def get_parametrized_fixture_keys(
|
||||||
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_by_item: dict[Scope, dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
|
argkeys_cache: dict[Scope, dict[nodes.Item, dict[FixtureArgKey, None]]] = {}
|
||||||
items_by_argkey: dict[
|
items_by_argkey: dict[
|
||||||
Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
|
Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
|
||||||
] = {}
|
] = {}
|
||||||
for scope in HIGH_SCOPES:
|
for scope in HIGH_SCOPES:
|
||||||
scoped_argkeys_by_item = argkeys_by_item[scope] = {}
|
scoped_argkeys_cache = argkeys_cache[scope] = {}
|
||||||
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
|
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
|
||||||
for item in items:
|
for item in items:
|
||||||
argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope))
|
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
||||||
if argkeys:
|
if keys:
|
||||||
scoped_argkeys_by_item[item] = argkeys
|
scoped_argkeys_cache[item] = keys
|
||||||
for argkey in argkeys:
|
for key in keys:
|
||||||
scoped_items_by_argkey[argkey][item] = None
|
scoped_items_by_argkey[key][item] = None
|
||||||
|
items_dict = dict.fromkeys(items, None)
|
||||||
items_set = dict.fromkeys(items)
|
|
||||||
return list(
|
return list(
|
||||||
reorder_items_atscope(
|
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
||||||
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, OrderedDict[nodes.Item, None]]],
|
||||||
|
) -> None:
|
||||||
|
for scope in HIGH_SCOPES:
|
||||||
|
scoped_items_by_argkey = items_by_argkey[scope]
|
||||||
|
for key in argkeys_cache[scope].get(item, []):
|
||||||
|
scoped_items_by_argkey[key][item] = None
|
||||||
|
scoped_items_by_argkey[key].move_to_end(item, last=False)
|
||||||
|
|
||||||
|
|
||||||
def reorder_items_atscope(
|
def reorder_items_atscope(
|
||||||
items: OrderedSet[nodes.Item],
|
items: dict[nodes.Item, None],
|
||||||
argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]],
|
argkeys_cache: dict[Scope, dict[nodes.Item, dict[FixtureArgKey, None]]],
|
||||||
items_by_argkey: Mapping[
|
items_by_argkey: dict[Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
|
||||||
Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]]
|
|
||||||
],
|
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
) -> OrderedSet[nodes.Item]:
|
) -> dict[nodes.Item, None]:
|
||||||
if scope is Scope.Function or len(items) < 3:
|
if scope is Scope.Function or len(items) < 3:
|
||||||
return items
|
return items
|
||||||
|
ignore: set[FixtureArgKey | None] = set()
|
||||||
scoped_items_by_argkey = items_by_argkey[scope]
|
|
||||||
scoped_argkeys_by_item = argkeys_by_item[scope]
|
|
||||||
|
|
||||||
ignore: set[FixtureArgKey] = set()
|
|
||||||
items_deque = deque(items)
|
items_deque = deque(items)
|
||||||
items_done: OrderedSet[nodes.Item] = {}
|
items_done: dict[nodes.Item, None] = {}
|
||||||
|
scoped_items_by_argkey = items_by_argkey[scope]
|
||||||
|
scoped_argkeys_cache = argkeys_cache[scope]
|
||||||
while items_deque:
|
while items_deque:
|
||||||
no_argkey_items: OrderedSet[nodes.Item] = {}
|
no_argkey_group: dict[nodes.Item, None] = {}
|
||||||
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_items:
|
if item in items_done or item in no_argkey_group:
|
||||||
continue
|
continue
|
||||||
argkeys = dict.fromkeys(
|
argkeys = dict.fromkeys(
|
||||||
k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore
|
(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None
|
||||||
)
|
)
|
||||||
if not argkeys:
|
if not argkeys:
|
||||||
no_argkey_items[item] = None
|
no_argkey_group[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
|
||||||
|
@ -272,23 +268,16 @@ 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_items:
|
if no_argkey_group:
|
||||||
reordered_no_argkey_items = reorder_items_atscope(
|
no_argkey_group = reorder_items_atscope(
|
||||||
no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower()
|
no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower()
|
||||||
)
|
)
|
||||||
items_done.update(reordered_no_argkey_items)
|
for item in no_argkey_group:
|
||||||
if slicing_argkey is not None:
|
items_done[item] = None
|
||||||
ignore.add(slicing_argkey)
|
ignore.add(slicing_argkey)
|
||||||
return items_done
|
return items_done
|
||||||
|
|
||||||
|
|
||||||
|
@ -687,7 +676,7 @@ class TopRequest(FixtureRequest):
|
||||||
return self._pyfuncitem
|
return self._pyfuncitem
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<FixtureRequest for {self.node!r}>"
|
return "<FixtureRequest for %r>" % (self.node)
|
||||||
|
|
||||||
def _fillfixtures(self) -> None:
|
def _fillfixtures(self) -> None:
|
||||||
item = self._pyfuncitem
|
item = self._pyfuncitem
|
||||||
|
@ -1060,8 +1049,8 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
# numpy arrays (#6497).
|
# numpy arrays (#6497).
|
||||||
if my_cache_key is cache_key:
|
if my_cache_key is cache_key:
|
||||||
if self.cached_result[2] is not None:
|
if self.cached_result[2] is not None:
|
||||||
exc, exc_tb = self.cached_result[2]
|
exc = self.cached_result[2]
|
||||||
raise exc.with_traceback(exc_tb)
|
raise exc
|
||||||
else:
|
else:
|
||||||
result = self.cached_result[0]
|
result = self.cached_result[0]
|
||||||
return result
|
return result
|
||||||
|
@ -1137,7 +1126,7 @@ def pytest_fixture_setup(
|
||||||
# Don't show the fixture as the skip location, as then the user
|
# Don't show the fixture as the skip location, as then the user
|
||||||
# wouldn't know which test skipped.
|
# wouldn't know which test skipped.
|
||||||
e._use_item_location = True
|
e._use_item_location = True
|
||||||
fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__))
|
fixturedef.cached_result = (None, my_cache_key, e)
|
||||||
raise
|
raise
|
||||||
fixturedef.cached_result = (result, my_cache_key, None)
|
fixturedef.cached_result = (result, my_cache_key, None)
|
||||||
return result
|
return result
|
||||||
|
@ -1625,7 +1614,10 @@ class FixtureManager:
|
||||||
name: str,
|
name: str,
|
||||||
func: _FixtureFunc[object],
|
func: _FixtureFunc[object],
|
||||||
nodeid: str | None,
|
nodeid: str | None,
|
||||||
scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function",
|
scope: Scope
|
||||||
|
| _ScopeName
|
||||||
|
| Callable[[str, Config], _ScopeName]
|
||||||
|
| None = "function",
|
||||||
params: Sequence[object] | None = None,
|
params: Sequence[object] | None = None,
|
||||||
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
|
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
|
||||||
autouse: bool = False,
|
autouse: bool = False,
|
||||||
|
@ -1818,10 +1810,7 @@ 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,
|
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
|
||||||
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)
|
||||||
|
@ -1898,14 +1887,12 @@ def _showfixtures_main(config: Config, session: Session) -> None:
|
||||||
continue
|
continue
|
||||||
tw.write(f"{argname}", green=True)
|
tw.write(f"{argname}", green=True)
|
||||||
if fixturedef.scope != "function":
|
if fixturedef.scope != "function":
|
||||||
tw.write(f" [{fixturedef.scope} scope]", cyan=True)
|
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
|
||||||
tw.write(f" -- {prettypath}", yellow=True)
|
tw.write(f" -- {prettypath}", yellow=True)
|
||||||
tw.write("\n")
|
tw.write("\n")
|
||||||
doc = inspect.getdoc(fixturedef.func)
|
doc = inspect.getdoc(fixturedef.func)
|
||||||
if doc:
|
if doc:
|
||||||
write_docstring(
|
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
|
||||||
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()
|
||||||
|
|
|
@ -120,11 +120,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
|
||||||
)
|
)
|
||||||
config.trace.root.setwriter(debugfile.write)
|
config.trace.root.setwriter(debugfile.write)
|
||||||
undo_tracing = config.pluginmanager.enable_tracing()
|
undo_tracing = config.pluginmanager.enable_tracing()
|
||||||
sys.stderr.write(f"writing pytest debug information to {path}\n")
|
sys.stderr.write("writing pytest debug information to %s\n" % path)
|
||||||
|
|
||||||
def unset_tracing() -> None:
|
def unset_tracing() -> None:
|
||||||
debugfile.close()
|
debugfile.close()
|
||||||
sys.stderr.write(f"wrote pytest debug information to {debugfile.name}\n")
|
sys.stderr.write("wrote pytest debug information to %s\n" % debugfile.name)
|
||||||
config.trace.root.setwriter(None)
|
config.trace.root.setwriter(None)
|
||||||
undo_tracing()
|
undo_tracing()
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ def showhelp(config: Config) -> None:
|
||||||
if help is None:
|
if help is None:
|
||||||
raise TypeError(f"help argument cannot be None for {name}")
|
raise TypeError(f"help argument cannot be None for {name}")
|
||||||
spec = f"{name} ({type}):"
|
spec = f"{name} ({type}):"
|
||||||
tw.write(f" {spec}")
|
tw.write(" %s" % spec)
|
||||||
spec_len = len(spec)
|
spec_len = len(spec)
|
||||||
if spec_len > (indent_len - 3):
|
if spec_len > (indent_len - 3):
|
||||||
# Display help starting at a new line.
|
# Display help starting at a new line.
|
||||||
|
@ -242,7 +242,7 @@ def getpluginversioninfo(config: Config) -> list[str]:
|
||||||
lines = []
|
lines = []
|
||||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||||
if plugininfo:
|
if plugininfo:
|
||||||
lines.append("registered third-party plugins:")
|
lines.append("setuptools registered plugins:")
|
||||||
for plugin, dist in plugininfo:
|
for plugin, dist in plugininfo:
|
||||||
loc = getattr(plugin, "__file__", repr(plugin))
|
loc = getattr(plugin, "__file__", repr(plugin))
|
||||||
content = f"{dist.project_name}-{dist.version} at {loc}"
|
content = f"{dist.project_name}-{dist.version} at {loc}"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
# ruff: noqa: T100
|
|
||||||
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
||||||
and by builtin plugins."""
|
and by builtin plugins."""
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jen
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
|
||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
@ -51,9 +50,9 @@ def bin_xml_escape(arg: object) -> str:
|
||||||
def repl(matchobj: Match[str]) -> str:
|
def repl(matchobj: Match[str]) -> str:
|
||||||
i = ord(matchobj.group())
|
i = ord(matchobj.group())
|
||||||
if i <= 0xFF:
|
if i <= 0xFF:
|
||||||
return f"#x{i:02X}"
|
return "#x%02X" % i
|
||||||
else:
|
else:
|
||||||
return f"#x{i:04X}"
|
return "#x%04X" % i
|
||||||
|
|
||||||
# The spec range of valid chars is:
|
# The spec range of valid chars is:
|
||||||
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
||||||
|
@ -147,7 +146,7 @@ class _NodeReporter:
|
||||||
self.attrs = temp_attrs
|
self.attrs = temp_attrs
|
||||||
|
|
||||||
def to_xml(self) -> ET.Element:
|
def to_xml(self) -> ET.Element:
|
||||||
testcase = ET.Element("testcase", self.attrs, time=f"{self.duration:.3f}")
|
testcase = ET.Element("testcase", self.attrs, time="%.3f" % self.duration)
|
||||||
properties = self.make_properties_node()
|
properties = self.make_properties_node()
|
||||||
if properties is not None:
|
if properties is not None:
|
||||||
testcase.append(properties)
|
testcase.append(properties)
|
||||||
|
@ -664,10 +663,8 @@ class LogXML:
|
||||||
failures=str(self.stats["failure"]),
|
failures=str(self.stats["failure"]),
|
||||||
skipped=str(self.stats["skipped"]),
|
skipped=str(self.stats["skipped"]),
|
||||||
tests=str(numtests),
|
tests=str(numtests),
|
||||||
time=f"{suite_time_delta:.3f}",
|
time="%.3f" % suite_time_delta,
|
||||||
timestamp=datetime.fromtimestamp(self.suite_start_time, timezone.utc)
|
timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(),
|
||||||
.astimezone()
|
|
||||||
.isoformat(),
|
|
||||||
hostname=platform.node(),
|
hostname=platform.node(),
|
||||||
)
|
)
|
||||||
global_properties = self._get_global_properties_node()
|
global_properties = self._get_global_properties_node()
|
||||||
|
|
|
@ -399,7 +399,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 # pylint: disable=misplaced-bare-raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
|
|
@ -35,6 +35,7 @@ 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
|
||||||
|
@ -51,8 +52,6 @@ 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(
|
||||||
|
@ -369,12 +368,22 @@ def pytest_runtestloop(session: Session) -> bool:
|
||||||
|
|
||||||
def _in_venv(path: Path) -> bool:
|
def _in_venv(path: Path) -> bool:
|
||||||
"""Attempt to detect if ``path`` is the root of a Virtual Environment by
|
"""Attempt to detect if ``path`` is the root of a Virtual Environment by
|
||||||
checking for the existence of the pyvenv.cfg file.
|
checking for the existence of the appropriate activate script."""
|
||||||
[https://peps.python.org/pep-0405/]"""
|
bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin")
|
||||||
try:
|
try:
|
||||||
return path.joinpath("pyvenv.cfg").is_file()
|
if not bindir.is_dir():
|
||||||
|
return False
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
|
activates = (
|
||||||
|
"activate",
|
||||||
|
"activate.csh",
|
||||||
|
"activate.fish",
|
||||||
|
"Activate",
|
||||||
|
"Activate.bat",
|
||||||
|
"Activate.ps1",
|
||||||
|
)
|
||||||
|
return any(fname.name in activates for fname in bindir.iterdir())
|
||||||
|
|
||||||
|
|
||||||
def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None:
|
def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None:
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Collection
|
from typing import Collection
|
||||||
from typing import Iterable
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -23,7 +21,6 @@ from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config import UsageError
|
from _pytest.config import UsageError
|
||||||
from _pytest.config.argparsing import NOT_SET
|
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
@ -125,7 +122,7 @@ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
|
||||||
parts = line.split(":", 1)
|
parts = line.split(":", 1)
|
||||||
name = parts[0]
|
name = parts[0]
|
||||||
rest = parts[1] if len(parts) == 2 else ""
|
rest = parts[1] if len(parts) == 2 else ""
|
||||||
tw.write(f"@pytest.mark.{name}:", bold=True)
|
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||||
tw.line(rest)
|
tw.line(rest)
|
||||||
tw.line()
|
tw.line()
|
||||||
config._ensure_unconfigure()
|
config._ensure_unconfigure()
|
||||||
|
@ -184,9 +181,7 @@ class KeywordMatcher:
|
||||||
|
|
||||||
return cls(mapped_names)
|
return cls(mapped_names)
|
||||||
|
|
||||||
def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool:
|
def __call__(self, subname: str) -> bool:
|
||||||
if kwargs:
|
|
||||||
raise UsageError("Keyword expressions do not support call parameters.")
|
|
||||||
subname = subname.lower()
|
subname = subname.lower()
|
||||||
names = (name.lower() for name in self._names)
|
names = (name.lower() for name in self._names)
|
||||||
|
|
||||||
|
@ -223,26 +218,17 @@ class MarkMatcher:
|
||||||
Tries to match on any marker names, attached to the given colitem.
|
Tries to match on any marker names, attached to the given colitem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("own_mark_name_mapping",)
|
__slots__ = ("own_mark_names",)
|
||||||
|
|
||||||
own_mark_name_mapping: dict[str, list[Mark]]
|
own_mark_names: AbstractSet[str]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher:
|
def from_item(cls, item: Item) -> MarkMatcher:
|
||||||
mark_name_mapping = collections.defaultdict(list)
|
mark_names = {mark.name for mark in item.iter_markers()}
|
||||||
for mark in markers:
|
return cls(mark_names)
|
||||||
mark_name_mapping[mark.name].append(mark)
|
|
||||||
return cls(mark_name_mapping)
|
|
||||||
|
|
||||||
def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool:
|
def __call__(self, name: str) -> bool:
|
||||||
if not (matches := self.own_mark_name_mapping.get(name, [])):
|
return name in self.own_mark_names
|
||||||
return False
|
|
||||||
|
|
||||||
for mark in matches:
|
|
||||||
if all(mark.kwargs.get(k, NOT_SET) == v for k, v in kwargs.items()):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def deselect_by_mark(items: list[Item], config: Config) -> None:
|
def deselect_by_mark(items: list[Item], config: Config) -> None:
|
||||||
|
@ -254,7 +240,7 @@ def deselect_by_mark(items: list[Item], config: Config) -> None:
|
||||||
remaining: list[Item] = []
|
remaining: list[Item] = []
|
||||||
deselected: list[Item] = []
|
deselected: list[Item] = []
|
||||||
for item in items:
|
for item in items:
|
||||||
if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())):
|
if expr.evaluate(MarkMatcher.from_item(item)):
|
||||||
remaining.append(item)
|
remaining.append(item)
|
||||||
else:
|
else:
|
||||||
deselected.append(item)
|
deselected.append(item)
|
||||||
|
|
|
@ -5,19 +5,14 @@ The grammar is:
|
||||||
expression: expr? EOF
|
expression: expr? EOF
|
||||||
expr: and_expr ('or' and_expr)*
|
expr: and_expr ('or' and_expr)*
|
||||||
and_expr: not_expr ('and' not_expr)*
|
and_expr: not_expr ('and' not_expr)*
|
||||||
not_expr: 'not' not_expr | '(' expr ')' | ident kwargs?
|
not_expr: 'not' not_expr | '(' expr ')' | ident
|
||||||
|
|
||||||
ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
|
ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
|
||||||
kwargs: ('(' name '=' value ( ', ' name '=' value )* ')')
|
|
||||||
name: a valid ident, but not a reserved keyword
|
|
||||||
value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None'
|
|
||||||
|
|
||||||
The semantics are:
|
The semantics are:
|
||||||
|
|
||||||
- Empty expression evaluates to False.
|
- Empty expression evaluates to False.
|
||||||
- ident evaluates to True or False according to a provided matcher function.
|
- ident evaluates to True of False according to a provided matcher function.
|
||||||
- or/and/not evaluate according to the usual boolean semantics.
|
- or/and/not evaluate according to the usual boolean semantics.
|
||||||
- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
@ -25,15 +20,12 @@ from __future__ import annotations
|
||||||
import ast
|
import ast
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import keyword
|
|
||||||
import re
|
import re
|
||||||
import types
|
import types
|
||||||
|
from typing import Callable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import Literal
|
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import overload
|
|
||||||
from typing import Protocol
|
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,9 +43,6 @@ class TokenType(enum.Enum):
|
||||||
NOT = "not"
|
NOT = "not"
|
||||||
IDENT = "identifier"
|
IDENT = "identifier"
|
||||||
EOF = "end of input"
|
EOF = "end of input"
|
||||||
EQUAL = "="
|
|
||||||
STRING = "string literal"
|
|
||||||
COMMA = ","
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
@ -97,27 +86,6 @@ class Scanner:
|
||||||
elif input[pos] == ")":
|
elif input[pos] == ")":
|
||||||
yield Token(TokenType.RPAREN, ")", pos)
|
yield Token(TokenType.RPAREN, ")", pos)
|
||||||
pos += 1
|
pos += 1
|
||||||
elif input[pos] == "=":
|
|
||||||
yield Token(TokenType.EQUAL, "=", pos)
|
|
||||||
pos += 1
|
|
||||||
elif input[pos] == ",":
|
|
||||||
yield Token(TokenType.COMMA, ",", pos)
|
|
||||||
pos += 1
|
|
||||||
elif (quote_char := input[pos]) in ("'", '"'):
|
|
||||||
end_quote_pos = input.find(quote_char, pos + 1)
|
|
||||||
if end_quote_pos == -1:
|
|
||||||
raise ParseError(
|
|
||||||
pos + 1,
|
|
||||||
f'closing quote "{quote_char}" is missing',
|
|
||||||
)
|
|
||||||
value = input[pos : end_quote_pos + 1]
|
|
||||||
if (backslash_pos := input.find("\\")) != -1:
|
|
||||||
raise ParseError(
|
|
||||||
backslash_pos + 1,
|
|
||||||
r'escaping with "\" not supported in marker expression',
|
|
||||||
)
|
|
||||||
yield Token(TokenType.STRING, value, pos)
|
|
||||||
pos += len(value)
|
|
||||||
else:
|
else:
|
||||||
match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
|
match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
|
||||||
if match:
|
if match:
|
||||||
|
@ -138,14 +106,6 @@ class Scanner:
|
||||||
)
|
)
|
||||||
yield Token(TokenType.EOF, "", pos)
|
yield Token(TokenType.EOF, "", pos)
|
||||||
|
|
||||||
@overload
|
|
||||||
def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def accept(
|
|
||||||
self, type: TokenType, *, reject: Literal[False] = False
|
|
||||||
) -> Token | None: ...
|
|
||||||
|
|
||||||
def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
|
def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
|
||||||
if self.current.type is type:
|
if self.current.type is type:
|
||||||
token = self.current
|
token = self.current
|
||||||
|
@ -206,87 +166,18 @@ def not_expr(s: Scanner) -> ast.expr:
|
||||||
return ret
|
return ret
|
||||||
ident = s.accept(TokenType.IDENT)
|
ident = s.accept(TokenType.IDENT)
|
||||||
if ident:
|
if ident:
|
||||||
name = ast.Name(IDENT_PREFIX + ident.value, ast.Load())
|
return ast.Name(IDENT_PREFIX + ident.value, ast.Load())
|
||||||
if s.accept(TokenType.LPAREN):
|
|
||||||
ret = ast.Call(func=name, args=[], keywords=all_kwargs(s))
|
|
||||||
s.accept(TokenType.RPAREN, reject=True)
|
|
||||||
else:
|
|
||||||
ret = name
|
|
||||||
return ret
|
|
||||||
|
|
||||||
s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
|
s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
|
||||||
|
|
||||||
|
|
||||||
BUILTIN_MATCHERS = {"True": True, "False": False, "None": None}
|
class MatcherAdapter(Mapping[str, bool]):
|
||||||
|
|
||||||
|
|
||||||
def single_kwarg(s: Scanner) -> ast.keyword:
|
|
||||||
keyword_name = s.accept(TokenType.IDENT, reject=True)
|
|
||||||
if not keyword_name.value.isidentifier():
|
|
||||||
raise ParseError(
|
|
||||||
keyword_name.pos + 1,
|
|
||||||
f"not a valid python identifier {keyword_name.value}",
|
|
||||||
)
|
|
||||||
if keyword.iskeyword(keyword_name.value):
|
|
||||||
raise ParseError(
|
|
||||||
keyword_name.pos + 1,
|
|
||||||
f"unexpected reserved python keyword `{keyword_name.value}`",
|
|
||||||
)
|
|
||||||
s.accept(TokenType.EQUAL, reject=True)
|
|
||||||
|
|
||||||
if value_token := s.accept(TokenType.STRING):
|
|
||||||
value: str | int | bool | None = value_token.value[1:-1] # strip quotes
|
|
||||||
else:
|
|
||||||
value_token = s.accept(TokenType.IDENT, reject=True)
|
|
||||||
if (
|
|
||||||
(number := value_token.value).isdigit()
|
|
||||||
or number.startswith("-")
|
|
||||||
and number[1:].isdigit()
|
|
||||||
):
|
|
||||||
value = int(number)
|
|
||||||
elif value_token.value in BUILTIN_MATCHERS:
|
|
||||||
value = BUILTIN_MATCHERS[value_token.value]
|
|
||||||
else:
|
|
||||||
raise ParseError(
|
|
||||||
value_token.pos + 1,
|
|
||||||
f'unexpected character/s "{value_token.value}"',
|
|
||||||
)
|
|
||||||
|
|
||||||
ret = ast.keyword(keyword_name.value, ast.Constant(value))
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def all_kwargs(s: Scanner) -> list[ast.keyword]:
|
|
||||||
ret = [single_kwarg(s)]
|
|
||||||
while s.accept(TokenType.COMMA):
|
|
||||||
ret.append(single_kwarg(s))
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class MatcherCall(Protocol):
|
|
||||||
def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ...
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class MatcherNameAdapter:
|
|
||||||
matcher: MatcherCall
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
|
||||||
return self.matcher(self.name)
|
|
||||||
|
|
||||||
def __call__(self, **kwargs: str | int | bool | None) -> bool:
|
|
||||||
return self.matcher(self.name, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class MatcherAdapter(Mapping[str, MatcherNameAdapter]):
|
|
||||||
"""Adapts a matcher function to a locals mapping as required by eval()."""
|
"""Adapts a matcher function to a locals mapping as required by eval()."""
|
||||||
|
|
||||||
def __init__(self, matcher: MatcherCall) -> None:
|
def __init__(self, matcher: Callable[[str], bool]) -> None:
|
||||||
self.matcher = matcher
|
self.matcher = matcher
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> MatcherNameAdapter:
|
def __getitem__(self, key: str) -> bool:
|
||||||
return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :])
|
return self.matcher(key[len(IDENT_PREFIX) :])
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
def __iter__(self) -> Iterator[str]:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -320,7 +211,7 @@ class Expression:
|
||||||
)
|
)
|
||||||
return Expression(code)
|
return Expression(code)
|
||||||
|
|
||||||
def evaluate(self, matcher: MatcherCall) -> bool:
|
def evaluate(self, matcher: Callable[[str], bool]) -> bool:
|
||||||
"""Evaluate the match expression.
|
"""Evaluate the match expression.
|
||||||
|
|
||||||
:param matcher:
|
:param matcher:
|
||||||
|
@ -329,5 +220,5 @@ class Expression:
|
||||||
|
|
||||||
:returns: Whether the expression matches or not.
|
:returns: Whether the expression matches or not.
|
||||||
"""
|
"""
|
||||||
ret: bool = bool(eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher)))
|
ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher))
|
||||||
return ret
|
return ret
|
||||||
|
|
|
@ -28,7 +28,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -428,6 +427,7 @@ 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]
|
||||||
|
@ -544,9 +544,9 @@ class MarkGenerator:
|
||||||
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
|
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
|
||||||
|
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f"Unknown pytest.mark.{name} - is this a typo? You can register "
|
"Unknown pytest.mark.%s - is this a typo? You can register "
|
||||||
"custom marks to avoid this warning - for details, see "
|
"custom marks to avoid this warning - for details, see "
|
||||||
"https://docs.pytest.org/en/stable/how-to/mark.html",
|
"https://docs.pytest.org/en/stable/how-to/mark.html" % name,
|
||||||
PytestUnknownMarkWarning,
|
PytestUnknownMarkWarning,
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|
|
@ -140,7 +140,6 @@ 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
|
||||||
|
|
|
@ -26,7 +26,6 @@ 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
|
||||||
|
@ -46,6 +45,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -408,7 +408,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
def _repr_failure_py(
|
def _repr_failure_py(
|
||||||
self,
|
self,
|
||||||
excinfo: ExceptionInfo[BaseException],
|
excinfo: ExceptionInfo[BaseException],
|
||||||
style: TracebackStyle | None = None,
|
style: _TracebackStyle | None = None,
|
||||||
) -> TerminalRepr:
|
) -> TerminalRepr:
|
||||||
from _pytest.fixtures import FixtureLookupError
|
from _pytest.fixtures import FixtureLookupError
|
||||||
|
|
||||||
|
@ -440,8 +440,6 @@ 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.
|
||||||
|
@ -460,13 +458,12 @@ 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: TracebackStyle | None = None,
|
style: _TracebackStyle | None = None,
|
||||||
) -> str | TerminalRepr:
|
) -> str | TerminalRepr:
|
||||||
"""Return a representation of a collection or test failure.
|
"""Return a representation of a collection or test failure.
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ def pytest_unconfigure(config: Config) -> None:
|
||||||
# Write summary.
|
# Write summary.
|
||||||
tr.write_sep("=", "Sending information to Paste Service")
|
tr.write_sep("=", "Sending information to Paste Service")
|
||||||
pastebinurl = create_new_paste(sessionlog)
|
pastebinurl = create_new_paste(sessionlog)
|
||||||
tr.write_line(f"pastebin session-log: {pastebinurl}\n")
|
tr.write_line("pastebin session-log: %s\n" % pastebinurl)
|
||||||
|
|
||||||
|
|
||||||
def create_new_paste(contents: str | bytes) -> str:
|
def create_new_paste(contents: str | bytes) -> str:
|
||||||
|
@ -86,7 +86,7 @@ def create_new_paste(contents: str | bytes) -> str:
|
||||||
urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
|
urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
|
||||||
)
|
)
|
||||||
except OSError as exc_info: # urllib errors
|
except OSError as exc_info: # urllib errors
|
||||||
return f"bad response: {exc_info}"
|
return "bad response: %s" % exc_info
|
||||||
m = re.search(r'href="/raw/(\w+)"', response)
|
m = re.search(r'href="/raw/(\w+)"', response)
|
||||||
if m:
|
if m:
|
||||||
return f"{url}/show/{m.group(1)}"
|
return f"{url}/show/{m.group(1)}"
|
||||||
|
|
|
@ -178,13 +178,13 @@ class LsofFdLeakChecker:
|
||||||
leaked_files = [t for t in lines2 if t[0] in new_fds]
|
leaked_files = [t for t in lines2 if t[0] in new_fds]
|
||||||
if leaked_files:
|
if leaked_files:
|
||||||
error = [
|
error = [
|
||||||
f"***** {len(leaked_files)} FD leakage detected",
|
"***** %s FD leakage detected" % len(leaked_files),
|
||||||
*(str(f) for f in leaked_files),
|
*(str(f) for f in leaked_files),
|
||||||
"*** Before:",
|
"*** Before:",
|
||||||
*(str(f) for f in lines1),
|
*(str(f) for f in lines1),
|
||||||
"*** After:",
|
"*** After:",
|
||||||
*(str(f) for f in lines2),
|
*(str(f) for f in lines2),
|
||||||
f"***** {len(leaked_files)} FD leakage detected",
|
"***** %s FD leakage detected" % len(leaked_files),
|
||||||
"*** function {}:{}: {} ".format(*item.location),
|
"*** function {}:{}: {} ".format(*item.location),
|
||||||
"See issue #2366",
|
"See issue #2366",
|
||||||
]
|
]
|
||||||
|
@ -310,7 +310,7 @@ class HookRecorder:
|
||||||
del self.calls[i]
|
del self.calls[i]
|
||||||
return call
|
return call
|
||||||
lines = [f"could not find call {name!r}, in:"]
|
lines = [f"could not find call {name!r}, in:"]
|
||||||
lines.extend([f" {x}" for x in self.calls])
|
lines.extend([" %s" % x for x in self.calls])
|
||||||
fail("\n".join(lines))
|
fail("\n".join(lines))
|
||||||
|
|
||||||
def getcall(self, name: str) -> RecordedHookCall:
|
def getcall(self, name: str) -> RecordedHookCall:
|
||||||
|
@ -801,7 +801,6 @@ 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")
|
||||||
|
@ -855,7 +854,6 @@ class Pytester:
|
||||||
existing files.
|
existing files.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_something(pytester):
|
def test_something(pytester):
|
||||||
|
@ -875,7 +873,6 @@ class Pytester:
|
||||||
existing files.
|
existing files.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_something(pytester):
|
def test_something(pytester):
|
||||||
|
@ -1201,9 +1198,7 @@ class Pytester:
|
||||||
if str(x).startswith("--basetemp"):
|
if str(x).startswith("--basetemp"):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
new_args.append(
|
new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp"))
|
||||||
"--basetemp={}".format(self.path.parent.joinpath("basetemp"))
|
|
||||||
)
|
|
||||||
return new_args
|
return new_args
|
||||||
|
|
||||||
def parseconfig(self, *args: str | os.PathLike[str]) -> Config:
|
def parseconfig(self, *args: str | os.PathLike[str]) -> Config:
|
||||||
|
@ -1484,7 +1479,7 @@ class Pytester:
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
|
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
|
||||||
args = (f"--basetemp={p}", *args)
|
args = ("--basetemp=%s" % p, *args)
|
||||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||||
if plugins:
|
if plugins:
|
||||||
args = ("-p", plugins[0], *args)
|
args = ("-p", plugins[0], *args)
|
||||||
|
@ -1590,7 +1585,7 @@ class LineMatcher:
|
||||||
self._log("matched: ", repr(line))
|
self._log("matched: ", repr(line))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
msg = f"line {line!r} not found in output"
|
msg = "line %r not found in output" % line
|
||||||
self._log(msg)
|
self._log(msg)
|
||||||
self._fail(msg)
|
self._fail(msg)
|
||||||
|
|
||||||
|
@ -1602,7 +1597,7 @@ class LineMatcher:
|
||||||
for i, line in enumerate(self.lines):
|
for i, line in enumerate(self.lines):
|
||||||
if fnline == line or fnmatch(line, fnline):
|
if fnline == line or fnmatch(line, fnline):
|
||||||
return self.lines[i + 1 :]
|
return self.lines[i + 1 :]
|
||||||
raise ValueError(f"line {fnline!r} not found in output")
|
raise ValueError("line %r not found in output" % fnline)
|
||||||
|
|
||||||
def _log(self, *args) -> None:
|
def _log(self, *args) -> None:
|
||||||
self._log_output.append(" ".join(str(x) for x in args))
|
self._log_output.append(" ".join(str(x) for x in args))
|
||||||
|
@ -1687,7 +1682,7 @@ class LineMatcher:
|
||||||
started = True
|
started = True
|
||||||
break
|
break
|
||||||
elif match_func(nextline, line):
|
elif match_func(nextline, line):
|
||||||
self._log(f"{match_nickname}:", repr(line))
|
self._log("%s:" % match_nickname, repr(line))
|
||||||
self._log(
|
self._log(
|
||||||
"{:>{width}}".format("with:", width=wnick), repr(nextline)
|
"{:>{width}}".format("with:", width=wnick), repr(nextline)
|
||||||
)
|
)
|
||||||
|
|
|
@ -226,7 +226,7 @@ def pytest_pycollect_makeitem(
|
||||||
filename, lineno = getfslineno(obj)
|
filename, lineno = getfslineno(obj)
|
||||||
warnings.warn_explicit(
|
warnings.warn_explicit(
|
||||||
message=PytestCollectionWarning(
|
message=PytestCollectionWarning(
|
||||||
f"cannot collect {name!r} because it is not a function."
|
"cannot collect %r because it is not a function." % name
|
||||||
),
|
),
|
||||||
category=None,
|
category=None,
|
||||||
filename=str(filename),
|
filename=str(filename),
|
||||||
|
@ -370,11 +370,7 @@ 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:
|
||||||
if not (self.classnamefilter(name) or self.isnosetest(obj)):
|
return 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
|
||||||
|
|
|
@ -7,7 +7,6 @@ from decimal import Decimal
|
||||||
import math
|
import math
|
||||||
from numbers import Complex
|
from numbers import Complex
|
||||||
import pprint
|
import pprint
|
||||||
import re
|
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
@ -129,8 +128,6 @@ def _recursive_sequence_map(f, x):
|
||||||
if isinstance(x, (list, tuple)):
|
if isinstance(x, (list, tuple)):
|
||||||
seq_type = type(x)
|
seq_type = type(x)
|
||||||
return seq_type(_recursive_sequence_map(f, xi) for xi in x)
|
return seq_type(_recursive_sequence_map(f, xi) for xi in x)
|
||||||
elif _is_sequence_like(x):
|
|
||||||
return [_recursive_sequence_map(f, xi) for xi in x]
|
|
||||||
else:
|
else:
|
||||||
return f(x)
|
return f(x)
|
||||||
|
|
||||||
|
@ -456,7 +453,7 @@ class ApproxScalar(ApproxBase):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Return true if the two numbers are within the tolerance.
|
# Return true if the two numbers are within the tolerance.
|
||||||
result: bool = abs(self.expected - actual) <= self.tolerance
|
result: bool = abs(self.expected - actual) <= self.tolerance # type: ignore[arg-type]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
||||||
|
@ -723,7 +720,11 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||||
elif _is_numpy_array(expected):
|
elif _is_numpy_array(expected):
|
||||||
expected = _as_numpy_array(expected)
|
expected = _as_numpy_array(expected)
|
||||||
cls = ApproxNumpy
|
cls = ApproxNumpy
|
||||||
elif _is_sequence_like(expected):
|
elif (
|
||||||
|
hasattr(expected, "__getitem__")
|
||||||
|
and isinstance(expected, Sized)
|
||||||
|
and not isinstance(expected, (str, bytes))
|
||||||
|
):
|
||||||
cls = ApproxSequenceLike
|
cls = ApproxSequenceLike
|
||||||
elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)):
|
elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)):
|
||||||
msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}"
|
msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}"
|
||||||
|
@ -734,14 +735,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||||
return cls(expected, rel, abs, nan_ok)
|
return cls(expected, rel, abs, nan_ok)
|
||||||
|
|
||||||
|
|
||||||
def _is_sequence_like(expected: object) -> bool:
|
|
||||||
return (
|
|
||||||
hasattr(expected, "__getitem__")
|
|
||||||
and isinstance(expected, Sized)
|
|
||||||
and not isinstance(expected, (str, bytes))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_numpy_array(obj: object) -> bool:
|
def _is_numpy_array(obj: object) -> bool:
|
||||||
"""
|
"""
|
||||||
Return true if the given object is implicitly convertible to ndarray,
|
Return true if the given object is implicitly convertible to ndarray,
|
||||||
|
@ -987,14 +980,6 @@ class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
|
||||||
self.message = message
|
self.message = message
|
||||||
self.match_expr = match_expr
|
self.match_expr = match_expr
|
||||||
self.excinfo: _pytest._code.ExceptionInfo[E] | None = None
|
self.excinfo: _pytest._code.ExceptionInfo[E] | None = None
|
||||||
if self.match_expr is not None:
|
|
||||||
re_error = None
|
|
||||||
try:
|
|
||||||
re.compile(self.match_expr)
|
|
||||||
except re.error as e:
|
|
||||||
re_error = e
|
|
||||||
if re_error is not None:
|
|
||||||
fail(f"Invalid regex pattern provided to 'match': {re_error}")
|
|
||||||
|
|
||||||
def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
|
def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
|
||||||
self.excinfo = _pytest._code.ExceptionInfo.for_later()
|
self.excinfo = _pytest._code.ExceptionInfo.for_later()
|
||||||
|
|
|
@ -13,7 +13,6 @@ from typing import Iterator
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import Sequence
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionChainRepr
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
|
@ -31,7 +30,6 @@ from _pytest._io import TerminalWriter
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.nodes import Collector
|
from _pytest.nodes import Collector
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
from _pytest.outcomes import fail
|
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
|
|
||||||
|
|
||||||
|
@ -192,26 +190,11 @@ class BaseReport:
|
||||||
return domain
|
return domain
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_verbose_word_with_markup(
|
def _get_verbose_word(self, config: Config):
|
||||||
self, config: Config, default_markup: Mapping[str, bool]
|
|
||||||
) -> tuple[str, Mapping[str, bool]]:
|
|
||||||
_category, _short, verbose = config.hook.pytest_report_teststatus(
|
_category, _short, verbose = config.hook.pytest_report_teststatus(
|
||||||
report=self, config=config
|
report=self, config=config
|
||||||
)
|
)
|
||||||
|
return verbose
|
||||||
if isinstance(verbose, str):
|
|
||||||
return verbose, default_markup
|
|
||||||
|
|
||||||
if isinstance(verbose, Sequence) and len(verbose) == 2:
|
|
||||||
word, markup = verbose
|
|
||||||
if isinstance(word, str) and isinstance(markup, Mapping):
|
|
||||||
return word, markup
|
|
||||||
|
|
||||||
fail( # pragma: no cover
|
|
||||||
"pytest_report_teststatus() hook (from a plugin) returned "
|
|
||||||
f"an invalid verbose value: {verbose!r}.\nExpected either a string "
|
|
||||||
"or a tuple of (word, markup)."
|
|
||||||
)
|
|
||||||
|
|
||||||
def _to_json(self) -> dict[str, Any]:
|
def _to_json(self) -> dict[str, Any]:
|
||||||
"""Return the contents of this report as a dict of builtin entries,
|
"""Return the contents of this report as a dict of builtin entries,
|
||||||
|
@ -243,10 +226,10 @@ def _report_unserialization_failure(
|
||||||
url = "https://github.com/pytest-dev/pytest/issues"
|
url = "https://github.com/pytest-dev/pytest/issues"
|
||||||
stream = StringIO()
|
stream = StringIO()
|
||||||
pprint("-" * 100, stream=stream)
|
pprint("-" * 100, stream=stream)
|
||||||
pprint(f"INTERNALERROR: Unknown entry type returned: {type_name}", stream=stream)
|
pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
|
||||||
pprint(f"report_name: {report_class}", stream=stream)
|
pprint("report_name: %s" % report_class, stream=stream)
|
||||||
pprint(reportdict, stream=stream)
|
pprint(reportdict, stream=stream)
|
||||||
pprint(f"Please report this bug at {url}", stream=stream)
|
pprint("Please report this bug at %s" % url, stream=stream)
|
||||||
pprint("-" * 100, stream=stream)
|
pprint("-" * 100, stream=stream)
|
||||||
raise RuntimeError(stream.getvalue())
|
raise RuntimeError(stream.getvalue())
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import bdb
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import types
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import final
|
from typing import final
|
||||||
|
@ -86,7 +85,7 @@ def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
|
||||||
if not durations:
|
if not durations:
|
||||||
tr.write_sep("=", "slowest durations")
|
tr.write_sep("=", "slowest durations")
|
||||||
else:
|
else:
|
||||||
tr.write_sep("=", f"slowest {durations} durations")
|
tr.write_sep("=", "slowest %s durations" % durations)
|
||||||
dlist = dlist[:durations]
|
dlist = dlist[:durations]
|
||||||
|
|
||||||
for i, rep in enumerate(dlist):
|
for i, rep in enumerate(dlist):
|
||||||
|
@ -130,10 +129,6 @@ def runtestprotocol(
|
||||||
show_test_item(item)
|
show_test_item(item)
|
||||||
if not item.config.getoption("setuponly", False):
|
if not item.config.getoption("setuponly", False):
|
||||||
reports.append(call_and_report(item, "call", log))
|
reports.append(call_and_report(item, "call", log))
|
||||||
# If the session is about to fail or stop, teardown everything - this is
|
|
||||||
# necessary to correctly report fixture teardown errors (see #11706)
|
|
||||||
if item.session.shouldfail or item.session.shouldstop:
|
|
||||||
nextitem = None
|
|
||||||
reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
|
reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
|
||||||
# After all teardown hooks have been called
|
# After all teardown hooks have been called
|
||||||
# want funcargs and request info to go away.
|
# want funcargs and request info to go away.
|
||||||
|
@ -167,7 +162,7 @@ def pytest_runtest_call(item: Item) -> None:
|
||||||
del sys.last_value
|
del sys.last_value
|
||||||
del sys.last_traceback
|
del sys.last_traceback
|
||||||
if sys.version_info >= (3, 12, 0):
|
if sys.version_info >= (3, 12, 0):
|
||||||
del sys.last_exc
|
del sys.last_exc # type: ignore[attr-defined]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
|
@ -177,11 +172,11 @@ def pytest_runtest_call(item: Item) -> None:
|
||||||
sys.last_type = type(e)
|
sys.last_type = type(e)
|
||||||
sys.last_value = e
|
sys.last_value = e
|
||||||
if sys.version_info >= (3, 12, 0):
|
if sys.version_info >= (3, 12, 0):
|
||||||
sys.last_exc = e
|
sys.last_exc = e # type: ignore[attr-defined]
|
||||||
assert e.__traceback__ is not None
|
assert e.__traceback__ is not None
|
||||||
# Skip *this* frame
|
# Skip *this* frame
|
||||||
sys.last_traceback = e.__traceback__.tb_next
|
sys.last_traceback = e.__traceback__.tb_next
|
||||||
raise
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None:
|
def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None:
|
||||||
|
@ -491,7 +486,7 @@ class SetupState:
|
||||||
# Node's finalizers.
|
# Node's finalizers.
|
||||||
list[Callable[[], object]],
|
list[Callable[[], object]],
|
||||||
# Node's exception and original traceback, if its setup raised.
|
# Node's exception and original traceback, if its setup raised.
|
||||||
tuple[OutcomeException | Exception, types.TracebackType | None] | None,
|
OutcomeException | Exception | None,
|
||||||
],
|
],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
|
@ -504,7 +499,7 @@ class SetupState:
|
||||||
for col, (finalizers, exc) in self.stack.items():
|
for col, (finalizers, exc) in self.stack.items():
|
||||||
assert col in needed_collectors, "previous item was not torn down properly"
|
assert col in needed_collectors, "previous item was not torn down properly"
|
||||||
if exc:
|
if exc:
|
||||||
raise exc[0].with_traceback(exc[1])
|
raise exc
|
||||||
|
|
||||||
for col in needed_collectors[len(self.stack) :]:
|
for col in needed_collectors[len(self.stack) :]:
|
||||||
assert col not in self.stack
|
assert col not in self.stack
|
||||||
|
@ -513,8 +508,8 @@ class SetupState:
|
||||||
try:
|
try:
|
||||||
col.setup()
|
col.setup()
|
||||||
except TEST_OUTCOME as exc:
|
except TEST_OUTCOME as exc:
|
||||||
self.stack[col] = (self.stack[col][0], (exc, exc.__traceback__))
|
self.stack[col] = (self.stack[col][0], exc)
|
||||||
raise
|
raise exc
|
||||||
|
|
||||||
def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
|
def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
|
||||||
"""Attach a finalizer to the given node.
|
"""Attach a finalizer to the given node.
|
||||||
|
|
|
@ -117,7 +117,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool,
|
||||||
result = eval(condition_code, globals_)
|
result = eval(condition_code, globals_)
|
||||||
except SyntaxError as exc:
|
except SyntaxError as exc:
|
||||||
msglines = [
|
msglines = [
|
||||||
f"Error evaluating {mark.name!r} condition",
|
"Error evaluating %r condition" % mark.name,
|
||||||
" " + condition,
|
" " + condition,
|
||||||
" " + " " * (exc.offset or 0) + "^",
|
" " + " " * (exc.offset or 0) + "^",
|
||||||
"SyntaxError: invalid syntax",
|
"SyntaxError: invalid syntax",
|
||||||
|
@ -125,7 +125,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool,
|
||||||
fail("\n".join(msglines), pytrace=False)
|
fail("\n".join(msglines), pytrace=False)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
msglines = [
|
msglines = [
|
||||||
f"Error evaluating {mark.name!r} condition",
|
"Error evaluating %r condition" % mark.name,
|
||||||
" " + condition,
|
" " + condition,
|
||||||
*traceback.format_exception_only(type(exc), exc),
|
*traceback.format_exception_only(type(exc), exc),
|
||||||
]
|
]
|
||||||
|
@ -137,7 +137,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool,
|
||||||
result = bool(condition)
|
result = bool(condition)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
msglines = [
|
msglines = [
|
||||||
f"Error evaluating {mark.name!r} condition as a boolean",
|
"Error evaluating %r condition as a boolean" % mark.name,
|
||||||
*traceback.format_exception_only(type(exc), exc),
|
*traceback.format_exception_only(type(exc), exc),
|
||||||
]
|
]
|
||||||
fail("\n".join(msglines), pytrace=False)
|
fail("\n".join(msglines), pytrace=False)
|
||||||
|
@ -149,7 +149,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool,
|
||||||
else:
|
else:
|
||||||
# XXX better be checked at collection time
|
# XXX better be checked at collection time
|
||||||
msg = (
|
msg = (
|
||||||
f"Error evaluating {mark.name!r}: "
|
"Error evaluating %r: " % mark.name
|
||||||
+ "you need to specify reason=STRING when using booleans as conditions."
|
+ "you need to specify reason=STRING when using booleans as conditions."
|
||||||
)
|
)
|
||||||
fail(msg, pytrace=False)
|
fail(msg, pytrace=False)
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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"
|
||||||
|
@ -32,6 +38,7 @@ 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 its own merits.
|
# allow --stepwise-skip to work on its own merits.
|
||||||
|
|
|
@ -212,13 +212,6 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
choices=["auto", "long", "short", "no", "line", "native"],
|
choices=["auto", "long", "short", "no", "line", "native"],
|
||||||
help="Traceback print mode (auto/long/short/line/native/no)",
|
help="Traceback print mode (auto/long/short/line/native/no)",
|
||||||
)
|
)
|
||||||
group._addoption(
|
|
||||||
"--xfail-tb",
|
|
||||||
action="store_true",
|
|
||||||
dest="xfail_tb",
|
|
||||||
default=False,
|
|
||||||
help="Show tracebacks for xfail (as long as --tb != no)",
|
|
||||||
)
|
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"--show-capture",
|
"--show-capture",
|
||||||
action="store",
|
action="store",
|
||||||
|
@ -435,7 +428,7 @@ class TerminalReporter:
|
||||||
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
|
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
|
||||||
return char in self.reportchars
|
return char in self.reportchars
|
||||||
|
|
||||||
def write_fspath_result(self, nodeid: str, res: str, **markup: bool) -> None:
|
def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None:
|
||||||
fspath = self.config.rootpath / nodeid.split("::")[0]
|
fspath = self.config.rootpath / nodeid.split("::")[0]
|
||||||
if self.currentfspath is None or fspath != self.currentfspath:
|
if self.currentfspath is None or fspath != self.currentfspath:
|
||||||
if self.currentfspath is not None and self._show_progress_info:
|
if self.currentfspath is not None and self._show_progress_info:
|
||||||
|
@ -568,11 +561,10 @@ class TerminalReporter:
|
||||||
def pytest_runtest_logstart(
|
def pytest_runtest_logstart(
|
||||||
self, nodeid: str, location: tuple[str, int | None, str]
|
self, nodeid: str, location: tuple[str, int | None, str]
|
||||||
) -> None:
|
) -> None:
|
||||||
fspath, lineno, domain = location
|
|
||||||
# Ensure that the path is printed before the
|
# Ensure that the path is printed before the
|
||||||
# 1st test of a module starts running.
|
# 1st test of a module starts running.
|
||||||
if self.showlongtestinfo:
|
if self.showlongtestinfo:
|
||||||
line = self._locationline(nodeid, fspath, lineno, domain)
|
line = self._locationline(nodeid, *location)
|
||||||
self.write_ensure_prefix(line, "")
|
self.write_ensure_prefix(line, "")
|
||||||
self.flush()
|
self.flush()
|
||||||
elif self.showfspath:
|
elif self.showfspath:
|
||||||
|
@ -595,6 +587,7 @@ class TerminalReporter:
|
||||||
if not letter and not word:
|
if not letter and not word:
|
||||||
# Probably passed setup/teardown.
|
# Probably passed setup/teardown.
|
||||||
return
|
return
|
||||||
|
running_xdist = hasattr(rep, "node")
|
||||||
if markup is None:
|
if markup is None:
|
||||||
was_xfail = hasattr(report, "wasxfail")
|
was_xfail = hasattr(report, "wasxfail")
|
||||||
if rep.passed and not was_xfail:
|
if rep.passed and not was_xfail:
|
||||||
|
@ -607,20 +600,11 @@ class TerminalReporter:
|
||||||
markup = {"yellow": True}
|
markup = {"yellow": True}
|
||||||
else:
|
else:
|
||||||
markup = {}
|
markup = {}
|
||||||
self._progress_nodeids_reported.add(rep.nodeid)
|
|
||||||
if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0:
|
if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0:
|
||||||
self._tw.write(letter, **markup)
|
self._tw.write(letter, **markup)
|
||||||
# When running in xdist, the logreport and logfinish of multiple
|
|
||||||
# items are interspersed, e.g. `logreport`, `logreport`,
|
|
||||||
# `logfinish`, `logfinish`. To avoid the "past edge" calculation
|
|
||||||
# from getting confused and overflowing (#7166), do the past edge
|
|
||||||
# printing here and not in logfinish, except for the 100% which
|
|
||||||
# should only be printed after all teardowns are finished.
|
|
||||||
if self._show_progress_info and not self._is_last_item:
|
|
||||||
self._write_progress_information_if_past_edge()
|
|
||||||
else:
|
else:
|
||||||
|
self._progress_nodeids_reported.add(rep.nodeid)
|
||||||
line = self._locationline(rep.nodeid, *rep.location)
|
line = self._locationline(rep.nodeid, *rep.location)
|
||||||
running_xdist = hasattr(rep, "node")
|
|
||||||
if not running_xdist:
|
if not running_xdist:
|
||||||
self.write_ensure_prefix(line, word, **markup)
|
self.write_ensure_prefix(line, word, **markup)
|
||||||
if rep.skipped or hasattr(report, "wasxfail"):
|
if rep.skipped or hasattr(report, "wasxfail"):
|
||||||
|
@ -643,7 +627,7 @@ class TerminalReporter:
|
||||||
self._write_progress_information_filling_space()
|
self._write_progress_information_filling_space()
|
||||||
else:
|
else:
|
||||||
self.ensure_newline()
|
self.ensure_newline()
|
||||||
self._tw.write(f"[{rep.node.gateway.id}]")
|
self._tw.write("[%s]" % rep.node.gateway.id)
|
||||||
if self._show_progress_info:
|
if self._show_progress_info:
|
||||||
self._tw.write(
|
self._tw.write(
|
||||||
self._get_progress_information_message() + " ", cyan=True
|
self._get_progress_information_message() + " ", cyan=True
|
||||||
|
@ -660,29 +644,39 @@ class TerminalReporter:
|
||||||
assert self._session is not None
|
assert self._session is not None
|
||||||
return len(self._progress_nodeids_reported) == self._session.testscollected
|
return len(self._progress_nodeids_reported) == self._session.testscollected
|
||||||
|
|
||||||
@hookimpl(wrapper=True)
|
def pytest_runtest_logfinish(self, nodeid: str) -> None:
|
||||||
def pytest_runtestloop(self) -> Generator[None, object, object]:
|
assert self._session
|
||||||
result = yield
|
|
||||||
|
|
||||||
# Write the final/100% progress -- deferred until the loop is complete.
|
|
||||||
if (
|
if (
|
||||||
self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0
|
self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0
|
||||||
and self._show_progress_info
|
and self._show_progress_info
|
||||||
and self._progress_nodeids_reported
|
|
||||||
):
|
):
|
||||||
self._write_progress_information_filling_space()
|
if self._show_progress_info == "count":
|
||||||
|
num_tests = self._session.testscollected
|
||||||
|
progress_length = len(f" [{num_tests}/{num_tests}]")
|
||||||
|
else:
|
||||||
|
progress_length = len(" [100%]")
|
||||||
|
|
||||||
return result
|
self._progress_nodeids_reported.add(nodeid)
|
||||||
|
|
||||||
|
if self._is_last_item:
|
||||||
|
self._write_progress_information_filling_space()
|
||||||
|
else:
|
||||||
|
main_color, _ = self._get_main_color()
|
||||||
|
w = self._width_of_current_line
|
||||||
|
past_edge = w + progress_length + 1 >= self._screen_width
|
||||||
|
if past_edge:
|
||||||
|
msg = self._get_progress_information_message()
|
||||||
|
self._tw.write(msg + "\n", **{main_color: True})
|
||||||
|
|
||||||
def _get_progress_information_message(self) -> str:
|
def _get_progress_information_message(self) -> str:
|
||||||
assert self._session
|
assert self._session
|
||||||
collected = self._session.testscollected
|
collected = self._session.testscollected
|
||||||
if self._show_progress_info == "count":
|
if self._show_progress_info == "count":
|
||||||
if collected:
|
if collected:
|
||||||
progress = len(self._progress_nodeids_reported)
|
progress = self._progress_nodeids_reported
|
||||||
counter_format = f"{{:{len(str(collected))}d}}"
|
counter_format = f"{{:{len(str(collected))}d}}"
|
||||||
format_string = f" [{counter_format}/{{}}]"
|
format_string = f" [{counter_format}/{{}}]"
|
||||||
return format_string.format(progress, collected)
|
return format_string.format(len(progress), collected)
|
||||||
return f" [ {collected} / {collected} ]"
|
return f" [ {collected} / {collected} ]"
|
||||||
else:
|
else:
|
||||||
if collected:
|
if collected:
|
||||||
|
@ -691,20 +685,6 @@ class TerminalReporter:
|
||||||
)
|
)
|
||||||
return " [100%]"
|
return " [100%]"
|
||||||
|
|
||||||
def _write_progress_information_if_past_edge(self) -> None:
|
|
||||||
w = self._width_of_current_line
|
|
||||||
if self._show_progress_info == "count":
|
|
||||||
assert self._session
|
|
||||||
num_tests = self._session.testscollected
|
|
||||||
progress_length = len(f" [{num_tests}/{num_tests}]")
|
|
||||||
else:
|
|
||||||
progress_length = len(" [100%]")
|
|
||||||
past_edge = w + progress_length + 1 >= self._screen_width
|
|
||||||
if past_edge:
|
|
||||||
main_color, _ = self._get_main_color()
|
|
||||||
msg = self._get_progress_information_message()
|
|
||||||
self._tw.write(msg + "\n", **{main_color: True})
|
|
||||||
|
|
||||||
def _write_progress_information_filling_space(self) -> None:
|
def _write_progress_information_filling_space(self) -> None:
|
||||||
color, _ = self._get_main_color()
|
color, _ = self._get_main_color()
|
||||||
msg = self._get_progress_information_message()
|
msg = self._get_progress_information_message()
|
||||||
|
@ -821,9 +801,7 @@ class TerminalReporter:
|
||||||
|
|
||||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||||
if plugininfo:
|
if plugininfo:
|
||||||
result.append(
|
result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
|
||||||
"plugins: {}".format(", ".join(_plugin_nameversions(plugininfo)))
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def pytest_collection_finish(self, session: Session) -> None:
|
def pytest_collection_finish(self, session: Session) -> None:
|
||||||
|
@ -955,7 +933,7 @@ class TerminalReporter:
|
||||||
line += "[".join(values)
|
line += "[".join(values)
|
||||||
return line
|
return line
|
||||||
|
|
||||||
# fspath comes from testid which has a "/"-normalized path.
|
# collect_fspath comes from testid which has a "/"-normalized path.
|
||||||
if fspath:
|
if fspath:
|
||||||
res = mkrel(nodeid)
|
res = mkrel(nodeid)
|
||||||
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
|
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
|
||||||
|
@ -1089,29 +1067,24 @@ class TerminalReporter:
|
||||||
self._tw.line(content)
|
self._tw.line(content)
|
||||||
|
|
||||||
def summary_failures(self) -> None:
|
def summary_failures(self) -> None:
|
||||||
style = self.config.option.tbstyle
|
self.summary_failures_combined("failed", "FAILURES")
|
||||||
self.summary_failures_combined("failed", "FAILURES", style=style)
|
|
||||||
|
|
||||||
def summary_xfailures(self) -> None:
|
def summary_xfailures(self) -> None:
|
||||||
show_tb = self.config.option.xfail_tb
|
self.summary_failures_combined("xfailed", "XFAILURES", needed_opt="x")
|
||||||
style = self.config.option.tbstyle if show_tb else "no"
|
|
||||||
self.summary_failures_combined("xfailed", "XFAILURES", style=style)
|
|
||||||
|
|
||||||
def summary_failures_combined(
|
def summary_failures_combined(
|
||||||
self,
|
self,
|
||||||
which_reports: str,
|
which_reports: str,
|
||||||
sep_title: str,
|
sep_title: str,
|
||||||
*,
|
|
||||||
style: str,
|
|
||||||
needed_opt: str | None = None,
|
needed_opt: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if style != "no":
|
if self.config.option.tbstyle != "no":
|
||||||
if not needed_opt or self.hasopt(needed_opt):
|
if not needed_opt or self.hasopt(needed_opt):
|
||||||
reports: list[BaseReport] = self.getreports(which_reports)
|
reports: list[BaseReport] = self.getreports(which_reports)
|
||||||
if not reports:
|
if not reports:
|
||||||
return
|
return
|
||||||
self.write_sep("=", sep_title)
|
self.write_sep("=", sep_title)
|
||||||
if style == "line":
|
if self.config.option.tbstyle == "line":
|
||||||
for rep in reports:
|
for rep in reports:
|
||||||
line = self._getcrashline(rep)
|
line = self._getcrashline(rep)
|
||||||
self.write_line(line)
|
self.write_line(line)
|
||||||
|
@ -1206,10 +1179,10 @@ class TerminalReporter:
|
||||||
def show_xfailed(lines: list[str]) -> None:
|
def show_xfailed(lines: list[str]) -> None:
|
||||||
xfailed = self.stats.get("xfailed", [])
|
xfailed = self.stats.get("xfailed", [])
|
||||||
for rep in xfailed:
|
for rep in xfailed:
|
||||||
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
|
verbose_word = rep._get_verbose_word(self.config)
|
||||||
self.config, {_color_for_type["warnings"]: True}
|
markup_word = self._tw.markup(
|
||||||
|
verbose_word, **{_color_for_type["warnings"]: True}
|
||||||
)
|
)
|
||||||
markup_word = self._tw.markup(verbose_word, **verbose_markup)
|
|
||||||
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
|
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
|
||||||
line = f"{markup_word} {nodeid}"
|
line = f"{markup_word} {nodeid}"
|
||||||
reason = rep.wasxfail
|
reason = rep.wasxfail
|
||||||
|
@ -1221,10 +1194,10 @@ class TerminalReporter:
|
||||||
def show_xpassed(lines: list[str]) -> None:
|
def show_xpassed(lines: list[str]) -> None:
|
||||||
xpassed = self.stats.get("xpassed", [])
|
xpassed = self.stats.get("xpassed", [])
|
||||||
for rep in xpassed:
|
for rep in xpassed:
|
||||||
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
|
verbose_word = rep._get_verbose_word(self.config)
|
||||||
self.config, {_color_for_type["warnings"]: True}
|
markup_word = self._tw.markup(
|
||||||
|
verbose_word, **{_color_for_type["warnings"]: True}
|
||||||
)
|
)
|
||||||
markup_word = self._tw.markup(verbose_word, **verbose_markup)
|
|
||||||
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
|
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
|
||||||
line = f"{markup_word} {nodeid}"
|
line = f"{markup_word} {nodeid}"
|
||||||
reason = rep.wasxfail
|
reason = rep.wasxfail
|
||||||
|
@ -1237,10 +1210,10 @@ class TerminalReporter:
|
||||||
fskips = _folded_skips(self.startpath, skipped) if skipped else []
|
fskips = _folded_skips(self.startpath, skipped) if skipped else []
|
||||||
if not fskips:
|
if not fskips:
|
||||||
return
|
return
|
||||||
verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup(
|
verbose_word = skipped[0]._get_verbose_word(self.config)
|
||||||
self.config, {_color_for_type["warnings"]: True}
|
markup_word = self._tw.markup(
|
||||||
|
verbose_word, **{_color_for_type["warnings"]: True}
|
||||||
)
|
)
|
||||||
markup_word = self._tw.markup(verbose_word, **verbose_markup)
|
|
||||||
prefix = "Skipped: "
|
prefix = "Skipped: "
|
||||||
for num, fspath, lineno, reason in fskips:
|
for num, fspath, lineno, reason in fskips:
|
||||||
if reason.startswith(prefix):
|
if reason.startswith(prefix):
|
||||||
|
@ -1421,10 +1394,8 @@ def _get_line_with_reprcrash_message(
|
||||||
config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool]
|
config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool]
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get summary line for a report, trying to add reprcrash message."""
|
"""Get summary line for a report, trying to add reprcrash message."""
|
||||||
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
|
verbose_word = rep._get_verbose_word(config)
|
||||||
config, word_markup
|
word = tw.markup(verbose_word, **word_markup)
|
||||||
)
|
|
||||||
word = tw.markup(verbose_word, **verbose_markup)
|
|
||||||
node = _get_node_id_with_markup(tw, config, rep)
|
node = _get_node_id_with_markup(tw, config, rep)
|
||||||
|
|
||||||
line = f"{word} {node}"
|
line = f"{word} {node}"
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
|
@ -41,29 +40,23 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
import twisted.trial.unittest
|
import twisted.trial.unittest
|
||||||
|
|
||||||
|
_SysExcInfoType = Union[
|
||||||
_SysExcInfoType = Union[
|
Tuple[Type[BaseException], BaseException, types.TracebackType],
|
||||||
Tuple[Type[BaseException], BaseException, types.TracebackType],
|
Tuple[None, None, None],
|
||||||
Tuple[None, None, None],
|
]
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_pycollect_makeitem(
|
def pytest_pycollect_makeitem(
|
||||||
collector: Module | Class, name: str, obj: object
|
collector: Module | Class, name: str, obj: object
|
||||||
) -> UnitTestCase | None:
|
) -> UnitTestCase | None:
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
|
@ -209,7 +209,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 = []
|
||||||
|
@ -553,7 +553,7 @@ def batch_make_numbered_dirs(rootdir, repeats):
|
||||||
for i in range(repeats):
|
for i in range(repeats):
|
||||||
dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir)
|
dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir)
|
||||||
file_ = dir_.join("foo")
|
file_ = dir_.join("foo")
|
||||||
file_.write_text(f"{i}", encoding="utf-8")
|
file_.write_text("%s" % i, encoding="utf-8")
|
||||||
actual = int(file_.read_text(encoding="utf-8"))
|
actual = int(file_.read_text(encoding="utf-8"))
|
||||||
assert (
|
assert (
|
||||||
actual == i
|
actual == i
|
||||||
|
@ -565,9 +565,9 @@ def batch_make_numbered_dirs(rootdir, repeats):
|
||||||
class TestLocalPath(CommonFSTests):
|
class TestLocalPath(CommonFSTests):
|
||||||
def test_join_normpath(self, tmpdir):
|
def test_join_normpath(self, tmpdir):
|
||||||
assert tmpdir.join(".") == tmpdir
|
assert tmpdir.join(".") == tmpdir
|
||||||
p = tmpdir.join(f"../{tmpdir.basename}")
|
p = tmpdir.join("../%s" % tmpdir.basename)
|
||||||
assert p == tmpdir
|
assert p == tmpdir
|
||||||
p = tmpdir.join(f"..//{tmpdir.basename}/")
|
p = tmpdir.join("..//%s/" % tmpdir.basename)
|
||||||
assert p == tmpdir
|
assert p == tmpdir
|
||||||
|
|
||||||
@skiponwin32
|
@skiponwin32
|
||||||
|
@ -724,7 +724,7 @@ class TestLocalPath(CommonFSTests):
|
||||||
|
|
||||||
@pytest.mark.parametrize("bin", (False, True))
|
@pytest.mark.parametrize("bin", (False, True))
|
||||||
def test_dump(self, tmpdir, bin):
|
def test_dump(self, tmpdir, bin):
|
||||||
path = tmpdir.join(f"dumpfile{int(bin)}")
|
path = tmpdir.join("dumpfile%s" % int(bin))
|
||||||
try:
|
try:
|
||||||
d = {"answer": 42}
|
d = {"answer": 42}
|
||||||
path.dump(d, bin=bin)
|
path.dump(d, bin=bin)
|
||||||
|
|
|
@ -402,7 +402,7 @@ class TestGeneralUsage:
|
||||||
|
|
||||||
for name, value in vars(hookspec).items():
|
for name, value in vars(hookspec).items():
|
||||||
if name.startswith("pytest_"):
|
if name.startswith("pytest_"):
|
||||||
assert value.__doc__, f"no docstring for {name}"
|
assert value.__doc__, "no docstring for %s" % name
|
||||||
|
|
||||||
def test_initialization_error_issue49(self, pytester: Pytester) -> None:
|
def test_initialization_error_issue49(self, pytester: Pytester) -> None:
|
||||||
pytester.makeconftest(
|
pytester.makeconftest(
|
||||||
|
@ -975,7 +975,7 @@ class TestDurations:
|
||||||
for x in tested:
|
for x in tested:
|
||||||
for y in ("call",): # 'setup', 'call', 'teardown':
|
for y in ("call",): # 'setup', 'call', 'teardown':
|
||||||
for line in result.stdout.lines:
|
for line in result.stdout.lines:
|
||||||
if (f"test_{x}") in line and y in line:
|
if ("test_%s" % x) in line and y in line:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise AssertionError(f"not found {x} {y}")
|
raise AssertionError(f"not found {x} {y}")
|
||||||
|
@ -988,7 +988,7 @@ class TestDurations:
|
||||||
for x in "123":
|
for x in "123":
|
||||||
for y in ("call",): # 'setup', 'call', 'teardown':
|
for y in ("call",): # 'setup', 'call', 'teardown':
|
||||||
for line in result.stdout.lines:
|
for line in result.stdout.lines:
|
||||||
if (f"test_{x}") in line and y in line:
|
if ("test_%s" % x) in line and y in line:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise AssertionError(f"not found {x} {y}")
|
raise AssertionError(f"not found {x} {y}")
|
||||||
|
@ -1466,21 +1466,14 @@ def test_issue_9765(pytester: Pytester) -> None:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
subprocess.run(
|
subprocess.run([sys.executable, "setup.py", "develop"], check=True)
|
||||||
[sys.executable, "-Im", "pip", "install", "-e", "."],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
# We are using subprocess.run rather than pytester.run on purpose.
|
# We are using subprocess.run rather than pytester.run on purpose.
|
||||||
# pytester.run is adding the current directory to PYTHONPATH which avoids
|
# pytester.run is adding the current directory to PYTHONPATH which avoids
|
||||||
# the bug. We also use pytest rather than python -m pytest for the same
|
# the bug. We also use pytest rather than python -m pytest for the same
|
||||||
# PYTHONPATH reason.
|
# PYTHONPATH reason.
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["pytest", "my_package"],
|
["pytest", "my_package"], capture_output=True, check=True, text=True
|
||||||
capture_output=True,
|
|
||||||
check=True,
|
|
||||||
encoding="utf-8",
|
|
||||||
text=True,
|
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
|
|
|
@ -11,7 +11,6 @@ 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
|
||||||
|
@ -28,7 +27,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
|
||||||
|
@ -713,29 +712,6 @@ 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(
|
||||||
"""
|
"""
|
||||||
|
@ -925,7 +901,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)
|
||||||
|
@ -1052,7 +1028,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)
|
||||||
|
@ -1434,7 +1410,7 @@ raise ValueError()
|
||||||
mod.f()
|
mod.f()
|
||||||
|
|
||||||
# emulate the issue described in #1984
|
# emulate the issue described in #1984
|
||||||
attr = f"__{reason}__"
|
attr = "__%s__" % reason
|
||||||
getattr(excinfo.value, attr).__traceback__ = None
|
getattr(excinfo.value, attr).__traceback__ = None
|
||||||
|
|
||||||
r = excinfo.getrepr()
|
r = excinfo.getrepr()
|
||||||
|
|
|
@ -13,4 +13,4 @@ if __name__ == "__main__":
|
||||||
executable = os.path.join(os.getcwd(), "dist", "runtests_script", "runtests_script")
|
executable = os.path.join(os.getcwd(), "dist", "runtests_script", "runtests_script")
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
executable += ".exe"
|
executable += ".exe"
|
||||||
sys.exit(os.system(f"{executable} tests"))
|
sys.exit(os.system("%s tests" % executable))
|
||||||
|
|
|
@ -146,7 +146,7 @@ def test_big_repr():
|
||||||
def test_repr_on_newstyle() -> None:
|
def test_repr_on_newstyle() -> None:
|
||||||
class Function:
|
class Function:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.name}>" # type: ignore[attr-defined]
|
return "<%s>" % (self.name) # type: ignore[attr-defined]
|
||||||
|
|
||||||
assert saferepr(Function())
|
assert saferepr(Function())
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
anyio[curio,trio]==4.4.0
|
anyio[curio,trio]==4.4.0
|
||||||
django==5.0.6
|
django==5.0.6
|
||||||
pytest-asyncio==0.23.7
|
pytest-asyncio==0.23.7
|
||||||
pytest-bdd==7.2.0
|
pytest-bdd==7.1.2
|
||||||
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
|
||||||
|
|
|
@ -953,43 +953,6 @@ class TestApprox:
|
||||||
with pytest.raises(TypeError, match="only supports ordered sequences"):
|
with pytest.raises(TypeError, match="only supports ordered sequences"):
|
||||||
assert {1, 2, 3} == approx({1, 2, 3})
|
assert {1, 2, 3} == approx({1, 2, 3})
|
||||||
|
|
||||||
def test_strange_sequence(self):
|
|
||||||
"""https://github.com/pytest-dev/pytest/issues/11797"""
|
|
||||||
a = MyVec3(1, 2, 3)
|
|
||||||
b = MyVec3(0, 1, 2)
|
|
||||||
|
|
||||||
# this would trigger the error inside the test
|
|
||||||
pytest.approx(a, abs=0.5)._repr_compare(b)
|
|
||||||
|
|
||||||
assert b == pytest.approx(a, abs=2)
|
|
||||||
assert b != pytest.approx(a, abs=0.5)
|
|
||||||
|
|
||||||
|
|
||||||
class MyVec3: # incomplete
|
|
||||||
"""sequence like"""
|
|
||||||
|
|
||||||
_x: int
|
|
||||||
_y: int
|
|
||||||
_z: int
|
|
||||||
|
|
||||||
def __init__(self, x: int, y: int, z: int):
|
|
||||||
self._x, self._y, self._z = x, y, z
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<MyVec3 {self._x} {self._y} {self._z}>"
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return 3
|
|
||||||
|
|
||||||
def __getitem__(self, key: int) -> int:
|
|
||||||
if key == 0:
|
|
||||||
return self._x
|
|
||||||
if key == 1:
|
|
||||||
return self._y
|
|
||||||
if key == 2:
|
|
||||||
return self._z
|
|
||||||
raise IndexError(key)
|
|
||||||
|
|
||||||
|
|
||||||
class TestRecursiveSequenceMap:
|
class TestRecursiveSequenceMap:
|
||||||
def test_map_over_scalar(self):
|
def test_map_over_scalar(self):
|
||||||
|
@ -1017,6 +980,3 @@ class TestRecursiveSequenceMap:
|
||||||
(5, 8),
|
(5, 8),
|
||||||
[(7)],
|
[(7)],
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_map_over_sequence_like(self):
|
|
||||||
assert _recursive_sequence_map(int, MyVec3(1, 2, 3)) == [1, 2, 3]
|
|
||||||
|
|
|
@ -37,9 +37,9 @@ class TestModule:
|
||||||
[
|
[
|
||||||
"*import*mismatch*",
|
"*import*mismatch*",
|
||||||
"*imported*test_whatever*",
|
"*imported*test_whatever*",
|
||||||
f"*{p1}*",
|
"*%s*" % p1,
|
||||||
"*not the same*",
|
"*not the same*",
|
||||||
f"*{p2}*",
|
"*%s*" % p2,
|
||||||
"*HINT*",
|
"*HINT*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -263,32 +263,6 @@ 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:
|
||||||
|
|
|
@ -2289,17 +2289,18 @@ class TestFixtureMarker:
|
||||||
This was a regression introduced in the fix for #736.
|
This was a regression introduced in the fix for #736.
|
||||||
"""
|
"""
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
f"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@pytest.fixture(params=[1, 2])
|
@pytest.fixture(params=[1, 2])
|
||||||
def fixt(request):
|
def fixt(request):
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
@pytest.mark.parametrize({param_args}, [(3, 'x'), (4, 'x')])
|
@pytest.mark.parametrize(%s, [(3, 'x'), (4, 'x')])
|
||||||
def test_foo(fixt, val):
|
def test_foo(fixt, val):
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
|
% param_args
|
||||||
)
|
)
|
||||||
reprec = pytester.inline_run()
|
reprec = pytester.inline_run()
|
||||||
reprec.assertoutcome(passed=2)
|
reprec.assertoutcome(passed=2)
|
||||||
|
@ -3417,28 +3418,6 @@ class TestErrors:
|
||||||
["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"]
|
["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_cached_exception_doesnt_get_longer(self, pytester: Pytester) -> None:
|
|
||||||
"""Regression test for #12204."""
|
|
||||||
pytester.makepyfile(
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def bad(): 1 / 0
|
|
||||||
|
|
||||||
def test_1(bad): pass
|
|
||||||
def test_2(bad): pass
|
|
||||||
def test_3(bad): pass
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
result = pytester.runpytest_inprocess("--tb=native")
|
|
||||||
assert result.ret == ExitCode.TESTS_FAILED
|
|
||||||
failures = result.reprec.getfailures() # type: ignore[attr-defined]
|
|
||||||
assert len(failures) == 3
|
|
||||||
lines1 = failures[1].longrepr.reprtraceback.reprentries[0].lines
|
|
||||||
lines2 = failures[2].longrepr.reprtraceback.reprentries[0].lines
|
|
||||||
assert len(lines1) == len(lines2)
|
|
||||||
|
|
||||||
|
|
||||||
class TestShowFixtures:
|
class TestShowFixtures:
|
||||||
def test_funcarg_compat(self, pytester: Pytester) -> None:
|
def test_funcarg_compat(self, pytester: Pytester) -> None:
|
||||||
|
@ -4295,39 +4274,6 @@ 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.
|
||||||
|
|
|
@ -132,26 +132,6 @@ class TestRaises:
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*2 failed*"])
|
result.stdout.fnmatch_lines(["*2 failed*"])
|
||||||
|
|
||||||
def test_raises_with_invalid_regex(self, pytester: Pytester) -> None:
|
|
||||||
pytester.makepyfile(
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
def test_invalid_regex():
|
|
||||||
with pytest.raises(ValueError, match="invalid regex character ["):
|
|
||||||
raise ValueError()
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
result = pytester.runpytest()
|
|
||||||
result.stdout.fnmatch_lines(
|
|
||||||
[
|
|
||||||
"*Invalid regex pattern provided to 'match': unterminated character set at position 24*",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
result.stdout.no_fnmatch_line("*Traceback*")
|
|
||||||
result.stdout.no_fnmatch_line("*File*")
|
|
||||||
result.stdout.no_fnmatch_line("*line*")
|
|
||||||
|
|
||||||
def test_noclass(self) -> None:
|
def test_noclass(self) -> None:
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
pytest.raises("wrong", lambda: None) # type: ignore[call-overload]
|
pytest.raises("wrong", lambda: None) # type: ignore[call-overload]
|
||||||
|
|
|
@ -101,7 +101,7 @@ class TestImportHookInstallation:
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
pytester.makepyfile(**contents)
|
pytester.makepyfile(**contents)
|
||||||
result = pytester.runpytest_subprocess(f"--assert={mode}")
|
result = pytester.runpytest_subprocess("--assert=%s" % mode)
|
||||||
if mode == "plain":
|
if mode == "plain":
|
||||||
expected = "E AssertionError"
|
expected = "E AssertionError"
|
||||||
elif mode == "rewrite":
|
elif mode == "rewrite":
|
||||||
|
@ -163,7 +163,7 @@ class TestImportHookInstallation:
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
pytester.makepyfile(**contents)
|
pytester.makepyfile(**contents)
|
||||||
result = pytester.runpytest_subprocess(f"--assert={mode}")
|
result = pytester.runpytest_subprocess("--assert=%s" % mode)
|
||||||
if mode == "plain":
|
if mode == "plain":
|
||||||
expected = "E AssertionError"
|
expected = "E AssertionError"
|
||||||
elif mode == "rewrite":
|
elif mode == "rewrite":
|
||||||
|
@ -223,7 +223,7 @@ class TestImportHookInstallation:
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
||||||
# Make sure the hook is installed early enough so that plugins
|
# Make sure the hook is installed early enough so that plugins
|
||||||
# installed via distribution package are rewritten.
|
# installed via setuptools are rewritten.
|
||||||
pytester.mkdir("hampkg")
|
pytester.mkdir("hampkg")
|
||||||
contents = {
|
contents = {
|
||||||
"hampkg/__init__.py": """\
|
"hampkg/__init__.py": """\
|
||||||
|
@ -280,7 +280,7 @@ class TestImportHookInstallation:
|
||||||
}
|
}
|
||||||
pytester.makepyfile(**contents)
|
pytester.makepyfile(**contents)
|
||||||
result = pytester.run(
|
result = pytester.run(
|
||||||
sys.executable, "mainwrapper.py", "-s", f"--assert={mode}"
|
sys.executable, "mainwrapper.py", "-s", "--assert=%s" % mode
|
||||||
)
|
)
|
||||||
if mode == "plain":
|
if mode == "plain":
|
||||||
expected = "E AssertionError"
|
expected = "E AssertionError"
|
||||||
|
@ -2045,36 +2045,3 @@ 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)
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import marshal
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import py_compile
|
import py_compile
|
||||||
import re
|
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
@ -25,7 +24,6 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
from _pytest.assertion.rewrite import _get_assertion_exprs
|
from _pytest.assertion.rewrite import _get_assertion_exprs
|
||||||
from _pytest.assertion.rewrite import _get_maxsize_for_saferepr
|
from _pytest.assertion.rewrite import _get_maxsize_for_saferepr
|
||||||
from _pytest.assertion.rewrite import _saferepr
|
|
||||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||||
from _pytest.assertion.rewrite import get_cache_dir
|
from _pytest.assertion.rewrite import get_cache_dir
|
||||||
from _pytest.assertion.rewrite import PYC_TAIL
|
from _pytest.assertion.rewrite import PYC_TAIL
|
||||||
|
@ -130,7 +128,6 @@ 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
|
||||||
|
@ -309,7 +306,9 @@ class TestAssertionRewrite:
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
assert result.ret == 1
|
assert result.ret == 1
|
||||||
result.stdout.fnmatch_lines([f"*AssertionError*{(1, 2)!r}*", "*assert 1 == 2*"])
|
result.stdout.fnmatch_lines(
|
||||||
|
["*AssertionError*%s*" % repr((1, 2)), "*assert 1 == 2*"]
|
||||||
|
)
|
||||||
|
|
||||||
def test_assertion_message_expr(self, pytester: Pytester) -> None:
|
def test_assertion_message_expr(self, pytester: Pytester) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
|
@ -907,7 +906,7 @@ def test_rewritten():
|
||||||
assert test_optimized.__doc__ is None"""
|
assert test_optimized.__doc__ is None"""
|
||||||
)
|
)
|
||||||
p = make_numbered_dir(root=Path(pytester.path), prefix="runpytest-")
|
p = make_numbered_dir(root=Path(pytester.path), prefix="runpytest-")
|
||||||
tmp = f"--basetemp={p}"
|
tmp = "--basetemp=%s" % p
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setenv("PYTHONOPTIMIZE", "2")
|
mp.setenv("PYTHONOPTIMIZE", "2")
|
||||||
mp.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
|
mp.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
|
||||||
|
@ -2038,9 +2037,7 @@ class TestPyCacheDir:
|
||||||
assert test_foo_pyc.is_file()
|
assert test_foo_pyc.is_file()
|
||||||
|
|
||||||
# normal file: not touched by pytest, normal cache tag
|
# normal file: not touched by pytest, normal cache tag
|
||||||
bar_init_pyc = (
|
bar_init_pyc = get_cache_dir(bar_init) / f"__init__.{sys.implementation.cache_tag}.pyc"
|
||||||
get_cache_dir(bar_init) / f"__init__.{sys.implementation.cache_tag}.pyc"
|
|
||||||
)
|
|
||||||
assert bar_init_pyc.is_file()
|
assert bar_init_pyc.is_file()
|
||||||
|
|
||||||
|
|
||||||
|
@ -2107,26 +2104,3 @@ class TestIssue11140:
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
class TestSafereprUnbounded:
|
|
||||||
class Help:
|
|
||||||
def bound_method(self): # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_saferepr_bound_method(self):
|
|
||||||
"""saferepr() of a bound method should show only the method name"""
|
|
||||||
assert _saferepr(self.Help().bound_method) == "bound_method"
|
|
||||||
|
|
||||||
def test_saferepr_unbounded(self):
|
|
||||||
"""saferepr() of an unbound method should still show the full information"""
|
|
||||||
obj = self.Help()
|
|
||||||
# using id() to fetch memory address fails on different platforms
|
|
||||||
pattern = re.compile(
|
|
||||||
rf"<{Path(__file__).stem}.{self.__class__.__name__}.Help object at 0x[0-9a-fA-F]*>",
|
|
||||||
)
|
|
||||||
assert pattern.match(_saferepr(obj))
|
|
||||||
assert (
|
|
||||||
_saferepr(self.Help)
|
|
||||||
== f"<class '{Path(__file__).stem}.{self.__class__.__name__}.Help'>"
|
|
||||||
)
|
|
||||||
|
|
|
@ -206,7 +206,7 @@ def test_cache_reportheader(
|
||||||
monkeypatch.delenv("TOX_ENV_DIR", raising=False)
|
monkeypatch.delenv("TOX_ENV_DIR", raising=False)
|
||||||
expected = ".pytest_cache"
|
expected = ".pytest_cache"
|
||||||
result = pytester.runpytest("-v")
|
result = pytester.runpytest("-v")
|
||||||
result.stdout.fnmatch_lines([f"cachedir: {expected}"])
|
result.stdout.fnmatch_lines(["cachedir: %s" % expected])
|
||||||
|
|
||||||
|
|
||||||
def test_cache_reportheader_external_abspath(
|
def test_cache_reportheader_external_abspath(
|
||||||
|
@ -1163,7 +1163,7 @@ class TestNewFirst:
|
||||||
)
|
)
|
||||||
|
|
||||||
p1.write_text(
|
p1.write_text(
|
||||||
"def test_1(): assert 1\ndef test_2(): assert 1\n", encoding="utf-8"
|
"def test_1(): assert 1\n" "def 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)))
|
||||||
|
|
||||||
|
|
|
@ -105,15 +105,16 @@ class TestCaptureManager:
|
||||||
def test_capturing_unicode(pytester: Pytester, method: str) -> None:
|
def test_capturing_unicode(pytester: Pytester, method: str) -> None:
|
||||||
obj = "'b\u00f6y'"
|
obj = "'b\u00f6y'"
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
f"""\
|
"""\
|
||||||
# taken from issue 227 from nosetests
|
# taken from issue 227 from nosetests
|
||||||
def test_unicode():
|
def test_unicode():
|
||||||
import sys
|
import sys
|
||||||
print(sys.stdout)
|
print(sys.stdout)
|
||||||
print({obj})
|
print(%s)
|
||||||
"""
|
"""
|
||||||
|
% obj
|
||||||
)
|
)
|
||||||
result = pytester.runpytest(f"--capture={method}")
|
result = pytester.runpytest("--capture=%s" % method)
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,7 +126,7 @@ def test_capturing_bytes_in_utf8_encoding(pytester: Pytester, method: str) -> No
|
||||||
print('b\\u00f6y')
|
print('b\\u00f6y')
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest(f"--capture={method}")
|
result = pytester.runpytest("--capture=%s" % method)
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -152,8 +152,20 @@ class TestCollectFS:
|
||||||
assert "test_notfound" not in s
|
assert "test_notfound" not in s
|
||||||
assert "test_found" in s
|
assert "test_found" in s
|
||||||
|
|
||||||
def test_ignored_virtualenvs(self, pytester: Pytester) -> None:
|
@pytest.mark.parametrize(
|
||||||
ensure_file(pytester.path / "virtual" / "pyvenv.cfg")
|
"fname",
|
||||||
|
(
|
||||||
|
"activate",
|
||||||
|
"activate.csh",
|
||||||
|
"activate.fish",
|
||||||
|
"Activate",
|
||||||
|
"Activate.bat",
|
||||||
|
"Activate.ps1",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_ignored_virtualenvs(self, pytester: Pytester, fname: str) -> None:
|
||||||
|
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
|
||||||
|
ensure_file(pytester.path / "virtual" / bindir / fname)
|
||||||
testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py")
|
testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py")
|
||||||
testfile.write_text("def test_hello(): pass", encoding="utf-8")
|
testfile.write_text("def test_hello(): pass", encoding="utf-8")
|
||||||
|
|
||||||
|
@ -167,11 +179,23 @@ class TestCollectFS:
|
||||||
result = pytester.runpytest("virtual")
|
result = pytester.runpytest("virtual")
|
||||||
assert "test_invenv" in result.stdout.str()
|
assert "test_invenv" in result.stdout.str()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"fname",
|
||||||
|
(
|
||||||
|
"activate",
|
||||||
|
"activate.csh",
|
||||||
|
"activate.fish",
|
||||||
|
"Activate",
|
||||||
|
"Activate.bat",
|
||||||
|
"Activate.ps1",
|
||||||
|
),
|
||||||
|
)
|
||||||
def test_ignored_virtualenvs_norecursedirs_precedence(
|
def test_ignored_virtualenvs_norecursedirs_precedence(
|
||||||
self, pytester: Pytester
|
self, pytester: Pytester, fname: str
|
||||||
) -> None:
|
) -> None:
|
||||||
|
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
|
||||||
# norecursedirs takes priority
|
# norecursedirs takes priority
|
||||||
ensure_file(pytester.path / ".virtual" / "pyvenv.cfg")
|
ensure_file(pytester.path / ".virtual" / bindir / fname)
|
||||||
testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py")
|
testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py")
|
||||||
testfile.write_text("def test_hello(): pass", encoding="utf-8")
|
testfile.write_text("def test_hello(): pass", encoding="utf-8")
|
||||||
result = pytester.runpytest("--collect-in-virtualenv")
|
result = pytester.runpytest("--collect-in-virtualenv")
|
||||||
|
@ -180,13 +204,27 @@ class TestCollectFS:
|
||||||
result = pytester.runpytest("--collect-in-virtualenv", ".virtual")
|
result = pytester.runpytest("--collect-in-virtualenv", ".virtual")
|
||||||
assert "test_invenv" in result.stdout.str()
|
assert "test_invenv" in result.stdout.str()
|
||||||
|
|
||||||
def test__in_venv(self, pytester: Pytester) -> None:
|
@pytest.mark.parametrize(
|
||||||
|
"fname",
|
||||||
|
(
|
||||||
|
"activate",
|
||||||
|
"activate.csh",
|
||||||
|
"activate.fish",
|
||||||
|
"Activate",
|
||||||
|
"Activate.bat",
|
||||||
|
"Activate.ps1",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test__in_venv(self, pytester: Pytester, fname: str) -> None:
|
||||||
"""Directly test the virtual env detection function"""
|
"""Directly test the virtual env detection function"""
|
||||||
# no pyvenv.cfg, not a virtualenv
|
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
|
||||||
|
# no bin/activate, not a virtualenv
|
||||||
base_path = pytester.mkdir("venv")
|
base_path = pytester.mkdir("venv")
|
||||||
assert _in_venv(base_path) is False
|
assert _in_venv(base_path) is False
|
||||||
# with pyvenv.cfg, totally a virtualenv
|
# with bin/activate, totally a virtualenv
|
||||||
base_path.joinpath("pyvenv.cfg").touch()
|
bin_path = base_path.joinpath(bindir)
|
||||||
|
bin_path.mkdir()
|
||||||
|
bin_path.joinpath(fname).touch()
|
||||||
assert _in_venv(base_path) is True
|
assert _in_venv(base_path) is True
|
||||||
|
|
||||||
def test_custom_norecursedirs(self, pytester: Pytester) -> None:
|
def test_custom_norecursedirs(self, pytester: Pytester) -> None:
|
||||||
|
@ -238,14 +276,14 @@ class TestCollectFS:
|
||||||
# collects the tests
|
# collects the tests
|
||||||
for dirname in ("a", "b", "c"):
|
for dirname in ("a", "b", "c"):
|
||||||
items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname))
|
items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname))
|
||||||
assert [x.name for x in items] == [f"test_{dirname}"]
|
assert [x.name for x in items] == ["test_%s" % dirname]
|
||||||
|
|
||||||
# changing cwd to each subdirectory and running pytest without
|
# changing cwd to each subdirectory and running pytest without
|
||||||
# arguments collects the tests in that directory normally
|
# arguments collects the tests in that directory normally
|
||||||
for dirname in ("a", "b", "c"):
|
for dirname in ("a", "b", "c"):
|
||||||
monkeypatch.chdir(pytester.path.joinpath(dirname))
|
monkeypatch.chdir(pytester.path.joinpath(dirname))
|
||||||
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] == ["test_%s" % dirname]
|
||||||
|
|
||||||
def test_missing_permissions_on_unselected_directory_doesnt_crash(
|
def test_missing_permissions_on_unselected_directory_doesnt_crash(
|
||||||
self, pytester: Pytester
|
self, pytester: Pytester
|
||||||
|
@ -552,7 +590,7 @@ class TestSession:
|
||||||
def test_collect_custom_nodes_multi_id(self, pytester: Pytester) -> None:
|
def test_collect_custom_nodes_multi_id(self, pytester: Pytester) -> None:
|
||||||
p = pytester.makepyfile("def test_func(): pass")
|
p = pytester.makepyfile("def test_func(): pass")
|
||||||
pytester.makeconftest(
|
pytester.makeconftest(
|
||||||
f"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
class SpecialItem(pytest.Item):
|
class SpecialItem(pytest.Item):
|
||||||
def runtest(self):
|
def runtest(self):
|
||||||
|
@ -561,9 +599,10 @@ class TestSession:
|
||||||
def collect(self):
|
def collect(self):
|
||||||
return [SpecialItem.from_parent(name="check", parent=self)]
|
return [SpecialItem.from_parent(name="check", parent=self)]
|
||||||
def pytest_collect_file(file_path, parent):
|
def pytest_collect_file(file_path, parent):
|
||||||
if file_path.name == {p.name!r}:
|
if file_path.name == %r:
|
||||||
return SpecialFile.from_parent(path=file_path, parent=parent)
|
return SpecialFile.from_parent(path=file_path, parent=parent)
|
||||||
"""
|
"""
|
||||||
|
% p.name
|
||||||
)
|
)
|
||||||
id = p.name
|
id = p.name
|
||||||
|
|
||||||
|
@ -841,7 +880,7 @@ def test_matchnodes_two_collections_same_file(pytester: Pytester) -> None:
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stdout.fnmatch_lines(["*2 passed*"])
|
result.stdout.fnmatch_lines(["*2 passed*"])
|
||||||
res = pytester.runpytest(f"{p.name}::item2")
|
res = pytester.runpytest("%s::item2" % p.name)
|
||||||
res.stdout.fnmatch_lines(["*1 passed*"])
|
res.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
|
||||||
|
|
||||||
|
@ -1423,7 +1462,7 @@ def test_collect_symlink_out_of_tree(pytester: Pytester) -> None:
|
||||||
symlink_to_sub = out_of_tree.joinpath("symlink_to_sub")
|
symlink_to_sub = out_of_tree.joinpath("symlink_to_sub")
|
||||||
symlink_or_skip(sub, symlink_to_sub)
|
symlink_or_skip(sub, symlink_to_sub)
|
||||||
os.chdir(sub)
|
os.chdir(sub)
|
||||||
result = pytester.runpytest("-vs", f"--rootdir={sub}", symlink_to_sub)
|
result = pytester.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub)
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
# Should not contain "sub/"!
|
# Should not contain "sub/"!
|
||||||
|
|
|
@ -64,12 +64,13 @@ class TestParseIni:
|
||||||
p1 = pytester.makepyfile("def test(): pass")
|
p1 = pytester.makepyfile("def test(): pass")
|
||||||
pytester.makefile(
|
pytester.makefile(
|
||||||
".cfg",
|
".cfg",
|
||||||
setup=f"""
|
setup="""
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
testpaths={p1.name}
|
testpaths=%s
|
||||||
[pytest]
|
[pytest]
|
||||||
testpaths=ignored
|
testpaths=ignored
|
||||||
""",
|
"""
|
||||||
|
% p1.name,
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["configfile: setup.cfg", "* 1 passed in *"])
|
result.stdout.fnmatch_lines(["configfile: setup.cfg", "* 1 passed in *"])
|
||||||
|
@ -834,10 +835,11 @@ class TestConfigAPI:
|
||||||
)
|
)
|
||||||
if str_val != "no-ini":
|
if str_val != "no-ini":
|
||||||
pytester.makeini(
|
pytester.makeini(
|
||||||
f"""
|
"""
|
||||||
[pytest]
|
[pytest]
|
||||||
strip={str_val}
|
strip=%s
|
||||||
"""
|
"""
|
||||||
|
% str_val
|
||||||
)
|
)
|
||||||
config = pytester.parseconfig()
|
config = pytester.parseconfig()
|
||||||
assert config.getini("strip") is bool_val
|
assert config.getini("strip") is bool_val
|
||||||
|
@ -1285,8 +1287,8 @@ def test_invalid_options_show_extra_information(pytester: Pytester) -> None:
|
||||||
result.stderr.fnmatch_lines(
|
result.stderr.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*error: unrecognized arguments: --invalid-option*",
|
"*error: unrecognized arguments: --invalid-option*",
|
||||||
"* inifile: {}*".format(pytester.path.joinpath("tox.ini")),
|
"* inifile: %s*" % pytester.path.joinpath("tox.ini"),
|
||||||
f"* rootdir: {pytester.path}*",
|
"* rootdir: %s*" % pytester.path,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1418,8 +1420,8 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
|
||||||
def test_get_plugin_specs_as_list() -> None:
|
def test_get_plugin_specs_as_list() -> None:
|
||||||
def exp_match(val: object) -> str:
|
def exp_match(val: object) -> str:
|
||||||
return (
|
return (
|
||||||
f"Plugins may be specified as a sequence or a ','-separated string "
|
"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %s"
|
||||||
f"of plugin names. Got: {re.escape(repr(val))}"
|
% re.escape(repr(val))
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(pytest.UsageError, match=exp_match({"foo"})):
|
with pytest.raises(pytest.UsageError, match=exp_match({"foo"})):
|
||||||
|
@ -1832,10 +1834,10 @@ class TestOverrideIniArgs:
|
||||||
self, monkeypatch: MonkeyPatch, _config_for_test, _sys_snapshot
|
self, monkeypatch: MonkeyPatch, _config_for_test, _sys_snapshot
|
||||||
) -> None:
|
) -> None:
|
||||||
cache_dir = ".custom_cache"
|
cache_dir = ".custom_cache"
|
||||||
monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}")
|
monkeypatch.setenv("PYTEST_ADDOPTS", "-o cache_dir=%s" % cache_dir)
|
||||||
config = _config_for_test
|
config = _config_for_test
|
||||||
config._preparse([], addopts=True)
|
config._preparse([], addopts=True)
|
||||||
assert config._override_ini == [f"cache_dir={cache_dir}"]
|
assert config._override_ini == ["cache_dir=%s" % cache_dir]
|
||||||
|
|
||||||
def test_addopts_from_env_not_concatenated(
|
def test_addopts_from_env_not_concatenated(
|
||||||
self, monkeypatch: MonkeyPatch, _config_for_test
|
self, monkeypatch: MonkeyPatch, _config_for_test
|
||||||
|
@ -2043,7 +2045,7 @@ def test_invocation_args(pytester: Pytester) -> None:
|
||||||
)
|
)
|
||||||
def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None:
|
def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None:
|
||||||
p = pytester.makepyfile("def test(): pass")
|
p = pytester.makepyfile("def test(): pass")
|
||||||
result = pytester.runpytest(str(p), f"-pno:{plugin}")
|
result = pytester.runpytest(str(p), "-pno:%s" % plugin)
|
||||||
|
|
||||||
if plugin == "python":
|
if plugin == "python":
|
||||||
assert result.ret == ExitCode.USAGE_ERROR
|
assert result.ret == ExitCode.USAGE_ERROR
|
||||||
|
@ -2060,7 +2062,7 @@ def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None
|
||||||
result.stdout.fnmatch_lines(["* 1 passed in *"])
|
result.stdout.fnmatch_lines(["* 1 passed in *"])
|
||||||
|
|
||||||
p = pytester.makepyfile("def test(): assert 0")
|
p = pytester.makepyfile("def test(): assert 0")
|
||||||
result = pytester.runpytest(str(p), f"-pno:{plugin}")
|
result = pytester.runpytest(str(p), "-pno:%s" % plugin)
|
||||||
assert result.ret == ExitCode.TESTS_FAILED
|
assert result.ret == ExitCode.TESTS_FAILED
|
||||||
if plugin != "terminal":
|
if plugin != "terminal":
|
||||||
result.stdout.fnmatch_lines(["* 1 failed in *"])
|
result.stdout.fnmatch_lines(["* 1 failed in *"])
|
||||||
|
|
|
@ -279,7 +279,7 @@ def test_conftest_confcutdir(pytester: Pytester) -> None:
|
||||||
),
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("-h", f"--confcutdir={x}", x)
|
result = pytester.runpytest("-h", "--confcutdir=%s" % x, x)
|
||||||
result.stdout.fnmatch_lines(["*--xyz*"])
|
result.stdout.fnmatch_lines(["*--xyz*"])
|
||||||
result.stdout.no_fnmatch_line("*warning: could not load initial*")
|
result.stdout.no_fnmatch_line("*warning: could not load initial*")
|
||||||
|
|
||||||
|
@ -379,7 +379,7 @@ def test_conftest_symlink_files(pytester: Pytester) -> None:
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
pytester.makepyfile(**{f"real/{k}": v for k, v in source.items()})
|
pytester.makepyfile(**{"real/%s" % k: v for k, v in source.items()})
|
||||||
|
|
||||||
# Create a build directory that contains symlinks to actual files
|
# Create a build directory that contains symlinks to actual files
|
||||||
# but doesn't symlink actual directories.
|
# but doesn't symlink actual directories.
|
||||||
|
@ -401,7 +401,7 @@ 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)."""
|
||||||
pytester.path.joinpath("JenkinsRoot/test").mkdir(parents=True)
|
pytester.path.joinpath("JenkinsRoot/test").mkdir(parents=True)
|
||||||
source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""}
|
source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""}
|
||||||
pytester.makepyfile(**{f"JenkinsRoot/{k}": v for k, v in source.items()})
|
pytester.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()})
|
||||||
|
|
||||||
os.chdir(pytester.path.joinpath("jenkinsroot/test"))
|
os.chdir(pytester.path.joinpath("jenkinsroot/test"))
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
|
@ -637,9 +637,9 @@ class TestConftestVisibility:
|
||||||
) -> None:
|
) -> None:
|
||||||
"""#616"""
|
"""#616"""
|
||||||
dirs = self._setup_tree(pytester)
|
dirs = self._setup_tree(pytester)
|
||||||
print(f"pytest run in cwd: {dirs[chdir].relative_to(pytester.path)}")
|
print("pytest run in cwd: %s" % (dirs[chdir].relative_to(pytester.path)))
|
||||||
print(f"pytestarg : {testarg}")
|
print("pytestarg : %s" % testarg)
|
||||||
print(f"expected pass : {expect_ntests_passed}")
|
print("expected pass : %s" % expect_ntests_passed)
|
||||||
os.chdir(dirs[chdir])
|
os.chdir(dirs[chdir])
|
||||||
reprec = pytester.inline_run(
|
reprec = pytester.inline_run(
|
||||||
testarg,
|
testarg,
|
||||||
|
@ -698,7 +698,7 @@ def test_search_conftest_up_to_inifile(
|
||||||
|
|
||||||
args = [str(src)]
|
args = [str(src)]
|
||||||
if confcutdir:
|
if confcutdir:
|
||||||
args = [f"--confcutdir={root.joinpath(confcutdir)}"]
|
args = ["--confcutdir=%s" % root.joinpath(confcutdir)]
|
||||||
result = pytester.runpytest(*args)
|
result = pytester.runpytest(*args)
|
||||||
match = ""
|
match = ""
|
||||||
if passed:
|
if passed:
|
||||||
|
|
|
@ -222,7 +222,7 @@ class TestPDB:
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"--pdb {p1}")
|
child = pytester.spawn_pytest("--pdb %s" % p1)
|
||||||
child.expect("captured stdout")
|
child.expect("captured stdout")
|
||||||
child.expect("get rekt")
|
child.expect("get rekt")
|
||||||
child.expect("captured stderr")
|
child.expect("captured stderr")
|
||||||
|
@ -247,7 +247,7 @@ class TestPDB:
|
||||||
assert False
|
assert False
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"--pdb {p1}")
|
child = pytester.spawn_pytest("--pdb %s" % p1)
|
||||||
child.expect("Pdb")
|
child.expect("Pdb")
|
||||||
output = child.before.decode("utf8")
|
output = child.before.decode("utf8")
|
||||||
child.sendeof()
|
child.sendeof()
|
||||||
|
@ -284,7 +284,7 @@ class TestPDB:
|
||||||
assert False
|
assert False
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"--show-capture=all --pdb -p no:logging {p1}")
|
child = pytester.spawn_pytest("--show-capture=all --pdb -p no:logging %s" % p1)
|
||||||
child.expect("get rekt")
|
child.expect("get rekt")
|
||||||
output = child.before.decode("utf8")
|
output = child.before.decode("utf8")
|
||||||
assert "captured log" not in output
|
assert "captured log" not in output
|
||||||
|
@ -304,7 +304,7 @@ class TestPDB:
|
||||||
pytest.raises(ValueError, globalfunc)
|
pytest.raises(ValueError, globalfunc)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"--pdb {p1}")
|
child = pytester.spawn_pytest("--pdb %s" % p1)
|
||||||
child.expect(".*def test_1")
|
child.expect(".*def test_1")
|
||||||
child.expect(".*pytest.raises.*globalfunc")
|
child.expect(".*pytest.raises.*globalfunc")
|
||||||
child.expect("Pdb")
|
child.expect("Pdb")
|
||||||
|
@ -321,7 +321,7 @@ class TestPDB:
|
||||||
xxx
|
xxx
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"--pdb {p1}")
|
child = pytester.spawn_pytest("--pdb %s" % p1)
|
||||||
# child.expect(".*import pytest.*")
|
# child.expect(".*import pytest.*")
|
||||||
child.expect("Pdb")
|
child.expect("Pdb")
|
||||||
child.sendline("c")
|
child.sendline("c")
|
||||||
|
@ -336,7 +336,7 @@ class TestPDB:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
p1 = pytester.makepyfile("def test_func(): pass")
|
p1 = pytester.makepyfile("def test_func(): pass")
|
||||||
child = pytester.spawn_pytest(f"--pdb {p1}")
|
child = pytester.spawn_pytest("--pdb %s" % p1)
|
||||||
child.expect("Pdb")
|
child.expect("Pdb")
|
||||||
|
|
||||||
# INTERNALERROR is only displayed once via terminal reporter.
|
# INTERNALERROR is only displayed once via terminal reporter.
|
||||||
|
@ -462,7 +462,7 @@ class TestPDB:
|
||||||
assert 0
|
assert 0
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"--pdb {p1!s}")
|
child = pytester.spawn_pytest("--pdb %s" % str(p1))
|
||||||
child.send("caplog.record_tuples\n")
|
child.send("caplog.record_tuples\n")
|
||||||
child.expect_exact(
|
child.expect_exact(
|
||||||
"[('test_pdb_with_caplog_on_pdb_invocation', 30, 'some_warning')]"
|
"[('test_pdb_with_caplog_on_pdb_invocation', 30, 'some_warning')]"
|
||||||
|
@ -502,7 +502,7 @@ class TestPDB:
|
||||||
'''
|
'''
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"--doctest-modules --pdb {p1}")
|
child = pytester.spawn_pytest("--doctest-modules --pdb %s" % p1)
|
||||||
child.expect("Pdb")
|
child.expect("Pdb")
|
||||||
|
|
||||||
assert "UNEXPECTED EXCEPTION: AssertionError()" in child.before.decode("utf8")
|
assert "UNEXPECTED EXCEPTION: AssertionError()" in child.before.decode("utf8")
|
||||||
|
@ -529,7 +529,7 @@ class TestPDB:
|
||||||
)
|
)
|
||||||
# NOTE: does not use pytest.set_trace, but Python's patched pdb,
|
# NOTE: does not use pytest.set_trace, but Python's patched pdb,
|
||||||
# therefore "-s" is required.
|
# therefore "-s" is required.
|
||||||
child = pytester.spawn_pytest(f"--doctest-modules --pdb -s {p1}")
|
child = pytester.spawn_pytest("--doctest-modules --pdb -s %s" % p1)
|
||||||
child.expect("Pdb")
|
child.expect("Pdb")
|
||||||
child.sendline("q")
|
child.sendline("q")
|
||||||
rest = child.read().decode("utf8")
|
rest = child.read().decode("utf8")
|
||||||
|
@ -622,7 +622,7 @@ class TestPDB:
|
||||||
pytest.fail("expected_failure")
|
pytest.fail("expected_failure")
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"--pdbcls=mytest:CustomPdb {p1!s}")
|
child = pytester.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1))
|
||||||
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
|
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
|
||||||
child.expect(r"\n\(Pdb")
|
child.expect(r"\n\(Pdb")
|
||||||
child.sendline("debug foo()")
|
child.sendline("debug foo()")
|
||||||
|
@ -659,7 +659,7 @@ class TestPDB:
|
||||||
pytest.set_trace()
|
pytest.set_trace()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
child = pytester.spawn_pytest(f"-s {p1}")
|
child = pytester.spawn_pytest("-s %s" % p1)
|
||||||
child.expect(r">>> PDB set_trace >>>")
|
child.expect(r">>> PDB set_trace >>>")
|
||||||
child.expect("Pdb")
|
child.expect("Pdb")
|
||||||
child.sendline("c")
|
child.sendline("c")
|
||||||
|
@ -915,7 +915,7 @@ class TestPDB:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
monkeypatch.setenv("PYTHONPATH", str(pytester.path))
|
monkeypatch.setenv("PYTHONPATH", str(pytester.path))
|
||||||
child = pytester.spawn_pytest(f"--pdbcls=custom_pdb:CustomPdb {p1!s}")
|
child = pytester.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1))
|
||||||
|
|
||||||
child.expect("__init__")
|
child.expect("__init__")
|
||||||
child.expect("custom set_trace>")
|
child.expect("custom set_trace>")
|
||||||
|
@ -1209,7 +1209,8 @@ def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> Non
|
||||||
child.expect("Pdb")
|
child.expect("Pdb")
|
||||||
before = child.before.decode("utf8")
|
before = child.before.decode("utf8")
|
||||||
assert (
|
assert (
|
||||||
f"> PDB set_trace (IO-capturing turned off for fixture {fixture}) >" in before
|
"> PDB set_trace (IO-capturing turned off for fixture %s) >" % (fixture)
|
||||||
|
in before
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test that capturing is really suspended.
|
# Test that capturing is really suspended.
|
||||||
|
@ -1225,7 +1226,7 @@ def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> Non
|
||||||
TestPDB.flush(child)
|
TestPDB.flush(child)
|
||||||
assert child.exitstatus == 0
|
assert child.exitstatus == 0
|
||||||
assert "= 1 passed in" in rest
|
assert "= 1 passed in" in rest
|
||||||
assert f"> PDB continue (IO-capturing resumed for fixture {fixture}) >" in rest
|
assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest
|
||||||
|
|
||||||
|
|
||||||
def test_pdbcls_via_local_module(pytester: Pytester) -> None:
|
def test_pdbcls_via_local_module(pytester: Pytester) -> None:
|
||||||
|
|
|
@ -1157,7 +1157,7 @@ class TestDoctestSkips:
|
||||||
pytester.maketxtfile(doctest)
|
pytester.maketxtfile(doctest)
|
||||||
else:
|
else:
|
||||||
assert mode == "module"
|
assert mode == "module"
|
||||||
pytester.makepyfile(f'"""\n{doctest}"""')
|
pytester.makepyfile('"""\n%s"""' % doctest)
|
||||||
|
|
||||||
return makeit
|
return makeit
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ def test_timeout(pytester: Pytester, enabled: bool) -> None:
|
||||||
result = pytester.runpytest_subprocess(*args)
|
result = pytester.runpytest_subprocess(*args)
|
||||||
tb_output = "most recent call first"
|
tb_output = "most recent call first"
|
||||||
if enabled:
|
if enabled:
|
||||||
result.stderr.fnmatch_lines([f"*{tb_output}*"])
|
result.stderr.fnmatch_lines(["*%s*" % tb_output])
|
||||||
else:
|
else:
|
||||||
assert tb_output not in result.stderr.str()
|
assert tb_output not in result.stderr.str()
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
|
|
@ -12,7 +12,7 @@ def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stdout.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"])
|
result.stdout.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"])
|
||||||
if pytestconfig.pluginmanager.list_plugin_distinfo():
|
if pytestconfig.pluginmanager.list_plugin_distinfo():
|
||||||
result.stdout.fnmatch_lines(["*registered third-party plugins:", "*at*"])
|
result.stdout.fnmatch_lines(["*setuptools registered plugins:", "*at*"])
|
||||||
|
|
||||||
|
|
||||||
def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
|
def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import platform
|
import platform
|
||||||
|
@ -43,7 +42,7 @@ class RunAndParse:
|
||||||
if family:
|
if family:
|
||||||
args = ("-o", "junit_family=" + family, *args)
|
args = ("-o", "junit_family=" + family, *args)
|
||||||
xml_path = self.pytester.path.joinpath("junit.xml")
|
xml_path = self.pytester.path.joinpath("junit.xml")
|
||||||
result = self.pytester.runpytest(f"--junitxml={xml_path}", *args)
|
result = self.pytester.runpytest("--junitxml=%s" % xml_path, *args)
|
||||||
if family == "xunit2":
|
if family == "xunit2":
|
||||||
with xml_path.open(encoding="utf-8") as f:
|
with xml_path.open(encoding="utf-8") as f:
|
||||||
self.schema.validate(f)
|
self.schema.validate(f)
|
||||||
|
@ -219,11 +218,11 @@ class TestPython:
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
start_time = datetime.now(timezone.utc)
|
start_time = datetime.now()
|
||||||
result, dom = run_and_parse(family=xunit_family)
|
result, dom = run_and_parse(family=xunit_family)
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z")
|
timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f")
|
||||||
assert start_time <= timestamp < datetime.now(timezone.utc)
|
assert start_time <= timestamp < datetime.now()
|
||||||
|
|
||||||
def test_timing_function(
|
def test_timing_function(
|
||||||
self, pytester: Pytester, run_and_parse: RunAndParse, mock_timing
|
self, pytester: Pytester, run_and_parse: RunAndParse, mock_timing
|
||||||
|
@ -519,7 +518,7 @@ class TestPython:
|
||||||
)
|
)
|
||||||
|
|
||||||
result, dom = run_and_parse(
|
result, dom = run_and_parse(
|
||||||
"-o", f"junit_logging={junit_logging}", family=xunit_family
|
"-o", "junit_logging=%s" % junit_logging, family=xunit_family
|
||||||
)
|
)
|
||||||
assert result.ret, "Expected ret > 0"
|
assert result.ret, "Expected ret > 0"
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
|
@ -604,11 +603,11 @@ class TestPython:
|
||||||
for index, char in enumerate("<&'"):
|
for index, char in enumerate("<&'"):
|
||||||
tnode = node.find_nth_by_tag("testcase", index)
|
tnode = node.find_nth_by_tag("testcase", index)
|
||||||
tnode.assert_attr(
|
tnode.assert_attr(
|
||||||
classname="test_failure_escape", name=f"test_func[{char}]"
|
classname="test_failure_escape", name="test_func[%s]" % char
|
||||||
)
|
)
|
||||||
sysout = tnode.find_first_by_tag("system-out")
|
sysout = tnode.find_first_by_tag("system-out")
|
||||||
text = sysout.text
|
text = sysout.text
|
||||||
assert f"{char}\n" in text
|
assert "%s\n" % char in text
|
||||||
|
|
||||||
@parametrize_families
|
@parametrize_families
|
||||||
def test_junit_prefixing(
|
def test_junit_prefixing(
|
||||||
|
@ -693,7 +692,7 @@ class TestPython:
|
||||||
assert 0
|
assert 0
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
|
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
tnode = node.find_first_by_tag("testcase")
|
tnode = node.find_first_by_tag("testcase")
|
||||||
if junit_logging in ["system-err", "out-err", "all"]:
|
if junit_logging in ["system-err", "out-err", "all"]:
|
||||||
|
@ -763,12 +762,13 @@ class TestPython:
|
||||||
def test_unicode(self, pytester: Pytester, run_and_parse: RunAndParse) -> None:
|
def test_unicode(self, pytester: Pytester, run_and_parse: RunAndParse) -> None:
|
||||||
value = "hx\xc4\x85\xc4\x87\n"
|
value = "hx\xc4\x85\xc4\x87\n"
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
f"""\
|
"""\
|
||||||
# coding: latin1
|
# coding: latin1
|
||||||
def test_hello():
|
def test_hello():
|
||||||
print({value!r})
|
print(%r)
|
||||||
assert 0
|
assert 0
|
||||||
"""
|
"""
|
||||||
|
% value
|
||||||
)
|
)
|
||||||
result, dom = run_and_parse()
|
result, dom = run_and_parse()
|
||||||
assert result.ret == 1
|
assert result.ret == 1
|
||||||
|
@ -803,7 +803,7 @@ class TestPython:
|
||||||
print('hello-stdout')
|
print('hello-stdout')
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
|
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
pnode = node.find_first_by_tag("testcase")
|
pnode = node.find_first_by_tag("testcase")
|
||||||
if junit_logging == "no":
|
if junit_logging == "no":
|
||||||
|
@ -827,7 +827,7 @@ class TestPython:
|
||||||
sys.stderr.write('hello-stderr')
|
sys.stderr.write('hello-stderr')
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
|
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
pnode = node.find_first_by_tag("testcase")
|
pnode = node.find_first_by_tag("testcase")
|
||||||
if junit_logging == "no":
|
if junit_logging == "no":
|
||||||
|
@ -856,7 +856,7 @@ class TestPython:
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
|
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
pnode = node.find_first_by_tag("testcase")
|
pnode = node.find_first_by_tag("testcase")
|
||||||
if junit_logging == "no":
|
if junit_logging == "no":
|
||||||
|
@ -886,7 +886,7 @@ class TestPython:
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
|
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
pnode = node.find_first_by_tag("testcase")
|
pnode = node.find_first_by_tag("testcase")
|
||||||
if junit_logging == "no":
|
if junit_logging == "no":
|
||||||
|
@ -917,7 +917,7 @@ class TestPython:
|
||||||
sys.stdout.write('hello-stdout call')
|
sys.stdout.write('hello-stdout call')
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result, dom = run_and_parse("-o", f"junit_logging={junit_logging}")
|
result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
|
||||||
node = dom.find_first_by_tag("testsuite")
|
node = dom.find_first_by_tag("testsuite")
|
||||||
pnode = node.find_first_by_tag("testcase")
|
pnode = node.find_first_by_tag("testcase")
|
||||||
if junit_logging == "no":
|
if junit_logging == "no":
|
||||||
|
@ -1011,7 +1011,7 @@ def test_nullbyte(pytester: Pytester, junit_logging: str) -> None:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
xmlf = pytester.path.joinpath("junit.xml")
|
xmlf = pytester.path.joinpath("junit.xml")
|
||||||
pytester.runpytest(f"--junitxml={xmlf}", "-o", f"junit_logging={junit_logging}")
|
pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging)
|
||||||
text = xmlf.read_text(encoding="utf-8")
|
text = xmlf.read_text(encoding="utf-8")
|
||||||
assert "\x00" not in text
|
assert "\x00" not in text
|
||||||
if junit_logging == "system-out":
|
if junit_logging == "system-out":
|
||||||
|
@ -1033,7 +1033,7 @@ def test_nullbyte_replace(pytester: Pytester, junit_logging: str) -> None:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
xmlf = pytester.path.joinpath("junit.xml")
|
xmlf = pytester.path.joinpath("junit.xml")
|
||||||
pytester.runpytest(f"--junitxml={xmlf}", "-o", f"junit_logging={junit_logging}")
|
pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging)
|
||||||
text = xmlf.read_text(encoding="utf-8")
|
text = xmlf.read_text(encoding="utf-8")
|
||||||
if junit_logging == "system-out":
|
if junit_logging == "system-out":
|
||||||
assert "#x0" in text
|
assert "#x0" in text
|
||||||
|
@ -1069,9 +1069,9 @@ def test_invalid_xml_escape() -> None:
|
||||||
for i in invalid:
|
for i in invalid:
|
||||||
got = bin_xml_escape(chr(i))
|
got = bin_xml_escape(chr(i))
|
||||||
if i <= 0xFF:
|
if i <= 0xFF:
|
||||||
expected = f"#x{i:02X}"
|
expected = "#x%02X" % i
|
||||||
else:
|
else:
|
||||||
expected = f"#x{i:04X}"
|
expected = "#x%04X" % i
|
||||||
assert got == expected
|
assert got == expected
|
||||||
for i in valid:
|
for i in valid:
|
||||||
assert chr(i) == bin_xml_escape(chr(i))
|
assert chr(i) == bin_xml_escape(chr(i))
|
||||||
|
@ -1746,7 +1746,7 @@ def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430(
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result, dom = run_and_parse(
|
result, dom = run_and_parse(
|
||||||
"-o", f"junit_logging={junit_logging}", family=xunit_family
|
"-o", "junit_logging=%s" % junit_logging, family=xunit_family
|
||||||
)
|
)
|
||||||
assert result.ret == 1
|
assert result.ret == 1
|
||||||
node = dom.find_first_by_tag("testcase")
|
node = dom.find_first_by_tag("testcase")
|
||||||
|
|
|
@ -81,7 +81,7 @@ def test_tmpdir_always_is_realpath(pytester: pytest.Pytester) -> None:
|
||||||
assert os.path.realpath(str(tmpdir)) == str(tmpdir)
|
assert os.path.realpath(str(tmpdir)) == str(tmpdir)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest("-s", p, f"--basetemp={linktemp}/bt")
|
result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp)
|
||||||
assert not result.ret
|
assert not result.ret
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -233,54 +233,6 @@ def test_mark_option(
|
||||||
assert passed_str == expected_passed
|
assert passed_str == expected_passed
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("expr", "expected_passed"),
|
|
||||||
[
|
|
||||||
("car(color='red')", ["test_one"]),
|
|
||||||
("car(color='red') or car(color='blue')", ["test_one", "test_two"]),
|
|
||||||
("car and not car(temp=5)", ["test_one", "test_three"]),
|
|
||||||
("car(temp=4)", ["test_one"]),
|
|
||||||
("car(temp=4) or car(temp=5)", ["test_one", "test_two"]),
|
|
||||||
("car(temp=4) and car(temp=5)", []),
|
|
||||||
("car(temp=-5)", ["test_three"]),
|
|
||||||
("car(ac=True)", ["test_one"]),
|
|
||||||
("car(ac=False)", ["test_two"]),
|
|
||||||
("car(ac=None)", ["test_three"]), # test NOT_NONE_SENTINEL
|
|
||||||
],
|
|
||||||
ids=str,
|
|
||||||
)
|
|
||||||
def test_mark_option_with_kwargs(
|
|
||||||
expr: str, expected_passed: list[str | None], pytester: Pytester
|
|
||||||
) -> None:
|
|
||||||
pytester.makepyfile(
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
@pytest.mark.car
|
|
||||||
@pytest.mark.car(ac=True)
|
|
||||||
@pytest.mark.car(temp=4)
|
|
||||||
@pytest.mark.car(color="red")
|
|
||||||
def test_one():
|
|
||||||
pass
|
|
||||||
@pytest.mark.car
|
|
||||||
@pytest.mark.car(ac=False)
|
|
||||||
@pytest.mark.car(temp=5)
|
|
||||||
@pytest.mark.car(color="blue")
|
|
||||||
def test_two():
|
|
||||||
pass
|
|
||||||
@pytest.mark.car
|
|
||||||
@pytest.mark.car(ac=None)
|
|
||||||
@pytest.mark.car(temp=-5)
|
|
||||||
def test_three():
|
|
||||||
pass
|
|
||||||
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
rec = pytester.inline_run("-m", expr)
|
|
||||||
passed, skipped, fail = rec.listoutcomes()
|
|
||||||
passed_str = [x.nodeid.split("::")[-1] for x in passed]
|
|
||||||
assert passed_str == expected_passed
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("expr", "expected_passed"),
|
("expr", "expected_passed"),
|
||||||
[("interface", ["test_interface"]), ("not interface", ["test_nointer"])],
|
[("interface", ["test_interface"]), ("not interface", ["test_nointer"])],
|
||||||
|
@ -420,10 +372,6 @@ def test_parametrize_with_module(pytester: Pytester) -> None:
|
||||||
"not or",
|
"not or",
|
||||||
"at column 5: expected not OR left parenthesis OR identifier; got or",
|
"at column 5: expected not OR left parenthesis OR identifier; got or",
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"nonexistent_mark(non_supported='kwarg')",
|
|
||||||
"Keyword expressions do not support call parameters",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_keyword_option_wrong_arguments(
|
def test_keyword_option_wrong_arguments(
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from _pytest.mark import MarkMatcher
|
|
||||||
from _pytest.mark.expression import Expression
|
from _pytest.mark.expression import Expression
|
||||||
from _pytest.mark.expression import MatcherCall
|
|
||||||
from _pytest.mark.expression import ParseError
|
from _pytest.mark.expression import ParseError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def evaluate(input: str, matcher: Callable[[str], bool]) -> bool:
|
def evaluate(input: str, matcher: Callable[[str], bool]) -> bool:
|
||||||
return Expression.compile(input).evaluate(cast(MatcherCall, matcher))
|
return Expression.compile(input).evaluate(matcher)
|
||||||
|
|
||||||
|
|
||||||
def test_empty_is_false() -> None:
|
def test_empty_is_false() -> None:
|
||||||
|
@ -156,8 +153,6 @@ def test_syntax_errors(expr: str, column: int, message: str) -> None:
|
||||||
"1234",
|
"1234",
|
||||||
"1234abcd",
|
"1234abcd",
|
||||||
"1234and",
|
"1234and",
|
||||||
"1234or",
|
|
||||||
"1234not",
|
|
||||||
"notandor",
|
"notandor",
|
||||||
"not_and_or",
|
"not_and_or",
|
||||||
"not[and]or",
|
"not[and]or",
|
||||||
|
@ -200,120 +195,3 @@ def test_valid_idents(ident: str) -> None:
|
||||||
def test_invalid_idents(ident: str) -> None:
|
def test_invalid_idents(ident: str) -> None:
|
||||||
with pytest.raises(ParseError):
|
with pytest.raises(ParseError):
|
||||||
evaluate(ident, lambda ident: True)
|
evaluate(ident, lambda ident: True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"expr, expected_error_msg",
|
|
||||||
(
|
|
||||||
("mark(True=False)", "unexpected reserved python keyword `True`"),
|
|
||||||
("mark(def=False)", "unexpected reserved python keyword `def`"),
|
|
||||||
("mark(class=False)", "unexpected reserved python keyword `class`"),
|
|
||||||
("mark(if=False)", "unexpected reserved python keyword `if`"),
|
|
||||||
("mark(else=False)", "unexpected reserved python keyword `else`"),
|
|
||||||
("mark(valid=False, def=1)", "unexpected reserved python keyword `def`"),
|
|
||||||
("mark(1)", "not a valid python identifier 1"),
|
|
||||||
("mark(var:=False", "not a valid python identifier var:"),
|
|
||||||
("mark(1=2)", "not a valid python identifier 1"),
|
|
||||||
("mark(/=2)", "not a valid python identifier /"),
|
|
||||||
("mark(var==", "expected identifier; got ="),
|
|
||||||
("mark(var)", "expected =; got right parenthesis"),
|
|
||||||
("mark(var=none)", 'unexpected character/s "none"'),
|
|
||||||
("mark(var=1.1)", 'unexpected character/s "1.1"'),
|
|
||||||
("mark(var=')", """closing quote "'" is missing"""),
|
|
||||||
('mark(var=")', 'closing quote """ is missing'),
|
|
||||||
("""mark(var="')""", 'closing quote """ is missing'),
|
|
||||||
("""mark(var='")""", """closing quote "'" is missing"""),
|
|
||||||
(
|
|
||||||
r"mark(var='\hugo')",
|
|
||||||
r'escaping with "\\" not supported in marker expression',
|
|
||||||
),
|
|
||||||
("mark(empty_list=[])", r'unexpected character/s "\[\]"'),
|
|
||||||
("'str'", "expected not OR left parenthesis OR identifier; got string literal"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_invalid_kwarg_name_or_value(
|
|
||||||
expr: str, expected_error_msg: str, mark_matcher: MarkMatcher
|
|
||||||
) -> None:
|
|
||||||
with pytest.raises(ParseError, match=expected_error_msg):
|
|
||||||
assert evaluate(expr, mark_matcher)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def mark_matcher() -> MarkMatcher:
|
|
||||||
markers = [
|
|
||||||
pytest.mark.number_mark(a=1, b=2, c=3, d=999_999).mark,
|
|
||||||
pytest.mark.builtin_matchers_mark(x=True, y=False, z=None).mark,
|
|
||||||
pytest.mark.str_mark(
|
|
||||||
m="M", space="with space", empty="", aaאבגדcc="aaאבגדcc", אבגד="אבגד"
|
|
||||||
).mark,
|
|
||||||
]
|
|
||||||
|
|
||||||
return MarkMatcher.from_markers(markers)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"expr, expected",
|
|
||||||
(
|
|
||||||
# happy cases
|
|
||||||
("number_mark(a=1)", True),
|
|
||||||
("number_mark(b=2)", True),
|
|
||||||
("number_mark(a=1,b=2)", True),
|
|
||||||
("number_mark(a=1, b=2)", True),
|
|
||||||
("number_mark(d=999999)", True),
|
|
||||||
("number_mark(a = 1,b= 2, c = 3)", True),
|
|
||||||
# sad cases
|
|
||||||
("number_mark(a=6)", False),
|
|
||||||
("number_mark(b=6)", False),
|
|
||||||
("number_mark(a=1,b=6)", False),
|
|
||||||
("number_mark(a=6,b=2)", False),
|
|
||||||
("number_mark(a = 1,b= 2, c = 6)", False),
|
|
||||||
("number_mark(a='1')", False),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_keyword_expressions_with_numbers(
|
|
||||||
expr: str, expected: bool, mark_matcher: MarkMatcher
|
|
||||||
) -> None:
|
|
||||||
assert evaluate(expr, mark_matcher) is expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"expr, expected",
|
|
||||||
(
|
|
||||||
("builtin_matchers_mark(x=True)", True),
|
|
||||||
("builtin_matchers_mark(x=False)", False),
|
|
||||||
("builtin_matchers_mark(y=True)", False),
|
|
||||||
("builtin_matchers_mark(y=False)", True),
|
|
||||||
("builtin_matchers_mark(z=None)", True),
|
|
||||||
("builtin_matchers_mark(z=False)", False),
|
|
||||||
("builtin_matchers_mark(z=True)", False),
|
|
||||||
("builtin_matchers_mark(z=0)", False),
|
|
||||||
("builtin_matchers_mark(z=1)", False),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_builtin_matchers_keyword_expressions(
|
|
||||||
expr: str, expected: bool, mark_matcher: MarkMatcher
|
|
||||||
) -> None:
|
|
||||||
assert evaluate(expr, mark_matcher) is expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"expr, expected",
|
|
||||||
(
|
|
||||||
("str_mark(m='M')", True),
|
|
||||||
('str_mark(m="M")', True),
|
|
||||||
("str_mark(aaאבגדcc='aaאבגדcc')", True),
|
|
||||||
("str_mark(אבגד='אבגד')", True),
|
|
||||||
("str_mark(space='with space')", True),
|
|
||||||
("str_mark(empty='')", True),
|
|
||||||
('str_mark(empty="")', True),
|
|
||||||
("str_mark(m='wrong')", False),
|
|
||||||
("str_mark(aaאבגדcc='wrong')", False),
|
|
||||||
("str_mark(אבגד='wrong')", False),
|
|
||||||
("str_mark(m='')", False),
|
|
||||||
('str_mark(m="")', False),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_str_keyword_expressions(
|
|
||||||
expr: str, expected: bool, mark_matcher: MarkMatcher
|
|
||||||
) -> None:
|
|
||||||
assert evaluate(expr, mark_matcher) is expected
|
|
||||||
|
|
|
@ -442,7 +442,7 @@ def test_syspath_prepend_with_namespace_packages(
|
||||||
lib = ns.joinpath(dirname)
|
lib = ns.joinpath(dirname)
|
||||||
lib.mkdir()
|
lib.mkdir()
|
||||||
lib.joinpath("__init__.py").write_text(
|
lib.joinpath("__init__.py").write_text(
|
||||||
f"def check(): return {dirname!r}", encoding="utf-8"
|
"def check(): return %r" % dirname, encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.syspath_prepend("hello")
|
monkeypatch.syspath_prepend("hello")
|
||||||
|
|
|
@ -171,7 +171,7 @@ class TestPaste:
|
||||||
assert type(data) is bytes
|
assert type(data) is bytes
|
||||||
lexer = "text"
|
lexer = "text"
|
||||||
assert url == "https://bpa.st"
|
assert url == "https://bpa.st"
|
||||||
assert f"lexer={lexer}" in data.decode()
|
assert "lexer=%s" % lexer in data.decode()
|
||||||
assert "code=full-paste-contents" in data.decode()
|
assert "code=full-paste-contents" in data.decode()
|
||||||
assert "expiry=1week" in data.decode()
|
assert "expiry=1week" in data.decode()
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue