Merge branch 'master' into term-color
Conflicts: src/_pytest/terminal.py testing/test_debugging.py testing/test_terminal.py
This commit is contained in:
commit
e872532d0c
|
@ -3,10 +3,9 @@ Thanks for submitting a PR, your contribution is really appreciated!
|
||||||
|
|
||||||
Here is a quick checklist that should be present in PRs.
|
Here is a quick checklist that should be present in PRs.
|
||||||
|
|
||||||
- [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes.
|
|
||||||
- [ ] Target the `features` branch for new features, improvements, and removals/deprecations.
|
|
||||||
- [ ] Include documentation when adding new features.
|
- [ ] Include documentation when adding new features.
|
||||||
- [ ] Include new tests or update existing tests when applicable.
|
- [ ] Include new tests or update existing tests when applicable.
|
||||||
|
- [X] Allow maintainers to push and squash when merging my commits. Please uncheck this if you prefer to squash the commits yourself.
|
||||||
|
|
||||||
Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
|
Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,14 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- "[0-9]+.[0-9]+.x"
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- "[0-9]+.[0-9]+.x"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -52,6 +54,7 @@ jobs:
|
||||||
python: "3.5"
|
python: "3.5"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py35-xdist"
|
tox_env: "py35-xdist"
|
||||||
|
use_coverage: true
|
||||||
- name: "windows-py36"
|
- name: "windows-py36"
|
||||||
python: "3.6"
|
python: "3.6"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
|
@ -68,6 +71,7 @@ jobs:
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py38"
|
tox_env: "py38"
|
||||||
|
use_coverage: true
|
||||||
|
|
||||||
- name: "ubuntu-py35"
|
- name: "ubuntu-py35"
|
||||||
python: "3.5"
|
python: "3.5"
|
||||||
|
@ -81,6 +85,7 @@ jobs:
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py37-lsof-numpy-oldattrs-pexpect-twisted"
|
tox_env: "py37-lsof-numpy-oldattrs-pexpect-twisted"
|
||||||
|
use_coverage: true
|
||||||
- name: "ubuntu-py37-pluggy"
|
- name: "ubuntu-py37-pluggy"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -89,8 +94,6 @@ jobs:
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py37-freeze"
|
tox_env: "py37-freeze"
|
||||||
# coverage does not apply for freeze test, skip it
|
|
||||||
skip_coverage: true
|
|
||||||
- name: "ubuntu-py38"
|
- name: "ubuntu-py38"
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -99,8 +102,6 @@ jobs:
|
||||||
python: "pypy3"
|
python: "pypy3"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "pypy3-xdist"
|
tox_env: "pypy3-xdist"
|
||||||
# coverage too slow with pypy3, skip it
|
|
||||||
skip_coverage: true
|
|
||||||
|
|
||||||
- name: "macos-py37"
|
- name: "macos-py37"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
|
@ -110,21 +111,21 @@ jobs:
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py38-xdist"
|
tox_env: "py38-xdist"
|
||||||
|
use_coverage: true
|
||||||
|
|
||||||
- name: "linting"
|
- name: "linting"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "linting"
|
tox_env: "linting"
|
||||||
skip_coverage: true
|
|
||||||
- name: "docs"
|
- name: "docs"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "docs"
|
tox_env: "docs"
|
||||||
skip_coverage: true
|
|
||||||
- name: "doctesting"
|
- name: "doctesting"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "doctesting"
|
tox_env: "doctesting"
|
||||||
|
use_coverage: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
@ -138,11 +139,11 @@ jobs:
|
||||||
pip install tox coverage
|
pip install tox coverage
|
||||||
|
|
||||||
- name: Test without coverage
|
- name: Test without coverage
|
||||||
if: "matrix.skip_coverage"
|
if: "! matrix.use_coverage"
|
||||||
run: "tox -e ${{ matrix.tox_env }}"
|
run: "tox -e ${{ matrix.tox_env }}"
|
||||||
|
|
||||||
- name: Test with coverage
|
- name: Test with coverage
|
||||||
if: "! matrix.skip_coverage"
|
if: "matrix.use_coverage"
|
||||||
env:
|
env:
|
||||||
_PYTEST_TOX_COVERAGE_RUN: "coverage run -m"
|
_PYTEST_TOX_COVERAGE_RUN: "coverage run -m"
|
||||||
COVERAGE_PROCESS_START: ".coveragerc"
|
COVERAGE_PROCESS_START: ".coveragerc"
|
||||||
|
@ -150,12 +151,12 @@ jobs:
|
||||||
run: "tox -e ${{ matrix.tox_env }}"
|
run: "tox -e ${{ matrix.tox_env }}"
|
||||||
|
|
||||||
- name: Prepare coverage token
|
- name: Prepare coverage token
|
||||||
if: (!matrix.skip_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' ))
|
if: (matrix.use_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' ))
|
||||||
run: |
|
run: |
|
||||||
python scripts/append_codecov_token.py
|
python scripts/append_codecov_token.py
|
||||||
|
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
if: (!matrix.skip_coverage)
|
if: (matrix.use_coverage)
|
||||||
env:
|
env:
|
||||||
CODECOV_NAME: ${{ matrix.name }}
|
CODECOV_NAME: ${{ matrix.name }}
|
||||||
run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }}
|
run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }}
|
||||||
|
|
|
@ -47,14 +47,14 @@ repos:
|
||||||
- id: rst
|
- id: rst
|
||||||
name: rst
|
name: rst
|
||||||
entry: rst-lint --encoding utf-8
|
entry: rst-lint --encoding utf-8
|
||||||
files: ^(HOWTORELEASE.rst|README.rst|TIDELIFT.rst)$
|
files: ^(RELEASING.rst|README.rst|TIDELIFT.rst)$
|
||||||
language: python
|
language: python
|
||||||
additional_dependencies: [pygments, restructuredtext_lint]
|
additional_dependencies: [pygments, restructuredtext_lint]
|
||||||
- id: changelogs-rst
|
- id: changelogs-rst
|
||||||
name: changelog filenames
|
name: changelog filenames
|
||||||
language: fail
|
language: fail
|
||||||
entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst'
|
entry: 'changelog files must be named ####.(breaking|bugfix|deprecation|doc|feature|improvement|trivial|vendor).rst'
|
||||||
exclude: changelog/(\d+\.(feature|improvement|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst)
|
exclude: changelog/(\d+\.(breaking|bugfix|deprecation|doc|feature|improvement|trivial|vendor).rst|README.rst|_template.rst)
|
||||||
files: ^changelog/
|
files: ^changelog/
|
||||||
- id: py-deprecated
|
- id: py-deprecated
|
||||||
name: py library is deprecated
|
name: py library is deprecated
|
||||||
|
|
39
.travis.yml
39
.travis.yml
|
@ -1,6 +1,6 @@
|
||||||
language: python
|
language: python
|
||||||
dist: xenial
|
dist: trusty
|
||||||
python: '3.7'
|
python: '3.5.1'
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
@ -16,36 +16,11 @@ install:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
# OSX tests - first (in test stage), since they are the slower ones.
|
|
||||||
# Coverage for:
|
|
||||||
# - osx
|
|
||||||
# - verbose=1
|
|
||||||
- os: osx
|
|
||||||
osx_image: xcode10.1
|
|
||||||
language: generic
|
|
||||||
env: TOXENV=py37-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS=-v
|
|
||||||
before_install:
|
|
||||||
- which python3
|
|
||||||
- python3 -V
|
|
||||||
- ln -sfn "$(which python3)" /usr/local/bin/python
|
|
||||||
- python -V
|
|
||||||
- test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 37
|
|
||||||
|
|
||||||
# Full run of latest supported version, without xdist.
|
|
||||||
# Coverage for:
|
|
||||||
# - pytester's LsofFdLeakChecker
|
|
||||||
# - TestArgComplete (linux only)
|
|
||||||
# - numpy
|
|
||||||
# - old attrs
|
|
||||||
# - verbose=0
|
|
||||||
# - test_sys_breakpoint_interception (via pexpect).
|
|
||||||
- env: TOXENV=py37-lsof-numpy-oldattrs-pexpect-twisted PYTEST_COVERAGE=1 PYTEST_ADDOPTS=
|
|
||||||
python: '3.7'
|
|
||||||
|
|
||||||
# Coverage for Python 3.5.{0,1} specific code, mostly typing related.
|
# Coverage for Python 3.5.{0,1} specific code, mostly typing related.
|
||||||
- env: TOXENV=py35 PYTEST_COVERAGE=1 PYTEST_ADDOPTS="-k test_raises_cyclic_reference"
|
- env: TOXENV=py35 PYTEST_COVERAGE=1 PYTEST_ADDOPTS="-k test_raises_cyclic_reference"
|
||||||
python: '3.5.1'
|
before_install:
|
||||||
dist: trusty
|
# Work around https://github.com/jaraco/zipp/issues/40.
|
||||||
|
- python -m pip install -U pip 'setuptools>=34.4.0' virtualenv
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- |
|
- |
|
||||||
|
@ -82,6 +57,4 @@ notifications:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- features
|
- /^\d+\.\d+\.x$/
|
||||||
- 4.6-maintenance
|
|
||||||
- /^\d+(\.\d+)+$/
|
|
||||||
|
|
12
AUTHORS
12
AUTHORS
|
@ -52,6 +52,7 @@ Carl Friedrich Bolz
|
||||||
Carlos Jenkins
|
Carlos Jenkins
|
||||||
Ceridwen
|
Ceridwen
|
||||||
Charles Cloud
|
Charles Cloud
|
||||||
|
Charles Machalow
|
||||||
Charnjit SiNGH (CCSJ)
|
Charnjit SiNGH (CCSJ)
|
||||||
Chris Lamb
|
Chris Lamb
|
||||||
Christian Boelsen
|
Christian Boelsen
|
||||||
|
@ -59,13 +60,13 @@ Christian Fetzer
|
||||||
Christian Neumüller
|
Christian Neumüller
|
||||||
Christian Theunert
|
Christian Theunert
|
||||||
Christian Tismer
|
Christian Tismer
|
||||||
Christopher Gilling
|
Christoph Buelter
|
||||||
Christopher Dignam
|
Christopher Dignam
|
||||||
|
Christopher Gilling
|
||||||
Claudio Madotto
|
Claudio Madotto
|
||||||
CrazyMerlyn
|
CrazyMerlyn
|
||||||
Cyrus Maden
|
Cyrus Maden
|
||||||
Damian Skrzypczak
|
Damian Skrzypczak
|
||||||
Dhiren Serai
|
|
||||||
Daniel Grana
|
Daniel Grana
|
||||||
Daniel Hahler
|
Daniel Hahler
|
||||||
Daniel Nuri
|
Daniel Nuri
|
||||||
|
@ -80,6 +81,7 @@ David Szotten
|
||||||
David Vierra
|
David Vierra
|
||||||
Daw-Ran Liou
|
Daw-Ran Liou
|
||||||
Denis Kirisov
|
Denis Kirisov
|
||||||
|
Dhiren Serai
|
||||||
Diego Russo
|
Diego Russo
|
||||||
Dmitry Dygalo
|
Dmitry Dygalo
|
||||||
Dmitry Pribysh
|
Dmitry Pribysh
|
||||||
|
@ -112,6 +114,7 @@ Guido Wesdorp
|
||||||
Guoqiang Zhang
|
Guoqiang Zhang
|
||||||
Harald Armin Massa
|
Harald Armin Massa
|
||||||
Henk-Jaap Wagenaar
|
Henk-Jaap Wagenaar
|
||||||
|
Holger Kohr
|
||||||
Hugo van Kemenade
|
Hugo van Kemenade
|
||||||
Hui Wang (coldnight)
|
Hui Wang (coldnight)
|
||||||
Ian Bicking
|
Ian Bicking
|
||||||
|
@ -120,6 +123,7 @@ Ilya Konstantinov
|
||||||
Ionuț Turturică
|
Ionuț Turturică
|
||||||
Iwan Briquemont
|
Iwan Briquemont
|
||||||
Jaap Broekhuizen
|
Jaap Broekhuizen
|
||||||
|
Jakub Mitoraj
|
||||||
Jan Balster
|
Jan Balster
|
||||||
Janne Vanhala
|
Janne Vanhala
|
||||||
Jason R. Coombs
|
Jason R. Coombs
|
||||||
|
@ -206,8 +210,10 @@ Omer Hadari
|
||||||
Ondřej Súkup
|
Ondřej Súkup
|
||||||
Oscar Benjamin
|
Oscar Benjamin
|
||||||
Patrick Hayes
|
Patrick Hayes
|
||||||
|
Pauli Virtanen
|
||||||
Paweł Adamczak
|
Paweł Adamczak
|
||||||
Pedro Algarvio
|
Pedro Algarvio
|
||||||
|
Philipp Loose
|
||||||
Pieter Mulder
|
Pieter Mulder
|
||||||
Piotr Banaszkiewicz
|
Piotr Banaszkiewicz
|
||||||
Pulkit Goyal
|
Pulkit Goyal
|
||||||
|
@ -240,6 +246,7 @@ Simon Gomizelj
|
||||||
Skylar Downes
|
Skylar Downes
|
||||||
Srinivas Reddy Thatiparthy
|
Srinivas Reddy Thatiparthy
|
||||||
Stefan Farmbauer
|
Stefan Farmbauer
|
||||||
|
Stefan Scherfke
|
||||||
Stefan Zimmermann
|
Stefan Zimmermann
|
||||||
Stefano Taschini
|
Stefano Taschini
|
||||||
Steffen Allner
|
Steffen Allner
|
||||||
|
@ -268,6 +275,7 @@ Vidar T. Fauske
|
||||||
Virgil Dupras
|
Virgil Dupras
|
||||||
Vitaly Lashmanov
|
Vitaly Lashmanov
|
||||||
Vlad Dragos
|
Vlad Dragos
|
||||||
|
Vladyslav Rachek
|
||||||
Volodymyr Piskun
|
Volodymyr Piskun
|
||||||
Wei Lin
|
Wei Lin
|
||||||
Wil Cooley
|
Wil Cooley
|
||||||
|
|
|
@ -166,8 +166,6 @@ Short version
|
||||||
|
|
||||||
#. Fork the repository.
|
#. Fork the repository.
|
||||||
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
||||||
#. Target ``master`` for bug fixes and doc changes.
|
|
||||||
#. Target ``features`` for new features or functionality changes.
|
|
||||||
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
||||||
#. Tests are run using ``tox``::
|
#. Tests are run using ``tox``::
|
||||||
|
|
||||||
|
@ -204,14 +202,10 @@ Here is a simple overview, with pytest-specific bits:
|
||||||
|
|
||||||
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
|
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
|
||||||
$ cd pytest
|
$ cd pytest
|
||||||
# now, to fix a bug create your own branch off "master":
|
# now, create your own branch off "master":
|
||||||
|
|
||||||
$ git checkout -b your-bugfix-branch-name master
|
$ git checkout -b your-bugfix-branch-name master
|
||||||
|
|
||||||
# or to instead add a feature create your own branch off "features":
|
|
||||||
|
|
||||||
$ git checkout -b your-feature-branch-name features
|
|
||||||
|
|
||||||
Given we have "major.minor.micro" version numbers, bug fixes will usually
|
Given we have "major.minor.micro" version numbers, bug fixes will usually
|
||||||
be released in micro releases whereas features will be released in
|
be released in micro releases whereas features will be released in
|
||||||
minor releases and incompatible changes in major releases.
|
minor releases and incompatible changes in major releases.
|
||||||
|
@ -294,8 +288,7 @@ Here is a simple overview, with pytest-specific bits:
|
||||||
compare: your-branch-name
|
compare: your-branch-name
|
||||||
|
|
||||||
base-fork: pytest-dev/pytest
|
base-fork: pytest-dev/pytest
|
||||||
base: master # if it's a bug fix
|
base: master
|
||||||
base: features # if it's a feature
|
|
||||||
|
|
||||||
|
|
||||||
Writing Tests
|
Writing Tests
|
||||||
|
|
|
@ -10,40 +10,38 @@ taking a lot of time to make a new one.
|
||||||
pytest releases must be prepared on **Linux** because the docs and examples expect
|
pytest releases must be prepared on **Linux** because the docs and examples expect
|
||||||
to be executed on that platform.
|
to be executed on that platform.
|
||||||
|
|
||||||
#. Create a branch ``release-X.Y.Z`` with the version for the release.
|
To release a version ``MAJOR.MINOR.PATCH``, follow these steps:
|
||||||
|
|
||||||
* **maintenance releases**: from ``4.6-maintenance``;
|
#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from the
|
||||||
|
latest ``master`` and push it to the ``pytest-dev/pytest`` repo.
|
||||||
|
|
||||||
* **patch releases**: from the latest ``master``;
|
#. Create a branch ``release-MAJOR.MINOR.PATCH`` from the ``MAJOR.MINOR.x`` branch.
|
||||||
|
|
||||||
* **minor releases**: from the latest ``features``; then merge with the latest ``master``;
|
Ensure your are updated and in a clean working tree.
|
||||||
|
|
||||||
Ensure your are in a clean work tree.
|
|
||||||
|
|
||||||
#. Using ``tox``, generate docs, changelog, announcements::
|
#. Using ``tox``, generate docs, changelog, announcements::
|
||||||
|
|
||||||
$ tox -e release -- <VERSION>
|
$ tox -e release -- MAJOR.MINOR.PATCH
|
||||||
|
|
||||||
This will generate a commit with all the changes ready for pushing.
|
This will generate a commit with all the changes ready for pushing.
|
||||||
|
|
||||||
#. Open a PR for this branch targeting ``master`` (or ``4.6-maintenance`` for
|
#. Open a PR for the ``release-MAJOR.MINOR.PATCH`` branch targeting ``MAJOR.MINOR.x``.
|
||||||
maintenance releases).
|
|
||||||
|
|
||||||
#. After all tests pass and the PR has been approved, publish to PyPI by pushing the tag::
|
#. After all tests pass and the PR has been approved, tag the release commit
|
||||||
|
in the ``MAJOR.MINOR.x`` branch and push it. This will publish to PyPI::
|
||||||
|
|
||||||
git tag <VERSION>
|
git tag MAJOR.MINOR.PATCH
|
||||||
git push git@github.com:pytest-dev/pytest.git <VERSION>
|
git push git@github.com:pytest-dev/pytest.git MAJOR.MINOR.PATCH
|
||||||
|
|
||||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||||
|
|
||||||
#. Merge the PR.
|
#. Merge the PR.
|
||||||
|
|
||||||
#. If this is a maintenance release, cherry-pick the CHANGELOG / announce
|
#. Cherry-pick the CHANGELOG / announce files to the ``master`` branch::
|
||||||
files to the ``master`` branch::
|
|
||||||
|
|
||||||
git fetch --all --prune
|
git fetch --all --prune
|
||||||
git checkout origin/master -b cherry-pick-maintenance-release
|
git checkout origin/master -b cherry-pick-release
|
||||||
git cherry-pick --no-commit -m1 origin/4.6-maintenance
|
git cherry-pick --no-commit -m1 origin/MAJOR.MINOR.x
|
||||||
git checkout origin/master -- changelog
|
git checkout origin/master -- changelog
|
||||||
git commit # no arguments
|
git commit # no arguments
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
``pytest.mark.parametrize`` accepts integers for ``ids`` again, converting it to strings.
|
|
@ -0,0 +1,5 @@
|
||||||
|
Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and
|
||||||
|
provide feedback.
|
||||||
|
|
||||||
|
``--show-capture`` command-line option was added in ``pytest 3.5.0`` and allows to specify how to
|
||||||
|
display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default).
|
|
@ -0,0 +1 @@
|
||||||
|
``--trace`` now works with unittests.
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed some warning reports produced by pytest to point to the correct location of the warning in the user's code.
|
|
@ -0,0 +1 @@
|
||||||
|
Use "yellow" main color with any XPASSED tests.
|
|
@ -0,0 +1 @@
|
||||||
|
New :ref:`--capture=tee-sys <capture-method>` option to allow both live printing and capturing of test output.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Revert "A warning is now issued when assertions are made for ``None``".
|
||||||
|
|
||||||
|
The warning proved to be less useful than initially expected and had quite a
|
||||||
|
few false positive cases.
|
|
@ -0,0 +1 @@
|
||||||
|
``tmpdir_factory.mktemp`` now fails when given absolute and non-normalized paths.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Now all arguments to ``@pytest.mark.parametrize`` need to be explicitly declared in the function signature or via ``indirect``.
|
||||||
|
Previously it was possible to omit an argument if a fixture with the same name existed, which was just an accident of implementation and was not meant to be a part of the API.
|
|
@ -0,0 +1 @@
|
||||||
|
Report ``PytestUnknownMarkWarning`` at the level of the user's code, not ``pytest``'s.
|
|
@ -0,0 +1,10 @@
|
||||||
|
Deprecate using direct constructors for ``Nodes``.
|
||||||
|
|
||||||
|
Instead they are new constructed via ``Node.from_parent``.
|
||||||
|
|
||||||
|
This transitional mechanism enables us to detangle the very intensely
|
||||||
|
entangled ``Node`` relationships by enforcing more controlled creation/configruation patterns.
|
||||||
|
|
||||||
|
As part of that session/config are already disallowed parameters and as we work on the details we might need disallow a few more as well.
|
||||||
|
|
||||||
|
Subclasses are expected to use `super().from_parent` if they intend to expand the creation of `Nodes`.
|
|
@ -0,0 +1 @@
|
||||||
|
The ``pytest_warning_captured`` hook now receives a ``location`` parameter with the code location that generated the warning.
|
|
@ -0,0 +1 @@
|
||||||
|
Fix interaction with ``--pdb`` and unittests: do not use unittest's ``TestCase.debug()``.
|
|
@ -0,0 +1 @@
|
||||||
|
pytester: the ``testdir`` fixture respects environment settings from the ``monkeypatch`` fixture for inner runs.
|
|
@ -0,0 +1 @@
|
||||||
|
``--fulltrace`` is honored with collection errors.
|
|
@ -0,0 +1 @@
|
||||||
|
Matching of ``-k EXPRESSION`` to test names is now case-insensitive.
|
|
@ -0,0 +1,3 @@
|
||||||
|
Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``).
|
||||||
|
|
||||||
|
The upper case variants were never documented and the preferred form should be the lower case.
|
|
@ -0,0 +1 @@
|
||||||
|
Make `--showlocals` work also with `--tb=short`.
|
|
@ -0,0 +1 @@
|
||||||
|
Remove usage of ``parser`` module, deprecated in Python 3.9.
|
|
@ -0,0 +1,3 @@
|
||||||
|
Plugins specified with ``-p`` are now loaded after internal plugins, which results in their hooks being called *before* the internal ones.
|
||||||
|
|
||||||
|
This makes the ``-p`` behavior consistent with ``PYTEST_PLUGINS``.
|
|
@ -0,0 +1 @@
|
||||||
|
`--disable-warnings` is honored with `-ra` and `-rA`.
|
|
@ -0,0 +1 @@
|
||||||
|
Changed default for `-r` to `fE`, which displays failures and errors in the :ref:`short test summary <pytest.detailed_failed_tests_usage>`. `-rN` can be used to disable it (the old behavior).
|
|
@ -0,0 +1 @@
|
||||||
|
New options have been added to the :confval:`junit_logging` option: ``log``, ``out-err``, and ``all``.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Fix bug in the comparison of request key with cached key in fixture.
|
||||||
|
|
||||||
|
A construct ``if key == cached_key:`` can fail either because ``==`` is explicitly disallowed, or for, e.g., NumPy arrays, where the result of ``a == b`` cannot generally be converted to `bool`.
|
||||||
|
The implemented fix replaces `==` with ``is``.
|
|
@ -0,0 +1 @@
|
||||||
|
Fix ``EncodedFile.writelines`` to call the underlying buffer's ``writelines`` method.
|
|
@ -0,0 +1,3 @@
|
||||||
|
Fix internal crash when ``faulthandler`` starts initialized
|
||||||
|
(for example with ``PYTHONFAULTHANDLER=1`` environment variable set) and ``faulthandler_timeout`` defined
|
||||||
|
in the configuration file.
|
|
@ -0,0 +1 @@
|
||||||
|
Fix node ids which contain a parametrized empty-string variable.
|
|
@ -0,0 +1,3 @@
|
||||||
|
Removed the long-deprecated ``pytest_itemstart`` hook.
|
||||||
|
|
||||||
|
This hook has been marked as deprecated and not been even called by pytest for over 10 years now.
|
|
@ -0,0 +1 @@
|
||||||
|
Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc.
|
|
@ -0,0 +1 @@
|
||||||
|
Add support for matching lines consecutively with :attr:`LineMatcher <_pytest.pytester.LineMatcher>`'s :func:`~_pytest.pytester.LineMatcher.fnmatch_lines` and :func:`~_pytest.pytester.LineMatcher.re_match_lines`.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Code is now highlighted in tracebacks when ``pygments`` is installed.
|
||||||
|
|
||||||
|
Users are encouraged to install ``pygments`` into their environment and provide feedback, because
|
||||||
|
the plan is to make ``pygments`` a regular dependency in the future.
|
|
@ -0,0 +1 @@
|
||||||
|
:func:`pytest.exit() <_pytest.outcomes.exit>` is handled when emitted from the :func:`pytest_sessionfinish <_pytest.hookspec.pytest_sessionfinish>` hook. This includes quitting from a debugger.
|
|
@ -0,0 +1 @@
|
||||||
|
Reversed / fix meaning of "+/-" in error diffs. "-" means that sth. expected is missing in the result and "+" means that there are unexpected extras in the result.
|
|
@ -0,0 +1,7 @@
|
||||||
|
The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when
|
||||||
|
the result is unavailable, instead of being deleted.
|
||||||
|
|
||||||
|
If your plugin perform checks like ``hasattr(fixturedef, 'cached_result')``,
|
||||||
|
for example in a ``pytest_fixture_post_finalizer`` hook implementation, replace
|
||||||
|
it with ``fixturedef.cached_result is not None``. If you ``del`` the attribute,
|
||||||
|
set it to ``None`` instead.
|
|
@ -0,0 +1 @@
|
||||||
|
``pytest.mark.parametrize`` supports iterators and generators for ``ids``.
|
|
@ -18,7 +18,7 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||||
* ``bugfix``: fixes a bug.
|
* ``bugfix``: fixes a bug.
|
||||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||||
* ``deprecation``: feature deprecation.
|
* ``deprecation``: feature deprecation.
|
||||||
* ``removal``: feature removal.
|
* ``breaking``: a change which may break existing suites, such as feature removal or behavior change.
|
||||||
* ``vendor``: changes in packages vendored in pytest.
|
* ``vendor``: changes in packages vendored in pytest.
|
||||||
* ``trivial``: fixing a small typo or internal change that might be noteworthy.
|
* ``trivial``: fixing a small typo or internal change that might be noteworthy.
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-5.3.5
|
||||||
release-5.3.4
|
release-5.3.4
|
||||||
release-5.3.3
|
release-5.3.3
|
||||||
release-5.3.2
|
release-5.3.2
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
pytest-5.3.5
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 5.3.5 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* Daniel Hahler
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -3,6 +3,61 @@
|
||||||
Backwards Compatibility Policy
|
Backwards Compatibility Policy
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
.. versionadded: 6.0
|
||||||
|
|
||||||
|
pytest is actively evolving and is a project that has been decades in the making,
|
||||||
|
we keep learning about new and better structures to express different details about testing.
|
||||||
|
|
||||||
|
While we implement those modifications we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors.
|
||||||
|
|
||||||
|
As of now, pytest considers multipe types of backward compatibility transitions:
|
||||||
|
|
||||||
|
a) trivial: APIs which trivially translate to the new mechanism,
|
||||||
|
and do not cause problematic changes.
|
||||||
|
|
||||||
|
We try to support those indefinitely while encouraging users to switch to newer/better mechanisms through documentation.
|
||||||
|
|
||||||
|
b) transitional: the old and new API don't conflict
|
||||||
|
and we can help users transition by using warnings, while supporting both for a prolonged time.
|
||||||
|
|
||||||
|
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
||||||
|
|
||||||
|
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
||||||
|
|
||||||
|
|
||||||
|
c) true breakage: should only to be considered when normal transition is unreasonably unsustainable and would offset important development/features by years.
|
||||||
|
In addition, they should be limited to APIs where the number of actual users is very small (for example only impacting some plugins), and can be coordinated with the community in advance.
|
||||||
|
|
||||||
|
Examples for such upcoming changes:
|
||||||
|
|
||||||
|
* removal of ``pytest_runtest_protocol/nextitem`` - `#895`_
|
||||||
|
* rearranging of the node tree to include ``FunctionDefinition``
|
||||||
|
* rearranging of ``SetupState`` `#895`_
|
||||||
|
|
||||||
|
True breakages must be announced first in an issue containing:
|
||||||
|
|
||||||
|
* Detailed description of the change
|
||||||
|
* Rationale
|
||||||
|
* Expected impact on users and plugin authors (example in `#895`_)
|
||||||
|
|
||||||
|
After there's no hard *-1* on the issue it should be followed up by an initial proof-of-concept Pull Request.
|
||||||
|
|
||||||
|
This POC serves as both a coordination point to assess impact and potential inspriation to come up with a transitional solution after all.
|
||||||
|
|
||||||
|
After a reasonable amount of time the PR can be merged to base a new major release.
|
||||||
|
|
||||||
|
For the PR to mature from POC to acceptance, it must contain:
|
||||||
|
* Setup of deprecation errors/warnings that help users fix and port their code. If it is possible to introduce a deprecation period under the current series, before the true breakage, it should be introduced in a separate PR and be part of the current release stream.
|
||||||
|
* Detailed description of the rationale and examples on how to port code in ``doc/en/deprecations.rst``.
|
||||||
|
|
||||||
|
|
||||||
|
History
|
||||||
|
=========
|
||||||
|
|
||||||
|
|
||||||
|
Focus primary on smooth transition - stance (pre 6.0)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Keeping backwards compatibility has a very high priority in the pytest project. Although we have deprecated functionality over the years, most of it is still supported. All deprecations in pytest were done because simpler or more efficient ways of accomplishing the same tasks have emerged, making the old way of doing things unnecessary.
|
Keeping backwards compatibility has a very high priority in the pytest project. Although we have deprecated functionality over the years, most of it is still supported. All deprecations in pytest were done because simpler or more efficient ways of accomplishing the same tasks have emerged, making the old way of doing things unnecessary.
|
||||||
|
|
||||||
With the pytest 3.0 release we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around.
|
With the pytest 3.0 release we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around.
|
||||||
|
@ -20,3 +75,6 @@ Deprecation Roadmap
|
||||||
Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`.
|
Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`.
|
||||||
|
|
||||||
We track future deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub.
|
We track future deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub.
|
||||||
|
|
||||||
|
|
||||||
|
.. _`#895`: https://github.com/pytest-dev/pytest/issues/895
|
||||||
|
|
|
@ -21,27 +21,36 @@ file descriptors. This allows to capture output from simple
|
||||||
print statements as well as output from a subprocess started by
|
print statements as well as output from a subprocess started by
|
||||||
a test.
|
a test.
|
||||||
|
|
||||||
|
.. _capture-method:
|
||||||
|
|
||||||
Setting capturing methods or disabling capturing
|
Setting capturing methods or disabling capturing
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
There are two ways in which ``pytest`` can perform capturing:
|
There are three ways in which ``pytest`` can perform capturing:
|
||||||
|
|
||||||
* file descriptor (FD) level capturing (default): All writes going to the
|
* ``fd`` (file descriptor) level capturing (default): All writes going to the
|
||||||
operating system file descriptors 1 and 2 will be captured.
|
operating system file descriptors 1 and 2 will be captured.
|
||||||
|
|
||||||
* ``sys`` level capturing: Only writes to Python files ``sys.stdout``
|
* ``sys`` level capturing: Only writes to Python files ``sys.stdout``
|
||||||
and ``sys.stderr`` will be captured. No capturing of writes to
|
and ``sys.stderr`` will be captured. No capturing of writes to
|
||||||
filedescriptors is performed.
|
filedescriptors is performed.
|
||||||
|
|
||||||
|
* ``tee-sys`` capturing: Python writes to ``sys.stdout`` and ``sys.stderr``
|
||||||
|
will be captured, however the writes will also be passed-through to
|
||||||
|
the actual ``sys.stdout`` and ``sys.stderr``. This allows output to be
|
||||||
|
'live printed' and captured for plugin use, such as junitxml (new in pytest 5.4).
|
||||||
|
|
||||||
.. _`disable capturing`:
|
.. _`disable capturing`:
|
||||||
|
|
||||||
You can influence output capturing mechanisms from the command line:
|
You can influence output capturing mechanisms from the command line:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest -s # disable all capturing
|
pytest -s # disable all capturing
|
||||||
pytest --capture=sys # replace sys.stdout/stderr with in-mem files
|
pytest --capture=sys # replace sys.stdout/stderr with in-mem files
|
||||||
pytest --capture=fd # also point filedescriptors 1 and 2 to temp file
|
pytest --capture=fd # also point filedescriptors 1 and 2 to temp file
|
||||||
|
pytest --capture=tee-sys # combines 'sys' and '-s', capturing sys.stdout/stderr
|
||||||
|
# and passing it along to the actual sys.stdout/stderr
|
||||||
|
|
||||||
.. _printdebugging:
|
.. _printdebugging:
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,15 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 5.3.5 (2020-01-29)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#6517 <https://github.com/pytest-dev/pytest/issues/6517>`_: Fix regression in pytest 5.3.4 causing an INTERNALERROR due to a wrong assertion.
|
||||||
|
|
||||||
|
|
||||||
pytest 5.3.4 (2020-01-20)
|
pytest 5.3.4 (2020-01-20)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,7 @@ html_logo = "img/pytest1.png"
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
# pixels large.
|
# pixels large.
|
||||||
html_favicon = "img/pytest1favi.ico"
|
html_favicon = "img/favicon.png"
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
|
|
@ -19,6 +19,30 @@ Below is a complete list of all pytest features which are considered deprecated.
|
||||||
:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
|
:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
|
||||||
:ref:`standard warning filters <warnings>`.
|
:ref:`standard warning filters <warnings>`.
|
||||||
|
|
||||||
|
|
||||||
|
``--no-print-logs`` command-line option
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. deprecated:: 5.4
|
||||||
|
|
||||||
|
|
||||||
|
Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and
|
||||||
|
provide feedback.
|
||||||
|
|
||||||
|
``--show-capture`` command-line option was added in ``pytest 3.5.0` and allows to specify how to
|
||||||
|
display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Node Construction changed to ``Node.from_parent``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. deprecated:: 5.3
|
||||||
|
|
||||||
|
The construction of nodes new should use the named constructor ``from_parent``.
|
||||||
|
This limitation in api surface intends to enable better/simpler refactoring of the collection tree.
|
||||||
|
|
||||||
|
|
||||||
``junit_family`` default value change to "xunit2"
|
``junit_family`` default value change to "xunit2"
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -57,4 +57,4 @@ Issues created at those events should have other relevant labels added as well.
|
||||||
Those labels should be removed after they are no longer relevant.
|
Those labels should be removed after they are no longer relevant.
|
||||||
|
|
||||||
|
|
||||||
.. include:: ../../HOWTORELEASE.rst
|
.. include:: ../../RELEASING.rst
|
||||||
|
|
|
@ -148,6 +148,10 @@ which implements a substring match on the test names instead of the
|
||||||
exact match on markers that ``-m`` provides. This makes it easy to
|
exact match on markers that ``-m`` provides. This makes it easy to
|
||||||
select tests based on their names:
|
select tests based on their names:
|
||||||
|
|
||||||
|
.. versionadded: 5.4
|
||||||
|
|
||||||
|
The expression matching is now case-insensitive.
|
||||||
|
|
||||||
.. code-block:: pytest
|
.. code-block:: pytest
|
||||||
|
|
||||||
$ pytest -v -k http # running with the above defined example module
|
$ pytest -v -k http # running with the above defined example module
|
||||||
|
|
|
@ -4,7 +4,7 @@ import pytest
|
||||||
|
|
||||||
def pytest_collect_file(parent, path):
|
def pytest_collect_file(parent, path):
|
||||||
if path.ext == ".yaml" and path.basename.startswith("test"):
|
if path.ext == ".yaml" and path.basename.startswith("test"):
|
||||||
return YamlFile(path, parent)
|
return YamlFile.from_parent(parent, fspath=path)
|
||||||
|
|
||||||
|
|
||||||
class YamlFile(pytest.File):
|
class YamlFile(pytest.File):
|
||||||
|
@ -13,7 +13,7 @@ class YamlFile(pytest.File):
|
||||||
|
|
||||||
raw = yaml.safe_load(self.fspath.open())
|
raw = yaml.safe_load(self.fspath.open())
|
||||||
for name, spec in sorted(raw.items()):
|
for name, spec in sorted(raw.items()):
|
||||||
yield YamlItem(name, self, spec)
|
yield YamlItem.from_parent(self, name=name, spec=spec)
|
||||||
|
|
||||||
|
|
||||||
class YamlItem(pytest.Item):
|
class YamlItem(pytest.Item):
|
||||||
|
|
|
@ -398,6 +398,9 @@ The result of this test will be successful:
|
||||||
|
|
||||||
.. regendoc:wipe
|
.. regendoc:wipe
|
||||||
|
|
||||||
|
Note, that each argument in `parametrize` list should be explicitly declared in corresponding
|
||||||
|
python test function or via `indirect`.
|
||||||
|
|
||||||
Parametrizing test methods through per-class configuration
|
Parametrizing test methods through per-class configuration
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -81,8 +81,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
def test_eq_text(self):
|
def test_eq_text(self):
|
||||||
> assert "spam" == "eggs"
|
> assert "spam" == "eggs"
|
||||||
E AssertionError: assert 'spam' == 'eggs'
|
E AssertionError: assert 'spam' == 'eggs'
|
||||||
E - spam
|
E - eggs
|
||||||
E + eggs
|
E + spam
|
||||||
|
|
||||||
failure_demo.py:45: AssertionError
|
failure_demo.py:45: AssertionError
|
||||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||||
|
@ -92,9 +92,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
def test_eq_similar_text(self):
|
def test_eq_similar_text(self):
|
||||||
> assert "foo 1 bar" == "foo 2 bar"
|
> assert "foo 1 bar" == "foo 2 bar"
|
||||||
E AssertionError: assert 'foo 1 bar' == 'foo 2 bar'
|
E AssertionError: assert 'foo 1 bar' == 'foo 2 bar'
|
||||||
E - foo 1 bar
|
E - foo 2 bar
|
||||||
E ? ^
|
E ? ^
|
||||||
E + foo 2 bar
|
E + foo 1 bar
|
||||||
E ? ^
|
E ? ^
|
||||||
|
|
||||||
failure_demo.py:48: AssertionError
|
failure_demo.py:48: AssertionError
|
||||||
|
@ -106,8 +106,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
|
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
|
||||||
E AssertionError: assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
|
E AssertionError: assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
|
||||||
E foo
|
E foo
|
||||||
E - spam
|
E - eggs
|
||||||
E + eggs
|
E + spam
|
||||||
E bar
|
E bar
|
||||||
|
|
||||||
failure_demo.py:51: AssertionError
|
failure_demo.py:51: AssertionError
|
||||||
|
@ -122,9 +122,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
E AssertionError: assert '111111111111...2222222222222' == '111111111111...2222222222222'
|
E AssertionError: assert '111111111111...2222222222222' == '111111111111...2222222222222'
|
||||||
E Skipping 90 identical leading characters in diff, use -v to show
|
E Skipping 90 identical leading characters in diff, use -v to show
|
||||||
E Skipping 91 identical trailing characters in diff, use -v to show
|
E Skipping 91 identical trailing characters in diff, use -v to show
|
||||||
E - 1111111111a222222222
|
E - 1111111111b222222222
|
||||||
E ? ^
|
E ? ^
|
||||||
E + 1111111111b222222222
|
E + 1111111111a222222222
|
||||||
E ? ^
|
E ? ^
|
||||||
|
|
||||||
failure_demo.py:56: AssertionError
|
failure_demo.py:56: AssertionError
|
||||||
|
|
|
@ -461,21 +461,49 @@ an ``incremental`` marker which is to be used on classes:
|
||||||
|
|
||||||
# content of conftest.py
|
# content of conftest.py
|
||||||
|
|
||||||
import pytest
|
# store history of failures per test class name and per index in parametrize (if parametrize used)
|
||||||
|
_test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {}
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
if "incremental" in item.keywords:
|
if "incremental" in item.keywords:
|
||||||
|
# incremental marker is used
|
||||||
if call.excinfo is not None:
|
if call.excinfo is not None:
|
||||||
parent = item.parent
|
# the test has failed
|
||||||
parent._previousfailed = item
|
# retrieve the class name of the test
|
||||||
|
cls_name = str(item.cls)
|
||||||
|
# retrieve the index of the test (if parametrize is used in combination with incremental)
|
||||||
|
parametrize_index = (
|
||||||
|
tuple(item.callspec.indices.values())
|
||||||
|
if hasattr(item, "callspec")
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
# retrieve the name of the test function
|
||||||
|
test_name = item.originalname or item.name
|
||||||
|
# store in _test_failed_incremental the original name of the failed test
|
||||||
|
_test_failed_incremental.setdefault(cls_name, {}).setdefault(
|
||||||
|
parametrize_index, test_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
if "incremental" in item.keywords:
|
if "incremental" in item.keywords:
|
||||||
previousfailed = getattr(item.parent, "_previousfailed", None)
|
# retrieve the class name of the test
|
||||||
if previousfailed is not None:
|
cls_name = str(item.cls)
|
||||||
pytest.xfail("previous test failed ({})".format(previousfailed.name))
|
# check if a previous test has failed for this class
|
||||||
|
if cls_name in _test_failed_incremental:
|
||||||
|
# retrieve the index of the test (if parametrize is used in combination with incremental)
|
||||||
|
parametrize_index = (
|
||||||
|
tuple(item.callspec.indices.values())
|
||||||
|
if hasattr(item, "callspec")
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
# retrieve the name of the first test function to fail for this class name and index
|
||||||
|
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
|
||||||
|
# if name found, test has failed for the combination of class name & test name
|
||||||
|
if test_name is not None:
|
||||||
|
pytest.xfail("previous test failed ({})".format(test_name))
|
||||||
|
|
||||||
|
|
||||||
These two hook implementations work together to abort incremental-marked
|
These two hook implementations work together to abort incremental-marked
|
||||||
tests in a class. Here is a test module example:
|
tests in a class. Here is a test module example:
|
||||||
|
|
|
@ -849,7 +849,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``:
|
||||||
Modularity: using fixtures from a fixture function
|
Modularity: using fixtures from a fixture function
|
||||||
----------------------------------------------------------
|
----------------------------------------------------------
|
||||||
|
|
||||||
You can not only use fixtures in test functions but fixture functions
|
In addition to using fixtures in test functions, fixture functions
|
||||||
can use other fixtures themselves. This contributes to a modular design
|
can use other fixtures themselves. This contributes to a modular design
|
||||||
of your fixtures and allows re-use of framework-specific fixtures across
|
of your fixtures and allows re-use of framework-specific fixtures across
|
||||||
many projects. As a simple example, we can extend the previous example
|
many projects. As a simple example, we can extend the previous example
|
||||||
|
|
|
@ -127,7 +127,7 @@ Once you develop multiple tests, you may want to group them into a class. pytest
|
||||||
x = "hello"
|
x = "hello"
|
||||||
assert hasattr(x, "check")
|
assert hasattr(x, "check")
|
||||||
|
|
||||||
``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery <test discovery>`, so it finds both ``test_`` prefixed functions. There is no need to subclass anything. We can simply run the module by passing its filename:
|
``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery <test discovery>`, so it finds both ``test_`` prefixed functions. There is no need to subclass anything, but make sure to prefix your class with ``Test`` otherwise the class will be skipped. We can simply run the module by passing its filename:
|
||||||
|
|
||||||
.. code-block:: pytest
|
.. code-block:: pytest
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.7 KiB |
|
@ -29,9 +29,9 @@ Maintenance of 4.6.X versions
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
Until January 2020, the pytest core team ported many bug-fixes from the main release into the
|
Until January 2020, the pytest core team ported many bug-fixes from the main release into the
|
||||||
``4.6-maintenance`` branch, with several 4.6.X releases being made along the year.
|
``4.6.x`` branch, with several 4.6.X releases being made along the year.
|
||||||
|
|
||||||
From now on, the core team will **no longer actively backport patches**, but the ``4.6-maintenance``
|
From now on, the core team will **no longer actively backport patches**, but the ``4.6.x``
|
||||||
branch will continue to exist so the community itself can contribute patches.
|
branch will continue to exist so the community itself can contribute patches.
|
||||||
|
|
||||||
The core team will be happy to accept those patches, and make new 4.6.X releases **until mid-2020**
|
The core team will be happy to accept those patches, and make new 4.6.X releases **until mid-2020**
|
||||||
|
@ -74,7 +74,7 @@ Please follow these instructions:
|
||||||
|
|
||||||
#. ``git fetch --all --prune``
|
#. ``git fetch --all --prune``
|
||||||
|
|
||||||
#. ``git checkout origin/4.6-maintenance -b backport-XXXX`` # use the PR number here
|
#. ``git checkout origin/4.6.x -b backport-XXXX`` # use the PR number here
|
||||||
|
|
||||||
#. Locate the merge commit on the PR, in the *merged* message, for example:
|
#. Locate the merge commit on the PR, in the *merged* message, for example:
|
||||||
|
|
||||||
|
@ -82,14 +82,14 @@ Please follow these instructions:
|
||||||
|
|
||||||
#. ``git cherry-pick -m1 REVISION`` # use the revision you found above (``0f8b462``).
|
#. ``git cherry-pick -m1 REVISION`` # use the revision you found above (``0f8b462``).
|
||||||
|
|
||||||
#. Open a PR targeting ``4.6-maintenance``:
|
#. Open a PR targeting ``4.6.x``:
|
||||||
|
|
||||||
* Prefix the message with ``[4.6]`` so it is an obvious backport
|
* Prefix the message with ``[4.6]`` so it is an obvious backport
|
||||||
* Delete the PR body, it usually contains a duplicate commit message.
|
* Delete the PR body, it usually contains a duplicate commit message.
|
||||||
|
|
||||||
**Providing new PRs to 4.6**
|
**Providing new PRs to 4.6**
|
||||||
|
|
||||||
Fresh pull requests to ``4.6-maintenance`` will be accepted provided that
|
Fresh pull requests to ``4.6.x`` will be accepted provided that
|
||||||
the equivalent code in the active branches does not contain that bug (for example, a bug is specific
|
the equivalent code in the active branches does not contain that bug (for example, a bug is specific
|
||||||
to Python 2 only).
|
to Python 2 only).
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,8 @@ This is also discussed in details in :ref:`test discovery`.
|
||||||
Invoking ``pytest`` versus ``python -m pytest``
|
Invoking ``pytest`` versus ``python -m pytest``
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
Running pytest with ``python -m pytest [...]`` instead of ``pytest [...]`` yields nearly
|
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
|
||||||
equivalent behaviour, except that the former call will add the current directory to ``sys.path``.
|
equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which
|
||||||
|
is standard ``python`` behavior.
|
||||||
|
|
||||||
See also :ref:`cmdline`.
|
See also :ref:`cmdline`.
|
||||||
|
|
|
@ -738,7 +738,7 @@ ExceptionInfo
|
||||||
pytest.ExitCode
|
pytest.ExitCode
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. autoclass:: _pytest.main.ExitCode
|
.. autoclass:: _pytest.config.ExitCode
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
@ -901,8 +901,8 @@ Can be either a ``str`` or ``Sequence[str]``.
|
||||||
pytest_plugins = ("myapp.testsupport.tools", "myapp.testsupport.regression")
|
pytest_plugins = ("myapp.testsupport.tools", "myapp.testsupport.regression")
|
||||||
|
|
||||||
|
|
||||||
pytest_mark
|
pytestmark
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
**Tutorial**: :ref:`scoped-marking`
|
**Tutorial**: :ref:`scoped-marking`
|
||||||
|
|
||||||
|
@ -1164,9 +1164,17 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
.. confval:: junit_logging
|
.. confval:: junit_logging
|
||||||
|
|
||||||
.. versionadded:: 3.5
|
.. versionadded:: 3.5
|
||||||
|
.. versionchanged:: 5.4
|
||||||
|
``log``, ``all``, ``out-err`` options added.
|
||||||
|
|
||||||
Configures if stdout/stderr should be written to the JUnit XML file. Valid values are
|
Configures if captured output should be written to the JUnit XML file. Valid values are:
|
||||||
``system-out``, ``system-err``, and ``no`` (the default).
|
|
||||||
|
* ``log``: write only ``logging`` captured output.
|
||||||
|
* ``system-out``: write captured ``stdout`` contents.
|
||||||
|
* ``system-err``: write captured ``stderr`` contents.
|
||||||
|
* ``out-err``: write both captured ``stdout`` and ``stderr`` contents.
|
||||||
|
* ``all``: write captured ``logging``, ``stdout`` and ``stderr`` contents.
|
||||||
|
* ``no`` (the default): no captured output is written.
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
|
|
|
@ -238,17 +238,6 @@ was executed ahead of the ``test_method``.
|
||||||
|
|
||||||
.. _pdb-unittest-note:
|
.. _pdb-unittest-note:
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Running tests from ``unittest.TestCase`` subclasses with ``--pdb`` will
|
|
||||||
disable tearDown and cleanup methods for the case that an Exception
|
|
||||||
occurs. This allows proper post mortem debugging for all applications
|
|
||||||
which have significant logic in their tearDown machinery. However,
|
|
||||||
supporting this feature has the following side effect: If people
|
|
||||||
overwrite ``unittest.TestCase`` ``__call__`` or ``run``, they need to
|
|
||||||
to overwrite ``debug`` in the same way (this is also true for standard
|
|
||||||
unittest).
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Due to architectural differences between the two frameworks, setup and
|
Due to architectural differences between the two frameworks, setup and
|
||||||
|
|
|
@ -33,7 +33,7 @@ Running ``pytest`` can result in six different exit codes:
|
||||||
:Exit code 4: pytest command line usage error
|
:Exit code 4: pytest command line usage error
|
||||||
:Exit code 5: No tests were collected
|
:Exit code 5: No tests were collected
|
||||||
|
|
||||||
They are represented by the :class:`_pytest.main.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
|
They are represented by the :class:`_pytest.config.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -94,8 +94,8 @@ Pytest supports several ways to run and select tests from the command-line.
|
||||||
|
|
||||||
pytest -k "MyClass and not method"
|
pytest -k "MyClass and not method"
|
||||||
|
|
||||||
This will run tests which contain names that match the given *string expression*, which can
|
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
||||||
include Python operators that use filenames, class names and function names as variables.
|
which can include Python operators that use filenames, class names and function names as variables.
|
||||||
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
||||||
|
|
||||||
.. _nodeids:
|
.. _nodeids:
|
||||||
|
@ -169,11 +169,11 @@ option you make sure a trace is shown.
|
||||||
Detailed summary report
|
Detailed summary report
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The ``-r`` flag can be used to display a "short test summary info" at the end of the test session,
|
The ``-r`` flag can be used to display a "short test summary info" at the end of the test session,
|
||||||
making it easy in large test suites to get a clear picture of all failures, skips, xfails, etc.
|
making it easy in large test suites to get a clear picture of all failures, skips, xfails, etc.
|
||||||
|
|
||||||
|
It defaults to ``fE`` to list failures and errors.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -261,8 +261,12 @@ Here is the full list of available characters that can be used:
|
||||||
- ``X`` - xpassed
|
- ``X`` - xpassed
|
||||||
- ``p`` - passed
|
- ``p`` - passed
|
||||||
- ``P`` - passed with output
|
- ``P`` - passed with output
|
||||||
|
|
||||||
|
Special characters for (de)selection of groups:
|
||||||
|
|
||||||
- ``a`` - all except ``pP``
|
- ``a`` - all except ``pP``
|
||||||
- ``A`` - all
|
- ``A`` - all
|
||||||
|
- ``N`` - none, this can be used to display nothing (since ``fE`` is the default)
|
||||||
|
|
||||||
More than one character can be used, so for example to only see failed and skipped tests, you can execute:
|
More than one character can be used, so for example to only see failed and skipped tests, you can execute:
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,7 @@ done via a :pep:`302` import hook which gets installed early on when
|
||||||
``pytest`` starts up and will perform this rewriting when modules get
|
``pytest`` starts up and will perform this rewriting when modules get
|
||||||
imported. However since we do not want to test different bytecode
|
imported. However since we do not want to test different bytecode
|
||||||
then you will run in production this hook only rewrites test modules
|
then you will run in production this hook only rewrites test modules
|
||||||
|
themselves (as defined by the :confval:`python_files` configuration option)
|
||||||
themselves as well as any modules which are part of plugins. Any
|
themselves as well as any modules which are part of plugins. Any
|
||||||
other imported module will not be rewritten and normal assertion
|
other imported module will not be rewritten and normal assertion
|
||||||
behaviour will happen.
|
behaviour will happen.
|
||||||
|
|
|
@ -16,8 +16,8 @@ title_format = "pytest {version} ({project_date})"
|
||||||
template = "changelog/_template.rst"
|
template = "changelog/_template.rst"
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
directory = "removal"
|
directory = "breaking"
|
||||||
name = "Removals"
|
name = "Breaking Changes"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[[tool.towncrier.type]]
|
[[tool.towncrier.type]]
|
||||||
|
|
|
@ -61,7 +61,9 @@ def parse_changelog(tag_name):
|
||||||
|
|
||||||
|
|
||||||
def convert_rst_to_md(text):
|
def convert_rst_to_md(text):
|
||||||
return pypandoc.convert_text(text, "md", format="rst")
|
return pypandoc.convert_text(
|
||||||
|
text, "md", format="rst", extra_args=["--wrap=preserve"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
|
|
|
@ -10,7 +10,6 @@ project_urls =
|
||||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||||
|
|
||||||
license = MIT license
|
license = MIT license
|
||||||
license_file = LICENSE
|
|
||||||
keywords = test, unittest
|
keywords = test, unittest
|
||||||
classifiers =
|
classifiers =
|
||||||
Development Status :: 6 - Mature
|
Development Status :: 6 - Mature
|
||||||
|
@ -66,6 +65,7 @@ formats = sdist.tgz,bdist_wheel
|
||||||
mypy_path = src
|
mypy_path = src
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
no_implicit_optional = True
|
no_implicit_optional = True
|
||||||
|
show_error_codes = True
|
||||||
strict_equality = True
|
strict_equality = True
|
||||||
warn_redundant_casts = True
|
warn_redundant_casts = True
|
||||||
warn_return_any = True
|
warn_return_any = True
|
||||||
|
|
|
@ -53,19 +53,22 @@ If things do not work right away:
|
||||||
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
||||||
global argcomplete script).
|
global argcomplete script).
|
||||||
"""
|
"""
|
||||||
|
import argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
from typing import Any
|
||||||
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class FastFilesCompleter:
|
class FastFilesCompleter:
|
||||||
"Fast file completer class"
|
"Fast file completer class"
|
||||||
|
|
||||||
def __init__(self, directories=True):
|
def __init__(self, directories: bool = True) -> None:
|
||||||
self.directories = directories
|
self.directories = directories
|
||||||
|
|
||||||
def __call__(self, prefix, **kwargs):
|
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
||||||
"""only called on non option completions"""
|
"""only called on non option completions"""
|
||||||
if os.path.sep in prefix[1:]:
|
if os.path.sep in prefix[1:]:
|
||||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
||||||
|
@ -94,13 +97,13 @@ if os.environ.get("_ARGCOMPLETE"):
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter]
|
filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter]
|
||||||
|
|
||||||
def try_argcomplete(parser):
|
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||||
argcomplete.autocomplete(parser, always_complete_options=False)
|
argcomplete.autocomplete(parser, always_complete_options=False)
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def try_argcomplete(parser):
|
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
filescompleter = None
|
filescompleter = None
|
||||||
|
|
|
@ -72,17 +72,18 @@ class Code:
|
||||||
""" return a path object pointing to source code (or a str in case
|
""" return a path object pointing to source code (or a str in case
|
||||||
of OSError / non-existing file).
|
of OSError / non-existing file).
|
||||||
"""
|
"""
|
||||||
|
if not self.raw.co_filename:
|
||||||
|
return ""
|
||||||
try:
|
try:
|
||||||
p = py.path.local(self.raw.co_filename)
|
p = py.path.local(self.raw.co_filename)
|
||||||
# maybe don't try this checking
|
# maybe don't try this checking
|
||||||
if not p.check():
|
if not p.check():
|
||||||
raise OSError("py.path check failed.")
|
raise OSError("py.path check failed.")
|
||||||
|
return p
|
||||||
except OSError:
|
except OSError:
|
||||||
# XXX maybe try harder like the weird logic
|
# XXX maybe try harder like the weird logic
|
||||||
# in the standard lib [linecache.updatecache] does?
|
# in the standard lib [linecache.updatecache] does?
|
||||||
p = self.raw.co_filename
|
return self.raw.co_filename
|
||||||
|
|
||||||
return p
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fullsource(self) -> Optional["Source"]:
|
def fullsource(self) -> Optional["Source"]:
|
||||||
|
@ -788,9 +789,7 @@ class FormattedExcinfo:
|
||||||
message = excinfo and excinfo.typename or ""
|
message = excinfo and excinfo.typename or ""
|
||||||
path = self._makepath(entry.path)
|
path = self._makepath(entry.path)
|
||||||
filelocrepr = ReprFileLocation(path, entry.lineno + 1, message)
|
filelocrepr = ReprFileLocation(path, entry.lineno + 1, message)
|
||||||
localsrepr = None
|
localsrepr = self.repr_locals(entry.locals)
|
||||||
if not short:
|
|
||||||
localsrepr = self.repr_locals(entry.locals)
|
|
||||||
return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style)
|
return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style)
|
||||||
if excinfo:
|
if excinfo:
|
||||||
lines.extend(self.get_exconly(excinfo, indent=4))
|
lines.extend(self.get_exconly(excinfo, indent=4))
|
||||||
|
@ -976,18 +975,13 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||||
super().toterminal(tw)
|
super().toterminal(tw)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
class ReprTraceback(TerminalRepr):
|
class ReprTraceback(TerminalRepr):
|
||||||
entrysep = "_ "
|
reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]])
|
||||||
|
extraline = attr.ib(type=Optional[str])
|
||||||
|
style = attr.ib(type="_TracebackStyle")
|
||||||
|
|
||||||
def __init__(
|
entrysep = "_ "
|
||||||
self,
|
|
||||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]],
|
|
||||||
extraline: Optional[str],
|
|
||||||
style: "_TracebackStyle",
|
|
||||||
) -> None:
|
|
||||||
self.reprentries = reprentries
|
|
||||||
self.extraline = extraline
|
|
||||||
self.style = style
|
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
# the entries might have different styles
|
# the entries might have different styles
|
||||||
|
@ -1040,19 +1034,58 @@ class ReprEntry(TerminalRepr):
|
||||||
self.reprfileloc = filelocrepr
|
self.reprfileloc = filelocrepr
|
||||||
self.style = style
|
self.style = style
|
||||||
|
|
||||||
|
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
||||||
|
"""Writes the source code portions of a list of traceback entries with syntax highlighting.
|
||||||
|
|
||||||
|
Usually entries are lines like these:
|
||||||
|
|
||||||
|
" x = 1"
|
||||||
|
"> assert x == 2"
|
||||||
|
"E assert 1 == 2"
|
||||||
|
|
||||||
|
This function takes care of rendering the "source" portions of it (the lines without
|
||||||
|
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
|
||||||
|
character, as doing so might break line continuations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
def is_fail(line):
|
||||||
|
return line.startswith("{} ".format(FormattedExcinfo.fail_marker))
|
||||||
|
|
||||||
|
if not self.lines:
|
||||||
|
return
|
||||||
|
|
||||||
|
# separate indents and source lines that are not failures: we want to
|
||||||
|
# highlight the code but not the indentation, which may contain markers
|
||||||
|
# such as "> assert 0"
|
||||||
|
indents = []
|
||||||
|
source_lines = []
|
||||||
|
for line in self.lines:
|
||||||
|
if not is_fail(line):
|
||||||
|
indents.append(line[:indent_size])
|
||||||
|
source_lines.append(line[indent_size:])
|
||||||
|
|
||||||
|
tw._write_source(source_lines, indents)
|
||||||
|
|
||||||
|
# failure lines are always completely red and bold
|
||||||
|
for line in (x for x in self.lines if is_fail(x)):
|
||||||
|
tw.line(line, bold=True, red=True)
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
if self.style == "short":
|
if self.style == "short":
|
||||||
assert self.reprfileloc is not None
|
assert self.reprfileloc is not None
|
||||||
self.reprfileloc.toterminal(tw)
|
self.reprfileloc.toterminal(tw)
|
||||||
for line in self.lines:
|
self._write_entry_lines(tw)
|
||||||
red = line.startswith("E ")
|
if self.reprlocals:
|
||||||
tw.line(line, bold=True, red=red)
|
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.reprfuncargs:
|
if self.reprfuncargs:
|
||||||
self.reprfuncargs.toterminal(tw)
|
self.reprfuncargs.toterminal(tw)
|
||||||
for line in self.lines:
|
|
||||||
red = line.startswith("E ")
|
self._write_entry_lines(tw)
|
||||||
tw.line(line, bold=True, red=red)
|
|
||||||
if self.reprlocals:
|
if self.reprlocals:
|
||||||
tw.line("")
|
tw.line("")
|
||||||
self.reprlocals.toterminal(tw)
|
self.reprlocals.toterminal(tw)
|
||||||
|
@ -1067,11 +1100,11 @@ class ReprEntry(TerminalRepr):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
class ReprFileLocation(TerminalRepr):
|
class ReprFileLocation(TerminalRepr):
|
||||||
def __init__(self, path, lineno: int, message: str) -> None:
|
path = attr.ib(type=str, converter=str)
|
||||||
self.path = str(path)
|
lineno = attr.ib(type=int)
|
||||||
self.lineno = lineno
|
message = attr.ib(type=str)
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
# filename and lineno output for each entry,
|
# filename and lineno output for each entry,
|
||||||
|
@ -1088,9 +1121,9 @@ class ReprLocals(TerminalRepr):
|
||||||
def __init__(self, lines: Sequence[str]) -> None:
|
def __init__(self, lines: Sequence[str]) -> None:
|
||||||
self.lines = lines
|
self.lines = lines
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter, indent="") -> None:
|
||||||
for line in self.lines:
|
for line in self.lines:
|
||||||
tw.line(line)
|
tw.line(indent + line)
|
||||||
|
|
||||||
|
|
||||||
class ReprFuncArgs(TerminalRepr):
|
class ReprFuncArgs(TerminalRepr):
|
||||||
|
|
|
@ -8,6 +8,7 @@ import warnings
|
||||||
from bisect import bisect_right
|
from bisect import bisect_right
|
||||||
from types import CodeType
|
from types import CodeType
|
||||||
from types import FrameType
|
from types import FrameType
|
||||||
|
from typing import Any
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
@ -17,6 +18,7 @@ from typing import Union
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import overload
|
from _pytest.compat import overload
|
||||||
from _pytest.compat import TYPE_CHECKING
|
from _pytest.compat import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -144,18 +146,13 @@ class Source:
|
||||||
""" return True if source is parseable, heuristically
|
""" return True if source is parseable, heuristically
|
||||||
deindenting it by default.
|
deindenting it by default.
|
||||||
"""
|
"""
|
||||||
from parser import suite as syntax_checker
|
|
||||||
|
|
||||||
if deindent:
|
if deindent:
|
||||||
source = str(self.deindent())
|
source = str(self.deindent())
|
||||||
else:
|
else:
|
||||||
source = str(self)
|
source = str(self)
|
||||||
try:
|
try:
|
||||||
# compile(source+'\n', "x", "exec")
|
ast.parse(source)
|
||||||
syntax_checker(source + "\n")
|
except (SyntaxError, ValueError, TypeError):
|
||||||
except KeyboardInterrupt:
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
@ -282,7 +279,7 @@ def compile_( # noqa: F811
|
||||||
return s.compile(filename, mode, flags, _genframe=_genframe)
|
return s.compile(filename, mode, flags, _genframe=_genframe)
|
||||||
|
|
||||||
|
|
||||||
def getfslineno(obj) -> Tuple[Optional[Union["Literal['']", py.path.local]], int]:
|
def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]:
|
||||||
""" Return source location (path, lineno) for the given object.
|
""" Return source location (path, lineno) for the given object.
|
||||||
If the source cannot be determined return ("", -1).
|
If the source cannot be determined return ("", -1).
|
||||||
|
|
||||||
|
@ -290,6 +287,13 @@ def getfslineno(obj) -> Tuple[Optional[Union["Literal['']", py.path.local]], int
|
||||||
"""
|
"""
|
||||||
from .code import Code
|
from .code import Code
|
||||||
|
|
||||||
|
# xxx let decorators etc specify a sane ordering
|
||||||
|
# NOTE: this used to be done in _pytest.compat.getfslineno, initially added
|
||||||
|
# in 6ec13a2b9. It ("place_as") appears to be something very custom.
|
||||||
|
obj = get_real_func(obj)
|
||||||
|
if hasattr(obj, "place_as"):
|
||||||
|
obj = obj.place_as
|
||||||
|
|
||||||
try:
|
try:
|
||||||
code = Code(obj)
|
code = Code(obj)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -298,18 +302,16 @@ def getfslineno(obj) -> Tuple[Optional[Union["Literal['']", py.path.local]], int
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return "", -1
|
return "", -1
|
||||||
|
|
||||||
fspath = fn and py.path.local(fn) or None
|
fspath = fn and py.path.local(fn) or ""
|
||||||
lineno = -1
|
lineno = -1
|
||||||
if fspath:
|
if fspath:
|
||||||
try:
|
try:
|
||||||
_, lineno = findsource(obj)
|
_, lineno = findsource(obj)
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
return fspath, lineno
|
||||||
else:
|
else:
|
||||||
fspath = code.path
|
return code.path, code.firstlineno
|
||||||
lineno = code.firstlineno
|
|
||||||
assert isinstance(lineno, int)
|
|
||||||
return fspath, lineno
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,3 +1,39 @@
|
||||||
# Reexport TerminalWriter from here instead of py, to make it easier to
|
from typing import List
|
||||||
# extend or swap our own implementation in the future.
|
from typing import Sequence
|
||||||
from py.io import TerminalWriter as TerminalWriter # noqa: F401
|
|
||||||
|
from py.io import TerminalWriter as BaseTerminalWriter # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalWriter(BaseTerminalWriter):
|
||||||
|
def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None:
|
||||||
|
"""Write lines of source code possibly highlighted.
|
||||||
|
|
||||||
|
Keeping this private for now because the API is clunky. We should discuss how
|
||||||
|
to evolve the terminal writer so we can have more precise color support, for example
|
||||||
|
being able to write part of a line in one color and the rest in another, and so on.
|
||||||
|
"""
|
||||||
|
if indents and len(indents) != len(lines):
|
||||||
|
raise ValueError(
|
||||||
|
"indents size ({}) should have same size as lines ({})".format(
|
||||||
|
len(indents), len(lines)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not indents:
|
||||||
|
indents = [""] * len(lines)
|
||||||
|
source = "\n".join(lines)
|
||||||
|
new_lines = self._highlight(source).splitlines()
|
||||||
|
for indent, new_line in zip(indents, new_lines):
|
||||||
|
self.line(indent + new_line)
|
||||||
|
|
||||||
|
def _highlight(self, source):
|
||||||
|
"""Highlight the given source code according to the "code_highlight" option"""
|
||||||
|
if not self.hasmarkup:
|
||||||
|
return source
|
||||||
|
try:
|
||||||
|
from pygments.formatters.terminal import TerminalFormatter
|
||||||
|
from pygments.lexers.python import PythonLexer
|
||||||
|
from pygments import highlight
|
||||||
|
except ImportError:
|
||||||
|
return source
|
||||||
|
else:
|
||||||
|
return highlight(source, PythonLexer(), TerminalFormatter(bg="dark"))
|
||||||
|
|
|
@ -80,3 +80,24 @@ def saferepr(obj: Any, maxsize: int = 240) -> str:
|
||||||
around the Repr/reprlib functionality of the standard 2.6 lib.
|
around the Repr/reprlib functionality of the standard 2.6 lib.
|
||||||
"""
|
"""
|
||||||
return SafeRepr(maxsize).repr(obj)
|
return SafeRepr(maxsize).repr(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
||||||
|
"""PrettyPrinter that always dispatches (regardless of width)."""
|
||||||
|
|
||||||
|
def _format(self, object, stream, indent, allowance, context, level):
|
||||||
|
p = self._dispatch.get(type(object).__repr__, None)
|
||||||
|
|
||||||
|
objid = id(object)
|
||||||
|
if objid in context or p is None:
|
||||||
|
return super()._format(object, stream, indent, allowance, context, level)
|
||||||
|
|
||||||
|
context[objid] = 1
|
||||||
|
p(self, object, stream, indent, allowance, context, level + 1)
|
||||||
|
del context[objid]
|
||||||
|
|
||||||
|
|
||||||
|
def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False):
|
||||||
|
return AlwaysDispatchingPrettyPrinter(
|
||||||
|
indent=indent, width=width, depth=depth, compact=compact
|
||||||
|
).pformat(object)
|
||||||
|
|
|
@ -7,6 +7,11 @@ from typing import Optional
|
||||||
from _pytest.assertion import rewrite
|
from _pytest.assertion import rewrite
|
||||||
from _pytest.assertion import truncate
|
from _pytest.assertion import truncate
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
|
from _pytest.compat import TYPE_CHECKING
|
||||||
|
from _pytest.config import hookimpl
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from _pytest.main import Session
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
|
@ -91,7 +96,7 @@ def install_importhook(config):
|
||||||
return hook
|
return hook
|
||||||
|
|
||||||
|
|
||||||
def pytest_collection(session):
|
def pytest_collection(session: "Session") -> None:
|
||||||
# this hook is only called when test modules are collected
|
# this hook is only called when test modules are collected
|
||||||
# so for example not in the master process of pytest-xdist
|
# so for example not in the master process of pytest-xdist
|
||||||
# (which does not collect test modules)
|
# (which does not collect test modules)
|
||||||
|
@ -101,7 +106,8 @@ def pytest_collection(session):
|
||||||
assertstate.hook.set_session(session)
|
assertstate.hook.set_session(session)
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
|
def pytest_runtest_protocol(item):
|
||||||
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks
|
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks
|
||||||
|
|
||||||
The newinterpret and rewrite modules will use util._reprcompare if
|
The newinterpret and rewrite modules will use util._reprcompare if
|
||||||
|
@ -139,6 +145,7 @@ def pytest_runtest_setup(item):
|
||||||
return res
|
return res
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
saved_assert_hooks = util._reprcompare, util._assertion_pass
|
||||||
util._reprcompare = callbinrepr
|
util._reprcompare = callbinrepr
|
||||||
|
|
||||||
if item.ihook.pytest_assertion_pass.get_hookimpls():
|
if item.ihook.pytest_assertion_pass.get_hookimpls():
|
||||||
|
@ -150,10 +157,9 @@ def pytest_runtest_setup(item):
|
||||||
|
|
||||||
util._assertion_pass = call_assertion_pass_hook
|
util._assertion_pass = call_assertion_pass_hook
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
def pytest_runtest_teardown(item):
|
util._reprcompare, util._assertion_pass = saved_assert_hooks
|
||||||
util._reprcompare = None
|
|
||||||
util._assertion_pass = None
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_sessionfinish(session):
|
def pytest_sessionfinish(session):
|
||||||
|
|
|
@ -13,6 +13,7 @@ from typing import Tuple
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
|
from _pytest._io.saferepr import _pformat_dispatch
|
||||||
from _pytest._io.saferepr import safeformat
|
from _pytest._io.saferepr import safeformat
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.compat import ATTRS_EQ_FIELD
|
from _pytest.compat import ATTRS_EQ_FIELD
|
||||||
|
@ -28,27 +29,6 @@ _reprcompare = None # type: Optional[Callable[[str, object, object], Optional[s
|
||||||
_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
|
_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
|
||||||
|
|
||||||
|
|
||||||
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
|
||||||
"""PrettyPrinter that always dispatches (regardless of width)."""
|
|
||||||
|
|
||||||
def _format(self, object, stream, indent, allowance, context, level):
|
|
||||||
p = self._dispatch.get(type(object).__repr__, None)
|
|
||||||
|
|
||||||
objid = id(object)
|
|
||||||
if objid in context or p is None:
|
|
||||||
return super()._format(object, stream, indent, allowance, context, level)
|
|
||||||
|
|
||||||
context[objid] = 1
|
|
||||||
p(self, object, stream, indent, allowance, context, level + 1)
|
|
||||||
del context[objid]
|
|
||||||
|
|
||||||
|
|
||||||
def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False):
|
|
||||||
return AlwaysDispatchingPrettyPrinter(
|
|
||||||
indent=1, width=80, depth=None, compact=False
|
|
||||||
).pformat(object)
|
|
||||||
|
|
||||||
|
|
||||||
def format_explanation(explanation: str) -> str:
|
def format_explanation(explanation: str) -> str:
|
||||||
"""This formats an explanation
|
"""This formats an explanation
|
||||||
|
|
||||||
|
@ -195,9 +175,10 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
explanation = [
|
explanation = [
|
||||||
"(pytest_assertion plugin: representation of details failed. "
|
"(pytest_assertion plugin: representation of details failed: {}.".format(
|
||||||
"Probably an object has a faulty __repr__.)",
|
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
|
||||||
str(_pytest._code.ExceptionInfo.from_current()),
|
),
|
||||||
|
" Probably an object has a faulty __repr__.)",
|
||||||
]
|
]
|
||||||
|
|
||||||
if not explanation:
|
if not explanation:
|
||||||
|
@ -245,9 +226,11 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||||
left = repr(str(left))
|
left = repr(str(left))
|
||||||
right = repr(str(right))
|
right = repr(str(right))
|
||||||
explanation += ["Strings contain only whitespace, escaping them using repr()"]
|
explanation += ["Strings contain only whitespace, escaping them using repr()"]
|
||||||
|
# "right" is the expected base against which we compare "left",
|
||||||
|
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||||
explanation += [
|
explanation += [
|
||||||
line.strip("\n")
|
line.strip("\n")
|
||||||
for line in ndiff(left.splitlines(keepends), right.splitlines(keepends))
|
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
|
||||||
]
|
]
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
@ -258,8 +241,8 @@ def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
|
||||||
right_lines = repr(right).splitlines(keepends)
|
right_lines = repr(right).splitlines(keepends)
|
||||||
|
|
||||||
explanation = [] # type: List[str]
|
explanation = [] # type: List[str]
|
||||||
explanation += ["-" + line for line in left_lines]
|
explanation += ["+" + line for line in left_lines]
|
||||||
explanation += ["+" + line for line in right_lines]
|
explanation += ["-" + line for line in right_lines]
|
||||||
|
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
@ -299,8 +282,10 @@ def _compare_eq_iterable(
|
||||||
_surrounding_parens_on_own_lines(right_formatting)
|
_surrounding_parens_on_own_lines(right_formatting)
|
||||||
|
|
||||||
explanation = ["Full diff:"]
|
explanation = ["Full diff:"]
|
||||||
|
# "right" is the expected base against which we compare "left",
|
||||||
|
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||||
explanation.extend(
|
explanation.extend(
|
||||||
line.rstrip() for line in difflib.ndiff(left_formatting, right_formatting)
|
line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
|
||||||
)
|
)
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
@ -335,8 +320,9 @@ def _compare_eq_sequence(
|
||||||
break
|
break
|
||||||
|
|
||||||
if comparing_bytes:
|
if comparing_bytes:
|
||||||
# when comparing bytes, it doesn't help to show the "sides contain one or more items"
|
# when comparing bytes, it doesn't help to show the "sides contain one or more
|
||||||
# longer explanation, so skip it
|
# items" longer explanation, so skip it
|
||||||
|
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
len_diff = len_left - len_right
|
len_diff = len_left - len_right
|
||||||
|
@ -463,7 +449,7 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
|
||||||
head = text[:index]
|
head = text[:index]
|
||||||
tail = text[index + len(term) :]
|
tail = text[index + len(term) :]
|
||||||
correct_text = head + tail
|
correct_text = head + tail
|
||||||
diff = _diff_text(correct_text, text, verbose)
|
diff = _diff_text(text, correct_text, verbose)
|
||||||
newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
|
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"):
|
||||||
|
|
|
@ -71,10 +71,10 @@ class Cache:
|
||||||
return resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
return resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
||||||
|
|
||||||
def warn(self, fmt, **args):
|
def warn(self, fmt, **args):
|
||||||
from _pytest.warnings import _issue_warning_captured
|
import warnings
|
||||||
from _pytest.warning_types import PytestCacheWarning
|
from _pytest.warning_types import PytestCacheWarning
|
||||||
|
|
||||||
_issue_warning_captured(
|
warnings.warn(
|
||||||
PytestCacheWarning(fmt.format(**args) if args else fmt),
|
PytestCacheWarning(fmt.format(**args) if args else fmt),
|
||||||
self._config.hook,
|
self._config.hook,
|
||||||
stacklevel=3,
|
stacklevel=3,
|
||||||
|
@ -259,7 +259,7 @@ class LFPlugin:
|
||||||
self._report_status = "no previously failed tests, "
|
self._report_status = "no previously failed tests, "
|
||||||
if self.config.getoption("last_failed_no_failures") == "none":
|
if self.config.getoption("last_failed_no_failures") == "none":
|
||||||
self._report_status += "deselecting all items."
|
self._report_status += "deselecting all items."
|
||||||
config.hook.pytest_deselected(items=items)
|
config.hook.pytest_deselected(items=items[:])
|
||||||
items[:] = []
|
items[:] = []
|
||||||
else:
|
else:
|
||||||
self._report_status += "not deselecting items."
|
self._report_status += "not deselecting items."
|
||||||
|
|
|
@ -9,9 +9,15 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from io import UnsupportedOperation
|
from io import UnsupportedOperation
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
|
from typing import BinaryIO
|
||||||
|
from typing import Generator
|
||||||
|
from typing import Iterable
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.compat import CaptureAndPassthroughIO
|
||||||
from _pytest.compat import CaptureIO
|
from _pytest.compat import CaptureIO
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
|
|
||||||
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
||||||
|
@ -24,8 +30,8 @@ def pytest_addoption(parser):
|
||||||
action="store",
|
action="store",
|
||||||
default="fd" if hasattr(os, "dup") else "sys",
|
default="fd" if hasattr(os, "dup") else "sys",
|
||||||
metavar="method",
|
metavar="method",
|
||||||
choices=["fd", "sys", "no"],
|
choices=["fd", "sys", "no", "tee-sys"],
|
||||||
help="per-test capturing method: one of fd|sys|no.",
|
help="per-test capturing method: one of fd|sys|no|tee-sys.",
|
||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"-s",
|
"-s",
|
||||||
|
@ -37,7 +43,7 @@ def pytest_addoption(parser):
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_load_initial_conftests(early_config, parser, args):
|
def pytest_load_initial_conftests(early_config: Config):
|
||||||
ns = early_config.known_args_namespace
|
ns = early_config.known_args_namespace
|
||||||
if ns.capture == "fd":
|
if ns.capture == "fd":
|
||||||
_py36_windowsconsoleio_workaround(sys.stdout)
|
_py36_windowsconsoleio_workaround(sys.stdout)
|
||||||
|
@ -73,14 +79,14 @@ class CaptureManager:
|
||||||
case special handling is needed to ensure the fixtures take precedence over the global capture.
|
case special handling is needed to ensure the fixtures take precedence over the global capture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, method):
|
def __init__(self, method) -> None:
|
||||||
self._method = method
|
self._method = method
|
||||||
self._global_capturing = None
|
self._global_capturing = None
|
||||||
self._current_item = None
|
self._capture_fixture = None # type: Optional[CaptureFixture]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<CaptureManager _method={!r} _global_capturing={!r} _current_item={!r}>".format(
|
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
|
||||||
self._method, self._global_capturing, self._current_item
|
self._method, self._global_capturing, self._capture_fixture
|
||||||
)
|
)
|
||||||
|
|
||||||
def _getcapture(self, method):
|
def _getcapture(self, method):
|
||||||
|
@ -90,16 +96,15 @@ class CaptureManager:
|
||||||
return MultiCapture(out=True, err=True, Capture=SysCapture)
|
return MultiCapture(out=True, err=True, Capture=SysCapture)
|
||||||
elif method == "no":
|
elif method == "no":
|
||||||
return MultiCapture(out=False, err=False, in_=False)
|
return MultiCapture(out=False, err=False, in_=False)
|
||||||
|
elif method == "tee-sys":
|
||||||
|
return MultiCapture(out=True, err=True, in_=False, Capture=TeeSysCapture)
|
||||||
raise ValueError("unknown capturing method: %r" % method) # pragma: no cover
|
raise ValueError("unknown capturing method: %r" % method) # pragma: no cover
|
||||||
|
|
||||||
def is_capturing(self):
|
def is_capturing(self):
|
||||||
if self.is_globally_capturing():
|
if self.is_globally_capturing():
|
||||||
return "global"
|
return "global"
|
||||||
capture_fixture = getattr(self._current_item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if capture_fixture is not None:
|
return "fixture %s" % self._capture_fixture.request.fixturename
|
||||||
return (
|
|
||||||
"fixture %s" % self._current_item._capture_fixture.request.fixturename
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Global capturing control
|
# Global capturing control
|
||||||
|
@ -131,41 +136,66 @@ class CaptureManager:
|
||||||
|
|
||||||
def suspend(self, in_=False):
|
def suspend(self, in_=False):
|
||||||
# Need to undo local capsys-et-al if it exists before disabling global capture.
|
# Need to undo local capsys-et-al if it exists before disabling global capture.
|
||||||
self.suspend_fixture(self._current_item)
|
self.suspend_fixture()
|
||||||
self.suspend_global_capture(in_)
|
self.suspend_global_capture(in_)
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
self.resume_global_capture()
|
self.resume_global_capture()
|
||||||
self.resume_fixture(self._current_item)
|
self.resume_fixture()
|
||||||
|
|
||||||
def read_global_capture(self):
|
def read_global_capture(self):
|
||||||
return self._global_capturing.readouterr()
|
return self._global_capturing.readouterr()
|
||||||
|
|
||||||
# Fixture Control (it's just forwarding, think about removing this later)
|
# Fixture Control (it's just forwarding, think about removing this later)
|
||||||
|
|
||||||
def activate_fixture(self, item):
|
@contextlib.contextmanager
|
||||||
|
def _capturing_for_request(
|
||||||
|
self, request: FixtureRequest
|
||||||
|
) -> Generator["CaptureFixture", None, None]:
|
||||||
|
"""
|
||||||
|
Context manager that creates a ``CaptureFixture`` instance for the
|
||||||
|
given ``request``, ensuring there is only a single one being requested
|
||||||
|
at the same time.
|
||||||
|
|
||||||
|
This is used as a helper with ``capsys``, ``capfd`` etc.
|
||||||
|
"""
|
||||||
|
if self._capture_fixture:
|
||||||
|
other_name = next(
|
||||||
|
k
|
||||||
|
for k, v in map_fixname_class.items()
|
||||||
|
if v is self._capture_fixture.captureclass
|
||||||
|
)
|
||||||
|
raise request.raiseerror(
|
||||||
|
"cannot use {} and {} at the same time".format(
|
||||||
|
request.fixturename, other_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
capture_class = map_fixname_class[request.fixturename]
|
||||||
|
self._capture_fixture = CaptureFixture(capture_class, request)
|
||||||
|
self.activate_fixture()
|
||||||
|
yield self._capture_fixture
|
||||||
|
self._capture_fixture.close()
|
||||||
|
self._capture_fixture = None
|
||||||
|
|
||||||
|
def activate_fixture(self):
|
||||||
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
|
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
|
||||||
the global capture.
|
the global capture.
|
||||||
"""
|
"""
|
||||||
fixture = getattr(item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if fixture is not None:
|
self._capture_fixture._start()
|
||||||
fixture._start()
|
|
||||||
|
|
||||||
def deactivate_fixture(self, item):
|
def deactivate_fixture(self):
|
||||||
"""Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
"""Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
||||||
fixture = getattr(item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if fixture is not None:
|
self._capture_fixture.close()
|
||||||
fixture.close()
|
|
||||||
|
|
||||||
def suspend_fixture(self, item):
|
def suspend_fixture(self):
|
||||||
fixture = getattr(item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if fixture is not None:
|
self._capture_fixture._suspend()
|
||||||
fixture._suspend()
|
|
||||||
|
|
||||||
def resume_fixture(self, item):
|
def resume_fixture(self):
|
||||||
fixture = getattr(item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if fixture is not None:
|
self._capture_fixture._resume()
|
||||||
fixture._resume()
|
|
||||||
|
|
||||||
# Helper context managers
|
# Helper context managers
|
||||||
|
|
||||||
|
@ -181,11 +211,11 @@ class CaptureManager:
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def item_capture(self, when, item):
|
def item_capture(self, when, item):
|
||||||
self.resume_global_capture()
|
self.resume_global_capture()
|
||||||
self.activate_fixture(item)
|
self.activate_fixture()
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
self.deactivate_fixture(item)
|
self.deactivate_fixture()
|
||||||
self.suspend_global_capture(in_=False)
|
self.suspend_global_capture(in_=False)
|
||||||
|
|
||||||
out, err = self.read_global_capture()
|
out, err = self.read_global_capture()
|
||||||
|
@ -209,12 +239,6 @@ class CaptureManager:
|
||||||
else:
|
else:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
|
||||||
def pytest_runtest_protocol(self, item):
|
|
||||||
self._current_item = item
|
|
||||||
yield
|
|
||||||
self._current_item = None
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_runtest_setup(self, item):
|
def pytest_runtest_setup(self, item):
|
||||||
with self.item_capture("setup", item):
|
with self.item_capture("setup", item):
|
||||||
|
@ -239,18 +263,6 @@ class CaptureManager:
|
||||||
self.stop_global_capturing()
|
self.stop_global_capturing()
|
||||||
|
|
||||||
|
|
||||||
capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"}
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_only_one_capture_fixture(request: FixtureRequest, name):
|
|
||||||
fixtures = sorted(set(request.fixturenames) & capture_fixtures - {name})
|
|
||||||
if fixtures:
|
|
||||||
arg = fixtures[0] if len(fixtures) == 1 else fixtures
|
|
||||||
raise request.raiseerror(
|
|
||||||
"cannot use {} and {} at the same time".format(arg, name)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def capsys(request):
|
def capsys(request):
|
||||||
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||||
|
@ -259,8 +271,8 @@ def capsys(request):
|
||||||
calls, which return a ``(out, err)`` namedtuple.
|
calls, which return a ``(out, err)`` namedtuple.
|
||||||
``out`` and ``err`` will be ``text`` objects.
|
``out`` and ``err`` will be ``text`` objects.
|
||||||
"""
|
"""
|
||||||
_ensure_only_one_capture_fixture(request, "capsys")
|
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
with _install_capture_fixture_on_item(request, SysCapture) as fixture:
|
with capman._capturing_for_request(request) as fixture:
|
||||||
yield fixture
|
yield fixture
|
||||||
|
|
||||||
|
|
||||||
|
@ -272,8 +284,8 @@ def capsysbinary(request):
|
||||||
method calls, which return a ``(out, err)`` namedtuple.
|
method calls, which return a ``(out, err)`` namedtuple.
|
||||||
``out`` and ``err`` will be ``bytes`` objects.
|
``out`` and ``err`` will be ``bytes`` objects.
|
||||||
"""
|
"""
|
||||||
_ensure_only_one_capture_fixture(request, "capsysbinary")
|
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture:
|
with capman._capturing_for_request(request) as fixture:
|
||||||
yield fixture
|
yield fixture
|
||||||
|
|
||||||
|
|
||||||
|
@ -285,12 +297,12 @@ def capfd(request):
|
||||||
calls, which return a ``(out, err)`` namedtuple.
|
calls, which return a ``(out, err)`` namedtuple.
|
||||||
``out`` and ``err`` will be ``text`` objects.
|
``out`` and ``err`` will be ``text`` objects.
|
||||||
"""
|
"""
|
||||||
_ensure_only_one_capture_fixture(request, "capfd")
|
|
||||||
if not hasattr(os, "dup"):
|
if not hasattr(os, "dup"):
|
||||||
pytest.skip(
|
pytest.skip(
|
||||||
"capfd fixture needs os.dup function which is not available in this system"
|
"capfd fixture needs os.dup function which is not available in this system"
|
||||||
)
|
)
|
||||||
with _install_capture_fixture_on_item(request, FDCapture) as fixture:
|
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
|
with capman._capturing_for_request(request) as fixture:
|
||||||
yield fixture
|
yield fixture
|
||||||
|
|
||||||
|
|
||||||
|
@ -302,35 +314,15 @@ def capfdbinary(request):
|
||||||
calls, which return a ``(out, err)`` namedtuple.
|
calls, which return a ``(out, err)`` namedtuple.
|
||||||
``out`` and ``err`` will be ``byte`` objects.
|
``out`` and ``err`` will be ``byte`` objects.
|
||||||
"""
|
"""
|
||||||
_ensure_only_one_capture_fixture(request, "capfdbinary")
|
|
||||||
if not hasattr(os, "dup"):
|
if not hasattr(os, "dup"):
|
||||||
pytest.skip(
|
pytest.skip(
|
||||||
"capfdbinary fixture needs os.dup function which is not available in this system"
|
"capfdbinary fixture needs os.dup function which is not available in this system"
|
||||||
)
|
)
|
||||||
with _install_capture_fixture_on_item(request, FDCaptureBinary) as fixture:
|
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
|
with capman._capturing_for_request(request) as fixture:
|
||||||
yield fixture
|
yield fixture
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def _install_capture_fixture_on_item(request, capture_class):
|
|
||||||
"""
|
|
||||||
Context manager which creates a ``CaptureFixture`` instance and "installs" it on
|
|
||||||
the item/node of the given request. Used by ``capsys`` and ``capfd``.
|
|
||||||
|
|
||||||
The CaptureFixture is added as attribute of the item because it needs to accessed
|
|
||||||
by ``CaptureManager`` during its ``pytest_runtest_*`` hooks.
|
|
||||||
"""
|
|
||||||
request.node._capture_fixture = fixture = CaptureFixture(capture_class, request)
|
|
||||||
capmanager = request.config.pluginmanager.getplugin("capturemanager")
|
|
||||||
# Need to active this fixture right away in case it is being used by another fixture (setup phase).
|
|
||||||
# If this fixture is being used only by a test function (call phase), then we wouldn't need this
|
|
||||||
# activation, but it doesn't hurt.
|
|
||||||
capmanager.activate_fixture(request.node)
|
|
||||||
yield fixture
|
|
||||||
fixture.close()
|
|
||||||
del request.node._capture_fixture
|
|
||||||
|
|
||||||
|
|
||||||
class CaptureFixture:
|
class CaptureFixture:
|
||||||
"""
|
"""
|
||||||
Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary`
|
Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary`
|
||||||
|
@ -413,30 +405,27 @@ def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||||
class EncodedFile:
|
class EncodedFile:
|
||||||
errors = "strict" # possibly needed by py3 code (issue555)
|
errors = "strict" # possibly needed by py3 code (issue555)
|
||||||
|
|
||||||
def __init__(self, buffer, encoding):
|
def __init__(self, buffer: BinaryIO, encoding: str) -> None:
|
||||||
self.buffer = buffer
|
self.buffer = buffer
|
||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
|
|
||||||
def write(self, obj):
|
def write(self, s: str) -> int:
|
||||||
if isinstance(obj, str):
|
if not isinstance(s, str):
|
||||||
obj = obj.encode(self.encoding, "replace")
|
|
||||||
else:
|
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"write() argument must be str, not {}".format(type(obj).__name__)
|
"write() argument must be str, not {}".format(type(s).__name__)
|
||||||
)
|
)
|
||||||
return self.buffer.write(obj)
|
return self.buffer.write(s.encode(self.encoding, "replace"))
|
||||||
|
|
||||||
def writelines(self, linelist):
|
def writelines(self, lines: Iterable[str]) -> None:
|
||||||
data = "".join(linelist)
|
self.buffer.writelines(x.encode(self.encoding, "replace") for x in lines)
|
||||||
self.write(data)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""Ensure that file.name is a string."""
|
"""Ensure that file.name is a string."""
|
||||||
return repr(self.buffer)
|
return repr(self.buffer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mode(self):
|
def mode(self) -> str:
|
||||||
return self.buffer.mode.replace("b", "")
|
return self.buffer.mode.replace("b", "")
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
@ -681,6 +670,19 @@ class SysCapture:
|
||||||
self._old.flush()
|
self._old.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class TeeSysCapture(SysCapture):
|
||||||
|
def __init__(self, fd, tmpfile=None):
|
||||||
|
name = patchsysdict[fd]
|
||||||
|
self._old = getattr(sys, name)
|
||||||
|
self.name = name
|
||||||
|
if tmpfile is None:
|
||||||
|
if name == "stdin":
|
||||||
|
tmpfile = DontReadFromInput()
|
||||||
|
else:
|
||||||
|
tmpfile = CaptureAndPassthroughIO(self._old)
|
||||||
|
self.tmpfile = tmpfile
|
||||||
|
|
||||||
|
|
||||||
class SysCaptureBinary(SysCapture):
|
class SysCaptureBinary(SysCapture):
|
||||||
# Ignore type because it doesn't match the type in the superclass (str).
|
# Ignore type because it doesn't match the type in the superclass (str).
|
||||||
EMPTY_BUFFER = b"" # type: ignore
|
EMPTY_BUFFER = b"" # type: ignore
|
||||||
|
@ -692,6 +694,14 @@ class SysCaptureBinary(SysCapture):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
map_fixname_class = {
|
||||||
|
"capfd": FDCapture,
|
||||||
|
"capfdbinary": FDCaptureBinary,
|
||||||
|
"capsys": SysCapture,
|
||||||
|
"capsysbinary": SysCaptureBinary,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DontReadFromInput:
|
class DontReadFromInput:
|
||||||
encoding = None
|
encoding = None
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ from inspect import signature
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
|
from typing import IO
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
@ -22,7 +23,6 @@ from typing import Union
|
||||||
import attr
|
import attr
|
||||||
import py
|
import py
|
||||||
|
|
||||||
import _pytest
|
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
|
@ -143,12 +143,12 @@ def getfuncargnames(
|
||||||
the case of cls, the function is a static method.
|
the case of cls, the function is a static method.
|
||||||
|
|
||||||
The name parameter should be the original name in which the function was collected.
|
The name parameter should be the original name in which the function was collected.
|
||||||
|
|
||||||
@RonnyPfannschmidt: This function should be refactored when we
|
|
||||||
revisit fixtures. The fixture mechanism should ask the node for
|
|
||||||
the fixture names, and not try to obtain directly from the
|
|
||||||
function object well after collection has occurred.
|
|
||||||
"""
|
"""
|
||||||
|
# TODO(RonnyPfannschmidt): This function should be refactored when we
|
||||||
|
# revisit fixtures. The fixture mechanism should ask the node for
|
||||||
|
# the fixture names, and not try to obtain directly from the
|
||||||
|
# function object well after collection has occurred.
|
||||||
|
|
||||||
# The parameters attribute of a Signature object contains an
|
# The parameters attribute of a Signature object contains an
|
||||||
# ordered mapping of parameter names to Parameter instances. This
|
# ordered mapping of parameter names to Parameter instances. This
|
||||||
# creates a tuple of the names of the parameters that don't have
|
# creates a tuple of the names of the parameters that don't have
|
||||||
|
@ -307,16 +307,6 @@ def get_real_method(obj, holder):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def getfslineno(obj) -> Tuple[Union[str, py.path.local], int]:
|
|
||||||
# xxx let decorators etc specify a sane ordering
|
|
||||||
obj = get_real_func(obj)
|
|
||||||
if hasattr(obj, "place_as"):
|
|
||||||
obj = obj.place_as
|
|
||||||
fslineno = _pytest._code.getfslineno(obj)
|
|
||||||
assert isinstance(fslineno[1], int), obj
|
|
||||||
return fslineno
|
|
||||||
|
|
||||||
|
|
||||||
def getimfunc(func):
|
def getimfunc(func):
|
||||||
try:
|
try:
|
||||||
return func.__func__
|
return func.__func__
|
||||||
|
@ -379,6 +369,16 @@ class CaptureIO(io.TextIOWrapper):
|
||||||
return self.buffer.getvalue().decode("UTF-8")
|
return self.buffer.getvalue().decode("UTF-8")
|
||||||
|
|
||||||
|
|
||||||
|
class CaptureAndPassthroughIO(CaptureIO):
|
||||||
|
def __init__(self, other: IO) -> None:
|
||||||
|
self._other = other
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def write(self, s) -> int:
|
||||||
|
super().write(s)
|
||||||
|
return self._other.write(s)
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 5, 2):
|
if sys.version_info < (3, 5, 2):
|
||||||
|
|
||||||
def overload(f): # noqa: F811
|
def overload(f): # noqa: F811
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
""" command line options, ini-file and conftest.py processing. """
|
""" command line options, ini-file and conftest.py processing. """
|
||||||
import argparse
|
import argparse
|
||||||
import copy
|
import copy
|
||||||
|
import enum
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
|
@ -27,7 +28,6 @@ from pluggy import HookspecMarker
|
||||||
from pluggy import PluginManager
|
from pluggy import PluginManager
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import _pytest.assertion
|
|
||||||
import _pytest.deprecated
|
import _pytest.deprecated
|
||||||
import _pytest.hookspec # the extension point definitions
|
import _pytest.hookspec # the extension point definitions
|
||||||
from .exceptions import PrintHelp
|
from .exceptions import PrintHelp
|
||||||
|
@ -47,11 +47,43 @@ from _pytest.warning_types import PytestConfigWarning
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
from .argparsing import Argument
|
||||||
|
|
||||||
|
|
||||||
|
_PluggyPlugin = object
|
||||||
|
"""A type to represent plugin objects.
|
||||||
|
Plugins can be any namespace, so we can't narrow it down much, but we use an
|
||||||
|
alias to make the intent clear.
|
||||||
|
Ideally this type would be provided by pluggy itself."""
|
||||||
|
|
||||||
|
|
||||||
hookimpl = HookimplMarker("pytest")
|
hookimpl = HookimplMarker("pytest")
|
||||||
hookspec = HookspecMarker("pytest")
|
hookspec = HookspecMarker("pytest")
|
||||||
|
|
||||||
|
|
||||||
|
class ExitCode(enum.IntEnum):
|
||||||
|
"""
|
||||||
|
.. versionadded:: 5.0
|
||||||
|
|
||||||
|
Encodes the valid exit codes by pytest.
|
||||||
|
|
||||||
|
Currently users and plugins may supply other exit codes as well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: tests passed
|
||||||
|
OK = 0
|
||||||
|
#: tests failed
|
||||||
|
TESTS_FAILED = 1
|
||||||
|
#: pytest was interrupted
|
||||||
|
INTERRUPTED = 2
|
||||||
|
#: an internal error got in the way
|
||||||
|
INTERNAL_ERROR = 3
|
||||||
|
#: pytest was misused
|
||||||
|
USAGE_ERROR = 4
|
||||||
|
#: pytest couldn't find tests
|
||||||
|
NO_TESTS_COLLECTED = 5
|
||||||
|
|
||||||
|
|
||||||
class ConftestImportFailure(Exception):
|
class ConftestImportFailure(Exception):
|
||||||
def __init__(self, path, excinfo):
|
def __init__(self, path, excinfo):
|
||||||
Exception.__init__(self, path, excinfo)
|
Exception.__init__(self, path, excinfo)
|
||||||
|
@ -59,7 +91,7 @@ class ConftestImportFailure(Exception):
|
||||||
self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType]
|
self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType]
|
||||||
|
|
||||||
|
|
||||||
def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]":
|
def main(args=None, plugins=None) -> Union[int, ExitCode]:
|
||||||
""" return exit code, after performing an in-process test run.
|
""" return exit code, after performing an in-process test run.
|
||||||
|
|
||||||
:arg args: list of command line arguments.
|
:arg args: list of command line arguments.
|
||||||
|
@ -67,8 +99,6 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]":
|
||||||
:arg plugins: list of plugin objects to be auto-registered during
|
:arg plugins: list of plugin objects to be auto-registered during
|
||||||
initialization.
|
initialization.
|
||||||
"""
|
"""
|
||||||
from _pytest.main import ExitCode
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
config = _prepareconfig(args, plugins)
|
config = _prepareconfig(args, plugins)
|
||||||
|
@ -184,7 +214,7 @@ def get_config(args=None, plugins=None):
|
||||||
|
|
||||||
if args is not None:
|
if args is not None:
|
||||||
# Handle any "-p no:plugin" args.
|
# Handle any "-p no:plugin" args.
|
||||||
pluginmanager.consider_preparse(args)
|
pluginmanager.consider_preparse(args, exclude_only=True)
|
||||||
|
|
||||||
for spec in default_plugins:
|
for spec in default_plugins:
|
||||||
pluginmanager.import_plugin(spec)
|
pluginmanager.import_plugin(spec)
|
||||||
|
@ -253,6 +283,8 @@ class PytestPluginManager(PluginManager):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
import _pytest.assertion
|
||||||
|
|
||||||
super().__init__("pytest")
|
super().__init__("pytest")
|
||||||
# The objects are module objects, only used generically.
|
# The objects are module objects, only used generically.
|
||||||
self._conftest_plugins = set() # type: Set[object]
|
self._conftest_plugins = set() # type: Set[object]
|
||||||
|
@ -490,7 +522,7 @@ class PytestPluginManager(PluginManager):
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
def consider_preparse(self, args):
|
def consider_preparse(self, args, *, exclude_only=False):
|
||||||
i = 0
|
i = 0
|
||||||
n = len(args)
|
n = len(args)
|
||||||
while i < n:
|
while i < n:
|
||||||
|
@ -507,6 +539,8 @@ class PytestPluginManager(PluginManager):
|
||||||
parg = opt[2:]
|
parg = opt[2:]
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
if exclude_only and not parg.startswith("no:"):
|
||||||
|
continue
|
||||||
self.consider_pluginarg(parg)
|
self.consider_pluginarg(parg)
|
||||||
|
|
||||||
def consider_pluginarg(self, arg):
|
def consider_pluginarg(self, arg):
|
||||||
|
@ -589,7 +623,7 @@ class PytestPluginManager(PluginManager):
|
||||||
_issue_warning_captured(
|
_issue_warning_captured(
|
||||||
PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)),
|
PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)),
|
||||||
self.hook,
|
self.hook,
|
||||||
stacklevel=1,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
mod = sys.modules[importspec]
|
mod = sys.modules[importspec]
|
||||||
|
@ -732,7 +766,7 @@ class Config:
|
||||||
plugins = attr.ib()
|
plugins = attr.ib()
|
||||||
dir = attr.ib(type=Path)
|
dir = attr.ib(type=Path)
|
||||||
|
|
||||||
def __init__(self, pluginmanager, *, invocation_params=None):
|
def __init__(self, pluginmanager, *, invocation_params=None) -> None:
|
||||||
from .argparsing import Parser, FILE_OR_DIR
|
from .argparsing import Parser, FILE_OR_DIR
|
||||||
|
|
||||||
if invocation_params is None:
|
if invocation_params is None:
|
||||||
|
@ -845,11 +879,11 @@ class Config:
|
||||||
config.pluginmanager.consider_pluginarg(x)
|
config.pluginmanager.consider_pluginarg(x)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def _processopt(self, opt):
|
def _processopt(self, opt: "Argument") -> None:
|
||||||
for name in opt._short_opts + opt._long_opts:
|
for name in opt._short_opts + opt._long_opts:
|
||||||
self._opt2dest[name] = opt.dest
|
self._opt2dest[name] = opt.dest
|
||||||
|
|
||||||
if hasattr(opt, "default") and opt.dest:
|
if hasattr(opt, "default"):
|
||||||
if not hasattr(self.option, opt.dest):
|
if not hasattr(self.option, opt.dest):
|
||||||
setattr(self.option, opt.dest, opt.default)
|
setattr(self.option, opt.dest, opt.default)
|
||||||
|
|
||||||
|
@ -857,7 +891,7 @@ class Config:
|
||||||
def pytest_load_initial_conftests(self, early_config):
|
def pytest_load_initial_conftests(self, early_config):
|
||||||
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
|
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
|
||||||
|
|
||||||
def _initini(self, args) -> None:
|
def _initini(self, args: Sequence[str]) -> None:
|
||||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(
|
ns, unknown_args = self._parser.parse_known_and_unknown_args(
|
||||||
args, namespace=copy.copy(self.option)
|
args, namespace=copy.copy(self.option)
|
||||||
)
|
)
|
||||||
|
@ -874,7 +908,7 @@ class Config:
|
||||||
self._parser.addini("minversion", "minimally required pytest version")
|
self._parser.addini("minversion", "minimally required pytest version")
|
||||||
self._override_ini = ns.override_ini or ()
|
self._override_ini = ns.override_ini or ()
|
||||||
|
|
||||||
def _consider_importhook(self, args):
|
def _consider_importhook(self, args: Sequence[str]) -> None:
|
||||||
"""Install the PEP 302 import hook if using assertion rewriting.
|
"""Install the PEP 302 import hook if using assertion rewriting.
|
||||||
|
|
||||||
Needs to parse the --assert=<mode> option from the commandline
|
Needs to parse the --assert=<mode> option from the commandline
|
||||||
|
@ -884,6 +918,8 @@ class Config:
|
||||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
|
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
|
||||||
mode = getattr(ns, "assertmode", "plain")
|
mode = getattr(ns, "assertmode", "plain")
|
||||||
if mode == "rewrite":
|
if mode == "rewrite":
|
||||||
|
import _pytest.assertion
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hook = _pytest.assertion.install_importhook(self)
|
hook = _pytest.assertion.install_importhook(self)
|
||||||
except SystemError:
|
except SystemError:
|
||||||
|
@ -914,19 +950,19 @@ class Config:
|
||||||
for name in _iter_rewritable_modules(package_files):
|
for name in _iter_rewritable_modules(package_files):
|
||||||
hook.mark_rewrite(name)
|
hook.mark_rewrite(name)
|
||||||
|
|
||||||
def _validate_args(self, args, via):
|
def _validate_args(self, args: List[str], via: str) -> List[str]:
|
||||||
"""Validate known args."""
|
"""Validate known args."""
|
||||||
self._parser._config_source_hint = via
|
self._parser._config_source_hint = via # type: ignore
|
||||||
try:
|
try:
|
||||||
self._parser.parse_known_and_unknown_args(
|
self._parser.parse_known_and_unknown_args(
|
||||||
args, namespace=copy.copy(self.option)
|
args, namespace=copy.copy(self.option)
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
del self._parser._config_source_hint
|
del self._parser._config_source_hint # type: ignore
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def _preparse(self, args, addopts=True):
|
def _preparse(self, args: List[str], addopts: bool = True) -> None:
|
||||||
if addopts:
|
if addopts:
|
||||||
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
||||||
if len(env_addopts):
|
if len(env_addopts):
|
||||||
|
@ -942,7 +978,7 @@ class Config:
|
||||||
|
|
||||||
self._checkversion()
|
self._checkversion()
|
||||||
self._consider_importhook(args)
|
self._consider_importhook(args)
|
||||||
self.pluginmanager.consider_preparse(args)
|
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 setuptools entry point. Only explicitly specified
|
# Don't autoload from setuptools entry point. Only explicitly specified
|
||||||
# plugins are going to be loaded.
|
# plugins are going to be loaded.
|
||||||
|
@ -990,7 +1026,7 @@ class Config:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse(self, args, addopts=True):
|
def parse(self, args: List[str], addopts: bool = True) -> None:
|
||||||
# parse given cmdline arguments into this config object.
|
# parse given cmdline arguments into this config object.
|
||||||
assert not hasattr(
|
assert not hasattr(
|
||||||
self, "args"
|
self, "args"
|
||||||
|
@ -1001,7 +1037,7 @@ class Config:
|
||||||
self._preparse(args, addopts=addopts)
|
self._preparse(args, addopts=addopts)
|
||||||
# XXX deprecated hook:
|
# XXX deprecated hook:
|
||||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||||
self._parser.after_preparse = True
|
self._parser.after_preparse = True # type: ignore
|
||||||
try:
|
try:
|
||||||
args = self._parser.parse_setoption(
|
args = self._parser.parse_setoption(
|
||||||
args, self.option, namespace=self.option
|
args, self.option, namespace=self.option
|
||||||
|
|
|
@ -3,15 +3,25 @@ import sys
|
||||||
import warnings
|
import warnings
|
||||||
from gettext import gettext
|
from gettext import gettext
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
from _pytest.compat import TYPE_CHECKING
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import NoReturn
|
||||||
|
from typing_extensions import Literal # noqa: F401
|
||||||
|
|
||||||
FILE_OR_DIR = "file_or_dir"
|
FILE_OR_DIR = "file_or_dir"
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,9 +32,13 @@ class Parser:
|
||||||
there's an error processing the command line arguments.
|
there's an error processing the command line arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
prog = None
|
prog = None # type: Optional[str]
|
||||||
|
|
||||||
def __init__(self, usage=None, processopt=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
usage: Optional[str] = None,
|
||||||
|
processopt: Optional[Callable[["Argument"], None]] = None,
|
||||||
|
) -> None:
|
||||||
self._anonymous = OptionGroup("custom options", parser=self)
|
self._anonymous = OptionGroup("custom options", parser=self)
|
||||||
self._groups = [] # type: List[OptionGroup]
|
self._groups = [] # type: List[OptionGroup]
|
||||||
self._processopt = processopt
|
self._processopt = processopt
|
||||||
|
@ -33,12 +47,14 @@ class Parser:
|
||||||
self._ininames = [] # type: List[str]
|
self._ininames = [] # type: List[str]
|
||||||
self.extra_info = {} # type: Dict[str, Any]
|
self.extra_info = {} # type: Dict[str, Any]
|
||||||
|
|
||||||
def processoption(self, option):
|
def processoption(self, option: "Argument") -> None:
|
||||||
if self._processopt:
|
if self._processopt:
|
||||||
if option.dest:
|
if option.dest:
|
||||||
self._processopt(option)
|
self._processopt(option)
|
||||||
|
|
||||||
def getgroup(self, name, description="", after=None):
|
def getgroup(
|
||||||
|
self, name: str, description: str = "", after: Optional[str] = None
|
||||||
|
) -> "OptionGroup":
|
||||||
""" get (or create) a named option Group.
|
""" get (or create) a named option Group.
|
||||||
|
|
||||||
:name: name of the option group.
|
:name: name of the option group.
|
||||||
|
@ -61,13 +77,13 @@ class Parser:
|
||||||
self._groups.insert(i + 1, group)
|
self._groups.insert(i + 1, group)
|
||||||
return group
|
return group
|
||||||
|
|
||||||
def addoption(self, *opts, **attrs):
|
def addoption(self, *opts: str, **attrs: Any) -> None:
|
||||||
""" register a command line option.
|
""" register a command line option.
|
||||||
|
|
||||||
:opts: option names, can be short or long options.
|
:opts: option names, can be short or long options.
|
||||||
:attrs: same attributes which the ``add_option()`` function of the
|
:attrs: same attributes which the ``add_argument()`` function of the
|
||||||
`argparse library
|
`argparse library
|
||||||
<http://docs.python.org/2/library/argparse.html>`_
|
<https://docs.python.org/library/argparse.html>`_
|
||||||
accepts.
|
accepts.
|
||||||
|
|
||||||
After command line parsing options are available on the pytest config
|
After command line parsing options are available on the pytest config
|
||||||
|
@ -77,7 +93,11 @@ class Parser:
|
||||||
"""
|
"""
|
||||||
self._anonymous.addoption(*opts, **attrs)
|
self._anonymous.addoption(*opts, **attrs)
|
||||||
|
|
||||||
def parse(self, args, namespace=None):
|
def parse(
|
||||||
|
self,
|
||||||
|
args: Sequence[Union[str, py.path.local]],
|
||||||
|
namespace: Optional[argparse.Namespace] = None,
|
||||||
|
) -> argparse.Namespace:
|
||||||
from _pytest._argcomplete import try_argcomplete
|
from _pytest._argcomplete import try_argcomplete
|
||||||
|
|
||||||
self.optparser = self._getparser()
|
self.optparser = self._getparser()
|
||||||
|
@ -98,27 +118,37 @@ class Parser:
|
||||||
n = option.names()
|
n = option.names()
|
||||||
a = option.attrs()
|
a = option.attrs()
|
||||||
arggroup.add_argument(*n, **a)
|
arggroup.add_argument(*n, **a)
|
||||||
|
file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*")
|
||||||
# bash like autocompletion for dirs (appending '/')
|
# bash like autocompletion for dirs (appending '/')
|
||||||
# Type ignored because typeshed doesn't know about argcomplete.
|
# Type ignored because typeshed doesn't know about argcomplete.
|
||||||
optparser.add_argument( # type: ignore
|
file_or_dir_arg.completer = filescompleter # type: ignore
|
||||||
FILE_OR_DIR, nargs="*"
|
|
||||||
).completer = filescompleter
|
|
||||||
return optparser
|
return optparser
|
||||||
|
|
||||||
def parse_setoption(self, args, option, namespace=None):
|
def parse_setoption(
|
||||||
|
self,
|
||||||
|
args: Sequence[Union[str, py.path.local]],
|
||||||
|
option: argparse.Namespace,
|
||||||
|
namespace: Optional[argparse.Namespace] = None,
|
||||||
|
) -> List[str]:
|
||||||
parsedoption = self.parse(args, namespace=namespace)
|
parsedoption = self.parse(args, namespace=namespace)
|
||||||
for name, value in parsedoption.__dict__.items():
|
for name, value in parsedoption.__dict__.items():
|
||||||
setattr(option, name, value)
|
setattr(option, name, value)
|
||||||
return getattr(parsedoption, FILE_OR_DIR)
|
return cast(List[str], getattr(parsedoption, FILE_OR_DIR))
|
||||||
|
|
||||||
def parse_known_args(self, args, namespace=None) -> argparse.Namespace:
|
def parse_known_args(
|
||||||
|
self,
|
||||||
|
args: Sequence[Union[str, py.path.local]],
|
||||||
|
namespace: Optional[argparse.Namespace] = None,
|
||||||
|
) -> argparse.Namespace:
|
||||||
"""parses and returns a namespace object with known arguments at this
|
"""parses and returns a namespace object with known arguments at this
|
||||||
point.
|
point.
|
||||||
"""
|
"""
|
||||||
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
|
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
|
||||||
|
|
||||||
def parse_known_and_unknown_args(
|
def parse_known_and_unknown_args(
|
||||||
self, args, namespace=None
|
self,
|
||||||
|
args: Sequence[Union[str, py.path.local]],
|
||||||
|
namespace: Optional[argparse.Namespace] = None,
|
||||||
) -> Tuple[argparse.Namespace, List[str]]:
|
) -> Tuple[argparse.Namespace, List[str]]:
|
||||||
"""parses and returns a namespace object with known arguments, and
|
"""parses and returns a namespace object with known arguments, and
|
||||||
the remaining arguments unknown at this point.
|
the remaining arguments unknown at this point.
|
||||||
|
@ -127,7 +157,13 @@ class Parser:
|
||||||
strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
||||||
return optparser.parse_known_args(strargs, namespace=namespace)
|
return optparser.parse_known_args(strargs, namespace=namespace)
|
||||||
|
|
||||||
def addini(self, name, help, type=None, default=None):
|
def addini(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
help: str,
|
||||||
|
type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
|
||||||
|
default=None,
|
||||||
|
) -> None:
|
||||||
""" register an ini-file option.
|
""" register an ini-file option.
|
||||||
|
|
||||||
:name: name of the ini-variable
|
:name: name of the ini-variable
|
||||||
|
@ -149,11 +185,11 @@ class ArgumentError(Exception):
|
||||||
inconsistent arguments.
|
inconsistent arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, msg, option):
|
def __init__(self, msg: str, option: Union["Argument", str]) -> None:
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.option_id = str(option)
|
self.option_id = str(option)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
if self.option_id:
|
if self.option_id:
|
||||||
return "option {}: {}".format(self.option_id, self.msg)
|
return "option {}: {}".format(self.option_id, self.msg)
|
||||||
else:
|
else:
|
||||||
|
@ -170,12 +206,11 @@ class Argument:
|
||||||
|
|
||||||
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
||||||
|
|
||||||
def __init__(self, *names, **attrs):
|
def __init__(self, *names: str, **attrs: Any) -> None:
|
||||||
"""store parms in private vars for use in add_argument"""
|
"""store parms in private vars for use in add_argument"""
|
||||||
self._attrs = attrs
|
self._attrs = attrs
|
||||||
self._short_opts = [] # type: List[str]
|
self._short_opts = [] # type: List[str]
|
||||||
self._long_opts = [] # type: List[str]
|
self._long_opts = [] # type: List[str]
|
||||||
self.dest = attrs.get("dest")
|
|
||||||
if "%default" in (attrs.get("help") or ""):
|
if "%default" in (attrs.get("help") or ""):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
'pytest now uses argparse. "%default" should be'
|
'pytest now uses argparse. "%default" should be'
|
||||||
|
@ -221,23 +256,25 @@ class Argument:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
self._set_opt_strings(names)
|
self._set_opt_strings(names)
|
||||||
if not self.dest:
|
dest = attrs.get("dest") # type: Optional[str]
|
||||||
if self._long_opts:
|
if dest:
|
||||||
self.dest = self._long_opts[0][2:].replace("-", "_")
|
self.dest = dest
|
||||||
else:
|
elif self._long_opts:
|
||||||
try:
|
self.dest = self._long_opts[0][2:].replace("-", "_")
|
||||||
self.dest = self._short_opts[0][1:]
|
else:
|
||||||
except IndexError:
|
try:
|
||||||
raise ArgumentError("need a long or short option", self)
|
self.dest = self._short_opts[0][1:]
|
||||||
|
except IndexError:
|
||||||
|
self.dest = "???" # Needed for the error repr.
|
||||||
|
raise ArgumentError("need a long or short option", self)
|
||||||
|
|
||||||
def names(self):
|
def names(self) -> List[str]:
|
||||||
return self._short_opts + self._long_opts
|
return self._short_opts + self._long_opts
|
||||||
|
|
||||||
def attrs(self):
|
def attrs(self) -> Mapping[str, Any]:
|
||||||
# update any attributes set by processopt
|
# update any attributes set by processopt
|
||||||
attrs = "default dest help".split()
|
attrs = "default dest help".split()
|
||||||
if self.dest:
|
attrs.append(self.dest)
|
||||||
attrs.append(self.dest)
|
|
||||||
for attr in attrs:
|
for attr in attrs:
|
||||||
try:
|
try:
|
||||||
self._attrs[attr] = getattr(self, attr)
|
self._attrs[attr] = getattr(self, attr)
|
||||||
|
@ -250,7 +287,7 @@ class Argument:
|
||||||
self._attrs["help"] = a
|
self._attrs["help"] = a
|
||||||
return self._attrs
|
return self._attrs
|
||||||
|
|
||||||
def _set_opt_strings(self, opts):
|
def _set_opt_strings(self, opts: Sequence[str]) -> None:
|
||||||
"""directly from optparse
|
"""directly from optparse
|
||||||
|
|
||||||
might not be necessary as this is passed to argparse later on"""
|
might not be necessary as this is passed to argparse later on"""
|
||||||
|
@ -293,13 +330,15 @@ class Argument:
|
||||||
|
|
||||||
|
|
||||||
class OptionGroup:
|
class OptionGroup:
|
||||||
def __init__(self, name, description="", parser=None):
|
def __init__(
|
||||||
|
self, name: str, description: str = "", parser: Optional[Parser] = None
|
||||||
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.options = [] # type: List[Argument]
|
self.options = [] # type: List[Argument]
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
|
|
||||||
def addoption(self, *optnames, **attrs):
|
def addoption(self, *optnames: str, **attrs: Any) -> None:
|
||||||
""" add an option to this group.
|
""" add an option to this group.
|
||||||
|
|
||||||
if a shortened version of a long option is specified it will
|
if a shortened version of a long option is specified it will
|
||||||
|
@ -315,11 +354,11 @@ class OptionGroup:
|
||||||
option = Argument(*optnames, **attrs)
|
option = Argument(*optnames, **attrs)
|
||||||
self._addoption_instance(option, shortupper=False)
|
self._addoption_instance(option, shortupper=False)
|
||||||
|
|
||||||
def _addoption(self, *optnames, **attrs):
|
def _addoption(self, *optnames: str, **attrs: Any) -> None:
|
||||||
option = Argument(*optnames, **attrs)
|
option = Argument(*optnames, **attrs)
|
||||||
self._addoption_instance(option, shortupper=True)
|
self._addoption_instance(option, shortupper=True)
|
||||||
|
|
||||||
def _addoption_instance(self, option, shortupper=False):
|
def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
|
||||||
if not shortupper:
|
if not shortupper:
|
||||||
for opt in option._short_opts:
|
for opt in option._short_opts:
|
||||||
if opt[0] == "-" and opt[1].islower():
|
if opt[0] == "-" and opt[1].islower():
|
||||||
|
@ -330,9 +369,12 @@ class OptionGroup:
|
||||||
|
|
||||||
|
|
||||||
class MyOptionParser(argparse.ArgumentParser):
|
class MyOptionParser(argparse.ArgumentParser):
|
||||||
def __init__(self, parser, extra_info=None, prog=None):
|
def __init__(
|
||||||
if not extra_info:
|
self,
|
||||||
extra_info = {}
|
parser: Parser,
|
||||||
|
extra_info: Optional[Dict[str, Any]] = None,
|
||||||
|
prog: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
self._parser = parser
|
self._parser = parser
|
||||||
argparse.ArgumentParser.__init__(
|
argparse.ArgumentParser.__init__(
|
||||||
self,
|
self,
|
||||||
|
@ -344,34 +386,42 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
)
|
)
|
||||||
# extra_info is a dict of (param -> value) to display if there's
|
# extra_info is a dict of (param -> value) to display if there's
|
||||||
# an usage error to provide more contextual information to the user
|
# an usage error to provide more contextual information to the user
|
||||||
self.extra_info = extra_info
|
self.extra_info = extra_info if extra_info else {}
|
||||||
|
|
||||||
def error(self, message):
|
def error(self, message: str) -> "NoReturn":
|
||||||
"""Transform argparse error message into UsageError."""
|
"""Transform argparse error message into UsageError."""
|
||||||
msg = "{}: error: {}".format(self.prog, message)
|
msg = "{}: error: {}".format(self.prog, message)
|
||||||
|
|
||||||
if hasattr(self._parser, "_config_source_hint"):
|
if hasattr(self._parser, "_config_source_hint"):
|
||||||
msg = "{} ({})".format(msg, self._parser._config_source_hint)
|
# Type ignored because the attribute is set dynamically.
|
||||||
|
msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore
|
||||||
|
|
||||||
raise UsageError(self.format_usage() + msg)
|
raise UsageError(self.format_usage() + msg)
|
||||||
|
|
||||||
def parse_args(self, args=None, namespace=None):
|
# Type ignored because typeshed has a very complex type in the superclass.
|
||||||
|
def parse_args( # type: ignore
|
||||||
|
self,
|
||||||
|
args: Optional[Sequence[str]] = None,
|
||||||
|
namespace: Optional[argparse.Namespace] = None,
|
||||||
|
) -> argparse.Namespace:
|
||||||
"""allow splitting of positional arguments"""
|
"""allow splitting of positional arguments"""
|
||||||
args, argv = self.parse_known_args(args, namespace)
|
parsed, unrecognized = self.parse_known_args(args, namespace)
|
||||||
if argv:
|
if unrecognized:
|
||||||
for arg in argv:
|
for arg in unrecognized:
|
||||||
if arg and arg[0] == "-":
|
if arg and arg[0] == "-":
|
||||||
lines = ["unrecognized arguments: %s" % (" ".join(argv))]
|
lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
|
||||||
for k, v in sorted(self.extra_info.items()):
|
for k, v in sorted(self.extra_info.items()):
|
||||||
lines.append(" {}: {}".format(k, v))
|
lines.append(" {}: {}".format(k, v))
|
||||||
self.error("\n".join(lines))
|
self.error("\n".join(lines))
|
||||||
getattr(args, FILE_OR_DIR).extend(argv)
|
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
|
||||||
return args
|
return parsed
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 9): # pragma: no cover
|
if sys.version_info[:2] < (3, 9): # pragma: no cover
|
||||||
# Backport of https://github.com/python/cpython/pull/14316 so we can
|
# Backport of https://github.com/python/cpython/pull/14316 so we can
|
||||||
# disable long --argument abbreviations without breaking short flags.
|
# disable long --argument abbreviations without breaking short flags.
|
||||||
def _parse_optional(self, arg_string):
|
def _parse_optional(
|
||||||
|
self, arg_string: str
|
||||||
|
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
|
||||||
if not arg_string:
|
if not arg_string:
|
||||||
return None
|
return None
|
||||||
if not arg_string[0] in self.prefix_chars:
|
if not arg_string[0] in self.prefix_chars:
|
||||||
|
@ -409,49 +459,45 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||||
"""shorten help for long options that differ only in extra hyphens
|
"""shorten help for long options that differ only in extra hyphens
|
||||||
|
|
||||||
- collapse **long** options that are the same except for extra hyphens
|
- collapse **long** options that are the same except for extra hyphens
|
||||||
- special action attribute map_long_option allows suppressing additional
|
|
||||||
long options
|
|
||||||
- shortcut if there are only two options and one of them is a short one
|
- shortcut if there are only two options and one of them is a short one
|
||||||
- cache result on action object as this is called at least 2 times
|
- cache result on action object as this is called at least 2 times
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""Use more accurate terminal width via pylib."""
|
"""Use more accurate terminal width via pylib."""
|
||||||
if "width" not in kwargs:
|
if "width" not in kwargs:
|
||||||
kwargs["width"] = py.io.get_terminal_width()
|
kwargs["width"] = py.io.get_terminal_width()
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def _format_action_invocation(self, action):
|
def _format_action_invocation(self, action: argparse.Action) -> str:
|
||||||
orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
|
orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
|
||||||
if orgstr and orgstr[0] != "-": # only optional arguments
|
if orgstr and orgstr[0] != "-": # only optional arguments
|
||||||
return orgstr
|
return orgstr
|
||||||
res = getattr(action, "_formatted_action_invocation", None)
|
res = getattr(
|
||||||
|
action, "_formatted_action_invocation", None
|
||||||
|
) # type: Optional[str]
|
||||||
if res:
|
if res:
|
||||||
return res
|
return res
|
||||||
options = orgstr.split(", ")
|
options = orgstr.split(", ")
|
||||||
if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
|
if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
|
||||||
# a shortcut for '-h, --help' or '--abc', '-a'
|
# a shortcut for '-h, --help' or '--abc', '-a'
|
||||||
action._formatted_action_invocation = orgstr
|
action._formatted_action_invocation = orgstr # type: ignore
|
||||||
return orgstr
|
return orgstr
|
||||||
return_list = []
|
return_list = []
|
||||||
option_map = getattr(action, "map_long_option", {})
|
|
||||||
if option_map is None:
|
|
||||||
option_map = {}
|
|
||||||
short_long = {} # type: Dict[str, str]
|
short_long = {} # type: Dict[str, str]
|
||||||
for option in options:
|
for option in options:
|
||||||
if len(option) == 2 or option[2] == " ":
|
if len(option) == 2 or option[2] == " ":
|
||||||
continue
|
continue
|
||||||
if not option.startswith("--"):
|
if not option.startswith("--"):
|
||||||
raise ArgumentError(
|
raise ArgumentError(
|
||||||
'long optional argument without "--": [%s]' % (option), self
|
'long optional argument without "--": [%s]' % (option), option
|
||||||
)
|
)
|
||||||
xxoption = option[2:]
|
xxoption = option[2:]
|
||||||
if xxoption.split()[0] not in option_map:
|
shortened = xxoption.replace("-", "")
|
||||||
shortened = xxoption.replace("-", "")
|
if shortened not in short_long or len(short_long[shortened]) < len(
|
||||||
if shortened not in short_long or len(short_long[shortened]) < len(
|
xxoption
|
||||||
xxoption
|
):
|
||||||
):
|
short_long[shortened] = xxoption
|
||||||
short_long[shortened] = xxoption
|
|
||||||
# now short_long has been filled out to the longest with dashes
|
# now short_long has been filled out to the longest with dashes
|
||||||
# **and** we keep the right option ordering from add_argument
|
# **and** we keep the right option ordering from add_argument
|
||||||
for option in options:
|
for option in options:
|
||||||
|
@ -459,5 +505,6 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||||
return_list.append(option)
|
return_list.append(option)
|
||||||
if option[2:] == short_long.get(option.replace("-", "")):
|
if option[2:] == short_long.get(option.replace("-", "")):
|
||||||
return_list.append(option.replace(" ", "=", 1))
|
return_list.append(option.replace(" ", "=", 1))
|
||||||
action._formatted_action_invocation = ", ".join(return_list)
|
formatted_action_invocation = ", ".join(return_list)
|
||||||
return action._formatted_action_invocation
|
action._formatted_action_invocation = formatted_action_invocation # type: ignore
|
||||||
|
return formatted_action_invocation
|
||||||
|
|
|
@ -9,6 +9,7 @@ All constants defined in this module should be either PytestWarning instances or
|
||||||
in case of warnings which need to format their messages.
|
in case of warnings which need to format their messages.
|
||||||
"""
|
"""
|
||||||
from _pytest.warning_types import PytestDeprecationWarning
|
from _pytest.warning_types import PytestDeprecationWarning
|
||||||
|
from _pytest.warning_types import UnformattedWarning
|
||||||
|
|
||||||
# set of plugins which have been integrated into the core; we use this list to ignore
|
# set of plugins which have been integrated into the core; we use this list to ignore
|
||||||
# them during registration to avoid conflicts
|
# them during registration to avoid conflicts
|
||||||
|
@ -18,13 +19,11 @@ DEPRECATED_EXTERNAL_PLUGINS = {
|
||||||
"pytest_faulthandler",
|
"pytest_faulthandler",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
FUNCARGNAMES = PytestDeprecationWarning(
|
FUNCARGNAMES = PytestDeprecationWarning(
|
||||||
"The `funcargnames` attribute was an alias for `fixturenames`, "
|
"The `funcargnames` attribute was an alias for `fixturenames`, "
|
||||||
"since pytest 2.3 - use the newer attribute instead."
|
"since pytest 2.3 - use the newer attribute instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
RESULT_LOG = PytestDeprecationWarning(
|
RESULT_LOG = PytestDeprecationWarning(
|
||||||
"--result-log is deprecated, please try the new pytest-reportlog plugin.\n"
|
"--result-log is deprecated, please try the new pytest-reportlog plugin.\n"
|
||||||
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
|
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
|
||||||
|
@ -35,8 +34,18 @@ FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning(
|
||||||
"as a keyword argument instead."
|
"as a keyword argument instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NODE_USE_FROM_PARENT = UnformattedWarning(
|
||||||
|
PytestDeprecationWarning,
|
||||||
|
"direct construction of {name} has been deprecated, please use {name}.from_parent",
|
||||||
|
)
|
||||||
|
|
||||||
JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning(
|
JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning(
|
||||||
"The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n"
|
"The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n"
|
||||||
"Add 'junit_family=xunit1' to your pytest.ini file to keep the current format "
|
"Add 'junit_family=xunit1' to your pytest.ini file to keep the current format "
|
||||||
"in future versions of pytest and silence this warning."
|
"in future versions of pytest and silence this warning."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NO_PRINT_LOGS = PytestDeprecationWarning(
|
||||||
|
"--no-print-logs is deprecated and scheduled for removal in pytest 6.0.\n"
|
||||||
|
"Please use --show-capture instead."
|
||||||
|
)
|
||||||
|
|
|
@ -112,9 +112,9 @@ def pytest_collect_file(path, parent):
|
||||||
config = parent.config
|
config = parent.config
|
||||||
if path.ext == ".py":
|
if path.ext == ".py":
|
||||||
if config.option.doctestmodules and not _is_setup_py(config, path, parent):
|
if config.option.doctestmodules and not _is_setup_py(config, path, parent):
|
||||||
return DoctestModule(path, parent)
|
return DoctestModule.from_parent(parent, fspath=path)
|
||||||
elif _is_doctest(config, path, parent):
|
elif _is_doctest(config, path, parent):
|
||||||
return DoctestTextfile(path, parent)
|
return DoctestTextfile.from_parent(parent, fspath=path)
|
||||||
|
|
||||||
|
|
||||||
def _is_setup_py(config, path, parent):
|
def _is_setup_py(config, path, parent):
|
||||||
|
@ -219,6 +219,16 @@ class DoctestItem(pytest.Item):
|
||||||
self.obj = None
|
self.obj = None
|
||||||
self.fixture_request = None
|
self.fixture_request = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_parent( # type: ignore
|
||||||
|
cls, parent: "Union[DoctestTextfile, DoctestModule]", *, name, runner, dtest
|
||||||
|
):
|
||||||
|
# incompatible signature due to to imposed limits on sublcass
|
||||||
|
"""
|
||||||
|
the public named constructor
|
||||||
|
"""
|
||||||
|
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
if self.dtest is not None:
|
if self.dtest is not None:
|
||||||
self.fixture_request = _setup_fixtures(self)
|
self.fixture_request = _setup_fixtures(self)
|
||||||
|
@ -374,7 +384,9 @@ class DoctestTextfile(pytest.Module):
|
||||||
parser = doctest.DocTestParser()
|
parser = doctest.DocTestParser()
|
||||||
test = parser.get_doctest(text, globs, name, filename, 0)
|
test = parser.get_doctest(text, globs, name, filename, 0)
|
||||||
if test.examples:
|
if test.examples:
|
||||||
yield DoctestItem(test.name, self, runner, test)
|
yield DoctestItem.from_parent(
|
||||||
|
self, name=test.name, runner=runner, dtest=test
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _check_all_skipped(test):
|
def _check_all_skipped(test):
|
||||||
|
@ -483,7 +495,9 @@ class DoctestModule(pytest.Module):
|
||||||
|
|
||||||
for test in finder.find(module, module.__name__):
|
for test in finder.find(module, module.__name__):
|
||||||
if test.examples: # skip empty doctests
|
if test.examples: # skip empty doctests
|
||||||
yield DoctestItem(test.name, self, runner, test)
|
yield DoctestItem.from_parent(
|
||||||
|
self, name=test.name, runner=runner, dtest=test
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _setup_fixtures(doctest_item):
|
def _setup_fixtures(doctest_item):
|
||||||
|
|
|
@ -17,70 +17,92 @@ def pytest_addoption(parser):
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
# avoid trying to dup sys.stderr if faulthandler is already enabled
|
if not faulthandler.is_enabled():
|
||||||
if faulthandler.is_enabled():
|
# faulthhandler is not enabled, so install plugin that does the actual work
|
||||||
return
|
# of enabling faulthandler before each test executes.
|
||||||
|
config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks")
|
||||||
|
else:
|
||||||
|
from _pytest.warnings import _issue_warning_captured
|
||||||
|
|
||||||
stderr_fd_copy = os.dup(_get_stderr_fileno())
|
# Do not handle dumping to stderr if faulthandler is already enabled, so warn
|
||||||
config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w")
|
# users that the option is being ignored.
|
||||||
faulthandler.enable(file=config.fault_handler_stderr)
|
timeout = FaultHandlerHooks.get_timeout_config_value(config)
|
||||||
|
if timeout > 0:
|
||||||
|
_issue_warning_captured(
|
||||||
|
pytest.PytestConfigWarning(
|
||||||
|
"faulthandler module enabled before pytest configuration step, "
|
||||||
|
"'faulthandler_timeout' option ignored"
|
||||||
|
),
|
||||||
|
config.hook,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_stderr_fileno():
|
class FaultHandlerHooks:
|
||||||
try:
|
"""Implements hooks that will actually install fault handler before tests execute,
|
||||||
return sys.stderr.fileno()
|
as well as correctly handle pdb and internal errors."""
|
||||||
except (AttributeError, io.UnsupportedOperation):
|
|
||||||
# python-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
|
||||||
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
|
||||||
# This is potentially dangerous, but the best we can do.
|
|
||||||
return sys.__stderr__.fileno()
|
|
||||||
|
|
||||||
|
def pytest_configure(self, config):
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
def pytest_unconfigure(config):
|
stderr_fd_copy = os.dup(self._get_stderr_fileno())
|
||||||
import faulthandler
|
config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w")
|
||||||
|
faulthandler.enable(file=config.fault_handler_stderr)
|
||||||
|
|
||||||
faulthandler.disable()
|
def pytest_unconfigure(self, config):
|
||||||
# close our dup file installed during pytest_configure
|
import faulthandler
|
||||||
f = getattr(config, "fault_handler_stderr", None)
|
|
||||||
if f is not None:
|
faulthandler.disable()
|
||||||
|
# close our dup file installed during pytest_configure
|
||||||
# re-enable the faulthandler, attaching it to the default sys.stderr
|
# re-enable the faulthandler, attaching it to the default sys.stderr
|
||||||
# so we can see crashes after pytest has finished, usually during
|
# so we can see crashes after pytest has finished, usually during
|
||||||
# garbage collection during interpreter shutdown
|
# garbage collection during interpreter shutdown
|
||||||
config.fault_handler_stderr.close()
|
config.fault_handler_stderr.close()
|
||||||
del config.fault_handler_stderr
|
del config.fault_handler_stderr
|
||||||
faulthandler.enable(file=_get_stderr_fileno())
|
faulthandler.enable(file=self._get_stderr_fileno())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_stderr_fileno():
|
||||||
|
try:
|
||||||
|
return sys.stderr.fileno()
|
||||||
|
except (AttributeError, io.UnsupportedOperation):
|
||||||
|
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||||
|
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||||
|
# This is potentially dangerous, but the best we can do.
|
||||||
|
return sys.__stderr__.fileno()
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@staticmethod
|
||||||
def pytest_runtest_protocol(item):
|
def get_timeout_config_value(config):
|
||||||
timeout = float(item.config.getini("faulthandler_timeout") or 0.0)
|
return float(config.getini("faulthandler_timeout") or 0.0)
|
||||||
if timeout > 0:
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_protocol(self, item):
|
||||||
|
timeout = self.get_timeout_config_value(item.config)
|
||||||
|
stderr = item.config.fault_handler_stderr
|
||||||
|
if timeout > 0 and stderr is not None:
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
|
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
faulthandler.cancel_dump_traceback_later()
|
||||||
|
else:
|
||||||
|
yield
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True)
|
||||||
|
def pytest_enter_pdb(self):
|
||||||
|
"""Cancel any traceback dumping due to timeout before entering pdb.
|
||||||
|
"""
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
stderr = item.config.fault_handler_stderr
|
faulthandler.cancel_dump_traceback_later()
|
||||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
faulthandler.cancel_dump_traceback_later()
|
|
||||||
else:
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True)
|
||||||
|
def pytest_exception_interact(self):
|
||||||
|
"""Cancel any traceback dumping due to an interactive exception being
|
||||||
|
raised.
|
||||||
|
"""
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True)
|
faulthandler.cancel_dump_traceback_later()
|
||||||
def pytest_enter_pdb():
|
|
||||||
"""Cancel any traceback dumping due to timeout before entering pdb.
|
|
||||||
"""
|
|
||||||
import faulthandler
|
|
||||||
|
|
||||||
faulthandler.cancel_dump_traceback_later()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True)
|
|
||||||
def pytest_exception_interact():
|
|
||||||
"""Cancel any traceback dumping due to an interactive exception being
|
|
||||||
raised.
|
|
||||||
"""
|
|
||||||
import faulthandler
|
|
||||||
|
|
||||||
faulthandler.cancel_dump_traceback_later()
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import itertools
|
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
@ -16,12 +15,12 @@ import py
|
||||||
import _pytest
|
import _pytest
|
||||||
from _pytest._code.code import FormattedExcinfo
|
from _pytest._code.code import FormattedExcinfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
|
from _pytest._code.source import getfslineno
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest.compat import _format_args
|
from _pytest.compat import _format_args
|
||||||
from _pytest.compat import _PytestWrapper
|
from _pytest.compat import _PytestWrapper
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import get_real_method
|
from _pytest.compat import get_real_method
|
||||||
from _pytest.compat import getfslineno
|
|
||||||
from _pytest.compat import getfuncargnames
|
from _pytest.compat import getfuncargnames
|
||||||
from _pytest.compat import getimfunc
|
from _pytest.compat import getimfunc
|
||||||
from _pytest.compat import getlocation
|
from _pytest.compat import getlocation
|
||||||
|
@ -31,6 +30,7 @@ from _pytest.compat import safe_getattr
|
||||||
from _pytest.compat import TYPE_CHECKING
|
from _pytest.compat import TYPE_CHECKING
|
||||||
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
|
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
|
||||||
from _pytest.deprecated import FUNCARGNAMES
|
from _pytest.deprecated import FUNCARGNAMES
|
||||||
|
from _pytest.mark import ParameterSet
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ if TYPE_CHECKING:
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
|
from _pytest.main import Session
|
||||||
|
|
||||||
|
|
||||||
@attr.s(frozen=True)
|
@attr.s(frozen=True)
|
||||||
|
@ -46,7 +47,7 @@ class PseudoFixtureDef:
|
||||||
scope = attr.ib()
|
scope = attr.ib()
|
||||||
|
|
||||||
|
|
||||||
def pytest_sessionstart(session):
|
def pytest_sessionstart(session: "Session"):
|
||||||
import _pytest.python
|
import _pytest.python
|
||||||
import _pytest.nodes
|
import _pytest.nodes
|
||||||
|
|
||||||
|
@ -513,13 +514,11 @@ class FixtureRequest:
|
||||||
values.append(fixturedef)
|
values.append(fixturedef)
|
||||||
current = current._parent_request
|
current = current._parent_request
|
||||||
|
|
||||||
def _compute_fixture_value(self, fixturedef):
|
def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None:
|
||||||
"""
|
"""
|
||||||
Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
|
Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
|
||||||
force the FixtureDef object to throw away any previous results and compute a new fixture value, which
|
force the FixtureDef object to throw away any previous results and compute a new fixture value, which
|
||||||
will be stored into the FixtureDef object itself.
|
will be stored into the FixtureDef object itself.
|
||||||
|
|
||||||
:param FixtureDef fixturedef:
|
|
||||||
"""
|
"""
|
||||||
# prepare a subrequest object before calling fixture function
|
# prepare a subrequest object before calling fixture function
|
||||||
# (latter managed by fixturedef)
|
# (latter managed by fixturedef)
|
||||||
|
@ -547,11 +546,11 @@ class FixtureRequest:
|
||||||
if has_params:
|
if has_params:
|
||||||
frame = inspect.stack()[3]
|
frame = inspect.stack()[3]
|
||||||
frameinfo = inspect.getframeinfo(frame[0])
|
frameinfo = inspect.getframeinfo(frame[0])
|
||||||
source_path = frameinfo.filename
|
source_path = py.path.local(frameinfo.filename)
|
||||||
source_lineno = frameinfo.lineno
|
source_lineno = frameinfo.lineno
|
||||||
source_path = py.path.local(source_path)
|
rel_source_path = source_path.relto(funcitem.config.rootdir)
|
||||||
if source_path.relto(funcitem.config.rootdir):
|
if rel_source_path:
|
||||||
source_path_str = source_path.relto(funcitem.config.rootdir)
|
source_path_str = rel_source_path
|
||||||
else:
|
else:
|
||||||
source_path_str = str(source_path)
|
source_path_str = str(source_path)
|
||||||
msg = (
|
msg = (
|
||||||
|
@ -856,6 +855,7 @@ class FixtureDef:
|
||||||
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
|
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
|
||||||
self.unittest = unittest
|
self.unittest = unittest
|
||||||
self.ids = ids
|
self.ids = ids
|
||||||
|
self.cached_result = None
|
||||||
self._finalizers = []
|
self._finalizers = []
|
||||||
|
|
||||||
def addfinalizer(self, finalizer):
|
def addfinalizer(self, finalizer):
|
||||||
|
@ -882,8 +882,7 @@ class FixtureDef:
|
||||||
# the cached fixture value and remove
|
# the cached fixture value and remove
|
||||||
# all finalizers because they may be bound methods which will
|
# all finalizers because they may be bound methods which will
|
||||||
# keep instances alive
|
# keep instances alive
|
||||||
if hasattr(self, "cached_result"):
|
self.cached_result = None
|
||||||
del self.cached_result
|
|
||||||
self._finalizers = []
|
self._finalizers = []
|
||||||
|
|
||||||
def execute(self, request):
|
def execute(self, request):
|
||||||
|
@ -895,10 +894,11 @@ class FixtureDef:
|
||||||
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
|
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
|
||||||
|
|
||||||
my_cache_key = self.cache_key(request)
|
my_cache_key = self.cache_key(request)
|
||||||
cached_result = getattr(self, "cached_result", None)
|
if self.cached_result is not None:
|
||||||
if cached_result is not None:
|
result, cache_key, err = self.cached_result
|
||||||
result, cache_key, err = cached_result
|
# note: comparison with `==` can fail (or be expensive) for e.g.
|
||||||
if my_cache_key == cache_key:
|
# numpy arrays (#6497)
|
||||||
|
if my_cache_key is cache_key:
|
||||||
if err is not None:
|
if err is not None:
|
||||||
_, val, tb = err
|
_, val, tb = err
|
||||||
raise val.with_traceback(tb)
|
raise val.with_traceback(tb)
|
||||||
|
@ -907,7 +907,7 @@ class FixtureDef:
|
||||||
# we have a previous but differently parametrized fixture instance
|
# we have a previous but differently parametrized fixture instance
|
||||||
# so we need to tear it down before creating a new one
|
# so we need to tear it down before creating a new one
|
||||||
self.finish(request)
|
self.finish(request)
|
||||||
assert not hasattr(self, "cached_result")
|
assert self.cached_result is None
|
||||||
|
|
||||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||||
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||||
|
@ -952,6 +952,7 @@ def pytest_fixture_setup(fixturedef, request):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for argname in fixturedef.argnames:
|
for argname in fixturedef.argnames:
|
||||||
fixdef = request._get_active_fixturedef(argname)
|
fixdef = request._get_active_fixturedef(argname)
|
||||||
|
assert fixdef.cached_result is not None
|
||||||
result, arg_cache_key, exc = fixdef.cached_result
|
result, arg_cache_key, exc = fixdef.cached_result
|
||||||
request._check_scope(argname, request.scope, fixdef.scope)
|
request._check_scope(argname, request.scope, fixdef.scope)
|
||||||
kwargs[argname] = result
|
kwargs[argname] = result
|
||||||
|
@ -1248,7 +1249,6 @@ class FixtureManager:
|
||||||
self.config = session.config
|
self.config = session.config
|
||||||
self._arg2fixturedefs = {}
|
self._arg2fixturedefs = {}
|
||||||
self._holderobjseen = set()
|
self._holderobjseen = set()
|
||||||
self._arg2finish = {}
|
|
||||||
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
|
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
|
||||||
session.config.pluginmanager.register(self, "funcmanage")
|
session.config.pluginmanager.register(self, "funcmanage")
|
||||||
|
|
||||||
|
@ -1261,8 +1261,6 @@ class FixtureManager:
|
||||||
This things are done later as well when dealing with parametrization
|
This things are done later as well when dealing with parametrization
|
||||||
so this could be improved
|
so this could be improved
|
||||||
"""
|
"""
|
||||||
from _pytest.mark import ParameterSet
|
|
||||||
|
|
||||||
parametrize_argnames = []
|
parametrize_argnames = []
|
||||||
for marker in node.iter_markers(name="parametrize"):
|
for marker in node.iter_markers(name="parametrize"):
|
||||||
if not marker.kwargs.get("indirect", False):
|
if not marker.kwargs.get("indirect", False):
|
||||||
|
@ -1279,10 +1277,8 @@ class FixtureManager:
|
||||||
else:
|
else:
|
||||||
argnames = ()
|
argnames = ()
|
||||||
|
|
||||||
usefixtures = itertools.chain.from_iterable(
|
usefixtures = get_use_fixtures_for_node(node)
|
||||||
mark.args for mark in node.iter_markers(name="usefixtures")
|
initialnames = usefixtures + argnames
|
||||||
)
|
|
||||||
initialnames = tuple(usefixtures) + argnames
|
|
||||||
fm = node.session._fixturemanager
|
fm = node.session._fixturemanager
|
||||||
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
||||||
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
|
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
|
||||||
|
@ -1479,3 +1475,12 @@ class FixtureManager:
|
||||||
for fixturedef in fixturedefs:
|
for fixturedef in fixturedefs:
|
||||||
if nodes.ischildnode(fixturedef.baseid, nodeid):
|
if nodes.ischildnode(fixturedef.baseid, nodeid):
|
||||||
yield fixturedef
|
yield fixturedef
|
||||||
|
|
||||||
|
|
||||||
|
def get_use_fixtures_for_node(node) -> Tuple[str, ...]:
|
||||||
|
"""Returns the names of all the usefixtures() marks on the given node"""
|
||||||
|
return tuple(
|
||||||
|
str(name)
|
||||||
|
for mark in node.iter_markers(name="usefixtures")
|
||||||
|
for name in mark.args
|
||||||
|
)
|
||||||
|
|
|
@ -40,8 +40,9 @@ def pytest_addoption(parser):
|
||||||
group = parser.getgroup("debugconfig")
|
group = parser.getgroup("debugconfig")
|
||||||
group.addoption(
|
group.addoption(
|
||||||
"--version",
|
"--version",
|
||||||
|
"-V",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="display pytest lib version and import information.",
|
help="display pytest version and information about plugins.",
|
||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"-h",
|
"-h",
|
||||||
|
@ -66,7 +67,7 @@ def pytest_addoption(parser):
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="trace considerations of conftest.py files.",
|
help="trace considerations of conftest.py files.",
|
||||||
),
|
)
|
||||||
group.addoption(
|
group.addoption(
|
||||||
"--debug",
|
"--debug",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pluggy import HookspecMarker
|
from pluggy import HookspecMarker
|
||||||
|
|
||||||
|
from _pytest.compat import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from _pytest.main import Session
|
||||||
|
|
||||||
|
|
||||||
hookspec = HookspecMarker("pytest")
|
hookspec = HookspecMarker("pytest")
|
||||||
|
|
||||||
|
@ -158,7 +166,7 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_collection(session):
|
def pytest_collection(session: "Session") -> Optional[Any]:
|
||||||
"""Perform the collection protocol for the given session.
|
"""Perform the collection protocol for the given session.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
@ -307,10 +315,6 @@ def pytest_runtestloop(session):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_itemstart(item, node):
|
|
||||||
"""(**Deprecated**) use pytest_runtest_logstart. """
|
|
||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_runtest_protocol(item, nextitem):
|
def pytest_runtest_protocol(item, nextitem):
|
||||||
""" implements the runtest_setup/call/teardown protocol for
|
""" implements the runtest_setup/call/teardown protocol for
|
||||||
|
@ -419,9 +423,9 @@ def pytest_fixture_setup(fixturedef, request):
|
||||||
|
|
||||||
|
|
||||||
def pytest_fixture_post_finalizer(fixturedef, request):
|
def pytest_fixture_post_finalizer(fixturedef, request):
|
||||||
""" called after fixture teardown, but before the cache is cleared so
|
"""Called after fixture teardown, but before the cache is cleared, so
|
||||||
the fixture result cache ``fixturedef.cached_result`` can
|
the fixture result ``fixturedef.cached_result`` is still available (not
|
||||||
still be accessed."""
|
``None``)."""
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
@ -562,7 +566,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
||||||
|
|
||||||
|
|
||||||
@hookspec(historic=True)
|
@hookspec(historic=True)
|
||||||
def pytest_warning_captured(warning_message, when, item):
|
def pytest_warning_captured(warning_message, when, item, location):
|
||||||
"""
|
"""
|
||||||
Process a warning captured by the internal pytest warnings plugin.
|
Process a warning captured by the internal pytest warnings plugin.
|
||||||
|
|
||||||
|
@ -582,6 +586,10 @@ def pytest_warning_captured(warning_message, when, item):
|
||||||
in a future release.
|
in a future release.
|
||||||
|
|
||||||
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
|
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
|
||||||
|
|
||||||
|
:param tuple location:
|
||||||
|
Holds information about the execution context of the captured warning (filename, linenumber, function).
|
||||||
|
``function`` evaluates to <module> when the execution context is at the module level.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -167,51 +167,28 @@ class _NodeReporter:
|
||||||
content_out = report.capstdout
|
content_out = report.capstdout
|
||||||
content_log = report.caplog
|
content_log = report.caplog
|
||||||
content_err = report.capstderr
|
content_err = report.capstderr
|
||||||
|
if self.xml.logging == "no":
|
||||||
|
return
|
||||||
|
content_all = ""
|
||||||
|
if self.xml.logging in ["log", "all"]:
|
||||||
|
content_all = self._prepare_content(content_log, " Captured Log ")
|
||||||
|
if self.xml.logging in ["system-out", "out-err", "all"]:
|
||||||
|
content_all += self._prepare_content(content_out, " Captured Out ")
|
||||||
|
self._write_content(report, content_all, "system-out")
|
||||||
|
content_all = ""
|
||||||
|
if self.xml.logging in ["system-err", "out-err", "all"]:
|
||||||
|
content_all += self._prepare_content(content_err, " Captured Err ")
|
||||||
|
self._write_content(report, content_all, "system-err")
|
||||||
|
content_all = ""
|
||||||
|
if content_all:
|
||||||
|
self._write_content(report, content_all, "system-out")
|
||||||
|
|
||||||
if content_log or content_out:
|
def _prepare_content(self, content, header):
|
||||||
if content_log and self.xml.logging == "system-out":
|
return "\n".join([header.center(80, "-"), content, ""])
|
||||||
if content_out:
|
|
||||||
# syncing stdout and the log-output is not done yet. It's
|
|
||||||
# probably not worth the effort. Therefore, first the captured
|
|
||||||
# stdout is shown and then the captured logs.
|
|
||||||
content = "\n".join(
|
|
||||||
[
|
|
||||||
" Captured Stdout ".center(80, "-"),
|
|
||||||
content_out,
|
|
||||||
"",
|
|
||||||
" Captured Log ".center(80, "-"),
|
|
||||||
content_log,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
content = content_log
|
|
||||||
else:
|
|
||||||
content = content_out
|
|
||||||
|
|
||||||
if content:
|
def _write_content(self, report, content, jheader):
|
||||||
tag = getattr(Junit, "system-out")
|
tag = getattr(Junit, jheader)
|
||||||
self.append(tag(bin_xml_escape(content)))
|
self.append(tag(bin_xml_escape(content)))
|
||||||
|
|
||||||
if content_log or content_err:
|
|
||||||
if content_log and self.xml.logging == "system-err":
|
|
||||||
if content_err:
|
|
||||||
content = "\n".join(
|
|
||||||
[
|
|
||||||
" Captured Stderr ".center(80, "-"),
|
|
||||||
content_err,
|
|
||||||
"",
|
|
||||||
" Captured Log ".center(80, "-"),
|
|
||||||
content_log,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
content = content_log
|
|
||||||
else:
|
|
||||||
content = content_err
|
|
||||||
|
|
||||||
if content:
|
|
||||||
tag = getattr(Junit, "system-err")
|
|
||||||
self.append(tag(bin_xml_escape(content)))
|
|
||||||
|
|
||||||
def append_pass(self, report):
|
def append_pass(self, report):
|
||||||
self.add_stats("passed")
|
self.add_stats("passed")
|
||||||
|
@ -408,9 +385,9 @@ def pytest_addoption(parser):
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"junit_logging",
|
"junit_logging",
|
||||||
"Write captured log messages to JUnit report: "
|
"Write captured log messages to JUnit report: "
|
||||||
"one of no|system-out|system-err",
|
"one of no|log|system-out|system-err|out-err|all",
|
||||||
default="no",
|
default="no",
|
||||||
) # choices=['no', 'stdout', 'stderr'])
|
)
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"junit_log_passing_tests",
|
"junit_log_passing_tests",
|
||||||
"Capture log information for passing tests to JUnit report: ",
|
"Capture log information for passing tests to JUnit report: ",
|
||||||
|
|
|
@ -5,10 +5,12 @@ from contextlib import contextmanager
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest import nodes
|
||||||
from _pytest.compat import nullcontext
|
from _pytest.compat import nullcontext
|
||||||
from _pytest.config import _strtobool
|
from _pytest.config import _strtobool
|
||||||
from _pytest.config import create_terminal_writer
|
from _pytest.config import create_terminal_writer
|
||||||
|
@ -325,13 +327,13 @@ class LogCaptureFixture:
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def handler(self):
|
def handler(self) -> LogCaptureHandler:
|
||||||
"""
|
"""
|
||||||
:rtype: LogCaptureHandler
|
:rtype: LogCaptureHandler
|
||||||
"""
|
"""
|
||||||
return self._item.catch_log_handler
|
return self._item.catch_log_handler # type: ignore[no-any-return] # noqa: F723
|
||||||
|
|
||||||
def get_records(self, when):
|
def get_records(self, when: str) -> List[logging.LogRecord]:
|
||||||
"""
|
"""
|
||||||
Get the logging records for one of the possible test phases.
|
Get the logging records for one of the possible test phases.
|
||||||
|
|
||||||
|
@ -345,7 +347,7 @@ class LogCaptureFixture:
|
||||||
"""
|
"""
|
||||||
handler = self._item.catch_log_handlers.get(when)
|
handler = self._item.catch_log_handlers.get(when)
|
||||||
if handler:
|
if handler:
|
||||||
return handler.records
|
return handler.records # type: ignore[no-any-return] # noqa: F723
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -485,6 +487,12 @@ class LoggingPlugin:
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
self.print_logs = get_option_ini(config, "log_print")
|
self.print_logs = get_option_ini(config, "log_print")
|
||||||
|
if not self.print_logs:
|
||||||
|
from _pytest.warnings import _issue_warning_captured
|
||||||
|
from _pytest.deprecated import NO_PRINT_LOGS
|
||||||
|
|
||||||
|
_issue_warning_captured(NO_PRINT_LOGS, self._config.hook, stacklevel=2)
|
||||||
|
|
||||||
self.formatter = self._create_formatter(
|
self.formatter = self._create_formatter(
|
||||||
get_option_ini(config, "log_format"),
|
get_option_ini(config, "log_format"),
|
||||||
get_option_ini(config, "log_date_format"),
|
get_option_ini(config, "log_date_format"),
|
||||||
|
@ -591,7 +599,7 @@ class LoggingPlugin:
|
||||||
) is not None or self._config.getini("log_cli")
|
) is not None or self._config.getini("log_cli")
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||||
def pytest_collection(self):
|
def pytest_collection(self) -> Generator[None, None, None]:
|
||||||
with self.live_logs_context():
|
with self.live_logs_context():
|
||||||
if self.log_cli_handler:
|
if self.log_cli_handler:
|
||||||
self.log_cli_handler.set_when("collection")
|
self.log_cli_handler.set_when("collection")
|
||||||
|
@ -612,7 +620,9 @@ class LoggingPlugin:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _runtest_for_main(self, item, when):
|
def _runtest_for_main(
|
||||||
|
self, item: nodes.Item, when: str
|
||||||
|
) -> Generator[None, None, None]:
|
||||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||||
with catching_logs(
|
with catching_logs(
|
||||||
LogCaptureHandler(), formatter=self.formatter, level=self.log_level
|
LogCaptureHandler(), formatter=self.formatter, level=self.log_level
|
||||||
|
@ -625,15 +635,15 @@ class LoggingPlugin:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not hasattr(item, "catch_log_handlers"):
|
if not hasattr(item, "catch_log_handlers"):
|
||||||
item.catch_log_handlers = {}
|
item.catch_log_handlers = {} # type: ignore[attr-defined] # noqa: F821
|
||||||
item.catch_log_handlers[when] = log_handler
|
item.catch_log_handlers[when] = log_handler # type: ignore[attr-defined] # noqa: F821
|
||||||
item.catch_log_handler = log_handler
|
item.catch_log_handler = log_handler # type: ignore[attr-defined] # noqa: F821
|
||||||
try:
|
try:
|
||||||
yield # run test
|
yield # run test
|
||||||
finally:
|
finally:
|
||||||
if when == "teardown":
|
if when == "teardown":
|
||||||
del item.catch_log_handler
|
del item.catch_log_handler # type: ignore[attr-defined] # noqa: F821
|
||||||
del item.catch_log_handlers
|
del item.catch_log_handlers # type: ignore[attr-defined] # noqa: F821
|
||||||
|
|
||||||
if self.print_logs:
|
if self.print_logs:
|
||||||
# Add a captured log section to the report.
|
# Add a captured log section to the report.
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
""" core implementation of testing process: init, session, runtest loop. """
|
""" core implementation of testing process: init, session, runtest loop. """
|
||||||
import enum
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import FrozenSet
|
from typing import FrozenSet
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import py
|
import py
|
||||||
|
@ -17,42 +21,22 @@ from _pytest import nodes
|
||||||
from _pytest.compat import TYPE_CHECKING
|
from _pytest.compat import TYPE_CHECKING
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import directory_arg
|
from _pytest.config import directory_arg
|
||||||
|
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.fixtures import FixtureManager
|
from _pytest.fixtures import FixtureManager
|
||||||
from _pytest.nodes import Node
|
from _pytest.outcomes import Exit
|
||||||
from _pytest.outcomes import exit
|
from _pytest.reports import CollectReport
|
||||||
from _pytest.runner import collect_one_node
|
from _pytest.runner import collect_one_node
|
||||||
from _pytest.runner import SetupState
|
from _pytest.runner import SetupState
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from _pytest.python import Package
|
from _pytest.python import Package
|
||||||
|
|
||||||
|
|
||||||
class ExitCode(enum.IntEnum):
|
|
||||||
"""
|
|
||||||
.. versionadded:: 5.0
|
|
||||||
|
|
||||||
Encodes the valid exit codes by pytest.
|
|
||||||
|
|
||||||
Currently users and plugins may supply other exit codes as well.
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: tests passed
|
|
||||||
OK = 0
|
|
||||||
#: tests failed
|
|
||||||
TESTS_FAILED = 1
|
|
||||||
#: pytest was interrupted
|
|
||||||
INTERRUPTED = 2
|
|
||||||
#: an internal error got in the way
|
|
||||||
INTERNAL_ERROR = 3
|
|
||||||
#: pytest was misused
|
|
||||||
USAGE_ERROR = 4
|
|
||||||
#: pytest couldn't find tests
|
|
||||||
NO_TESTS_COLLECTED = 5
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"norecursedirs",
|
"norecursedirs",
|
||||||
|
@ -192,9 +176,11 @@ def pytest_addoption(parser):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def wrap_session(config, doit):
|
def wrap_session(
|
||||||
|
config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
|
||||||
|
) -> Union[int, ExitCode]:
|
||||||
"""Skeleton command line program"""
|
"""Skeleton command line program"""
|
||||||
session = Session(config)
|
session = Session.from_config(config)
|
||||||
session.exitstatus = ExitCode.OK
|
session.exitstatus = ExitCode.OK
|
||||||
initstate = 0
|
initstate = 0
|
||||||
try:
|
try:
|
||||||
|
@ -209,10 +195,10 @@ def wrap_session(config, doit):
|
||||||
raise
|
raise
|
||||||
except Failed:
|
except Failed:
|
||||||
session.exitstatus = ExitCode.TESTS_FAILED
|
session.exitstatus = ExitCode.TESTS_FAILED
|
||||||
except (KeyboardInterrupt, exit.Exception):
|
except (KeyboardInterrupt, Exit):
|
||||||
excinfo = _pytest._code.ExceptionInfo.from_current()
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
||||||
exitstatus = ExitCode.INTERRUPTED
|
exitstatus = ExitCode.INTERRUPTED # type: Union[int, ExitCode]
|
||||||
if isinstance(excinfo.value, exit.Exception):
|
if isinstance(excinfo.value, Exit):
|
||||||
if excinfo.value.returncode is not None:
|
if excinfo.value.returncode is not None:
|
||||||
exitstatus = excinfo.value.returncode
|
exitstatus = excinfo.value.returncode
|
||||||
if initstate < 2:
|
if initstate < 2:
|
||||||
|
@ -226,7 +212,7 @@ def wrap_session(config, doit):
|
||||||
excinfo = _pytest._code.ExceptionInfo.from_current()
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
||||||
try:
|
try:
|
||||||
config.notify_exception(excinfo, config.option)
|
config.notify_exception(excinfo, config.option)
|
||||||
except exit.Exception as exc:
|
except Exit as exc:
|
||||||
if exc.returncode is not None:
|
if exc.returncode is not None:
|
||||||
session.exitstatus = exc.returncode
|
session.exitstatus = exc.returncode
|
||||||
sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc))
|
sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc))
|
||||||
|
@ -235,12 +221,18 @@ def wrap_session(config, doit):
|
||||||
sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
|
sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
excinfo = None # Explicitly break reference cycle.
|
# Explicitly break reference cycle.
|
||||||
|
excinfo = None # type: ignore
|
||||||
session.startdir.chdir()
|
session.startdir.chdir()
|
||||||
if initstate >= 2:
|
if initstate >= 2:
|
||||||
config.hook.pytest_sessionfinish(
|
try:
|
||||||
session=session, exitstatus=session.exitstatus
|
config.hook.pytest_sessionfinish(
|
||||||
)
|
session=session, exitstatus=session.exitstatus
|
||||||
|
)
|
||||||
|
except Exit as exc:
|
||||||
|
if exc.returncode is not None:
|
||||||
|
session.exitstatus = exc.returncode
|
||||||
|
sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc))
|
||||||
config._ensure_unconfigure()
|
config._ensure_unconfigure()
|
||||||
return session.exitstatus
|
return session.exitstatus
|
||||||
|
|
||||||
|
@ -249,7 +241,7 @@ def pytest_cmdline_main(config):
|
||||||
return wrap_session(config, _main)
|
return wrap_session(config, _main)
|
||||||
|
|
||||||
|
|
||||||
def _main(config, session):
|
def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
|
||||||
""" default command line protocol for initialization, session,
|
""" default command line protocol for initialization, session,
|
||||||
running tests and reporting. """
|
running tests and reporting. """
|
||||||
config.hook.pytest_collection(session=session)
|
config.hook.pytest_collection(session=session)
|
||||||
|
@ -259,6 +251,7 @@ def _main(config, session):
|
||||||
return ExitCode.TESTS_FAILED
|
return ExitCode.TESTS_FAILED
|
||||||
elif session.testscollected == 0:
|
elif session.testscollected == 0:
|
||||||
return ExitCode.NO_TESTS_COLLECTED
|
return ExitCode.NO_TESTS_COLLECTED
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def pytest_collection(session):
|
def pytest_collection(session):
|
||||||
|
@ -348,18 +341,6 @@ def pytest_collection_modifyitems(items, config):
|
||||||
items[:] = remaining
|
items[:] = remaining
|
||||||
|
|
||||||
|
|
||||||
class FSHookProxy:
|
|
||||||
def __init__(self, fspath, pm, remove_mods):
|
|
||||||
self.fspath = fspath
|
|
||||||
self.pm = pm
|
|
||||||
self.remove_mods = remove_mods
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
|
|
||||||
self.__dict__[name] = x
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class NoMatch(Exception):
|
class NoMatch(Exception):
|
||||||
""" raised if matching cannot locate a matching names. """
|
""" raised if matching cannot locate a matching names. """
|
||||||
|
|
||||||
|
@ -391,6 +372,7 @@ class Session(nodes.FSCollector):
|
||||||
_setupstate = None # type: SetupState
|
_setupstate = None # type: SetupState
|
||||||
# Set on the session by fixtures.pytest_sessionstart.
|
# Set on the session by fixtures.pytest_sessionstart.
|
||||||
_fixturemanager = None # type: FixtureManager
|
_fixturemanager = None # type: FixtureManager
|
||||||
|
exitstatus = None # type: Union[int, ExitCode]
|
||||||
|
|
||||||
def __init__(self, config: Config) -> None:
|
def __init__(self, config: Config) -> None:
|
||||||
nodes.FSCollector.__init__(
|
nodes.FSCollector.__init__(
|
||||||
|
@ -401,14 +383,22 @@ class Session(nodes.FSCollector):
|
||||||
self.shouldstop = False
|
self.shouldstop = False
|
||||||
self.shouldfail = False
|
self.shouldfail = False
|
||||||
self.trace = config.trace.root.get("collection")
|
self.trace = config.trace.root.get("collection")
|
||||||
self._norecursepatterns = config.getini("norecursedirs")
|
|
||||||
self.startdir = config.invocation_dir
|
self.startdir = config.invocation_dir
|
||||||
self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
|
self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
|
||||||
|
|
||||||
# Keep track of any collected nodes in here, so we don't duplicate fixtures
|
# Keep track of any collected nodes in here, so we don't duplicate fixtures
|
||||||
self._node_cache = {} # type: Dict[str, List[Node]]
|
self._collection_node_cache1 = (
|
||||||
|
{}
|
||||||
|
) # type: Dict[py.path.local, Sequence[nodes.Collector]]
|
||||||
|
self._collection_node_cache2 = (
|
||||||
|
{}
|
||||||
|
) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector]
|
||||||
|
self._collection_node_cache3 = (
|
||||||
|
{}
|
||||||
|
) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport]
|
||||||
|
|
||||||
# Dirnames of pkgs with dunder-init files.
|
# Dirnames of pkgs with dunder-init files.
|
||||||
self._pkg_roots = {} # type: Dict[py.path.local, Package]
|
self._collection_pkg_roots = {} # type: Dict[py.path.local, Package]
|
||||||
|
|
||||||
self._bestrelpathcache = _bestrelpath_cache(
|
self._bestrelpathcache = _bestrelpath_cache(
|
||||||
config.rootdir
|
config.rootdir
|
||||||
|
@ -416,6 +406,10 @@ class Session(nodes.FSCollector):
|
||||||
|
|
||||||
self.config.pluginmanager.register(self, name="session")
|
self.config.pluginmanager.register(self, name="session")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_config(cls, config):
|
||||||
|
return cls._create(config)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % (
|
return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % (
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
|
@ -449,19 +443,8 @@ class Session(nodes.FSCollector):
|
||||||
def isinitpath(self, path):
|
def isinitpath(self, path):
|
||||||
return path in self._initialpaths
|
return path in self._initialpaths
|
||||||
|
|
||||||
def gethookproxy(self, fspath):
|
def gethookproxy(self, fspath: py.path.local):
|
||||||
# check if we have the common case of running
|
return super()._gethookproxy(fspath)
|
||||||
# hooks with all conftest.py files
|
|
||||||
pm = self.config.pluginmanager
|
|
||||||
my_conftestmodules = pm._getconftestmodules(fspath)
|
|
||||||
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
|
||||||
if remove_mods:
|
|
||||||
# one or more conftests are not in use at this fspath
|
|
||||||
proxy = FSHookProxy(fspath, pm, remove_mods)
|
|
||||||
else:
|
|
||||||
# all plugins are active for this fspath
|
|
||||||
proxy = self.config.hook
|
|
||||||
return proxy
|
|
||||||
|
|
||||||
def perform_collect(self, args=None, genitems=True):
|
def perform_collect(self, args=None, genitems=True):
|
||||||
hook = self.config.hook
|
hook = self.config.hook
|
||||||
|
@ -482,13 +465,13 @@ class Session(nodes.FSCollector):
|
||||||
self.trace("perform_collect", self, args)
|
self.trace("perform_collect", self, args)
|
||||||
self.trace.root.indent += 1
|
self.trace.root.indent += 1
|
||||||
self._notfound = []
|
self._notfound = []
|
||||||
initialpaths = []
|
initialpaths = [] # type: List[py.path.local]
|
||||||
self._initialparts = []
|
self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]]
|
||||||
self.items = items = []
|
self.items = items = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
parts = self._parsearg(arg)
|
fspath, parts = self._parsearg(arg)
|
||||||
self._initialparts.append(parts)
|
self._initial_parts.append((fspath, parts))
|
||||||
initialpaths.append(parts[0])
|
initialpaths.append(fspath)
|
||||||
self._initialpaths = frozenset(initialpaths)
|
self._initialpaths = frozenset(initialpaths)
|
||||||
rep = collect_one_node(self)
|
rep = collect_one_node(self)
|
||||||
self.ihook.pytest_collectreport(report=rep)
|
self.ihook.pytest_collectreport(report=rep)
|
||||||
|
@ -508,25 +491,26 @@ class Session(nodes.FSCollector):
|
||||||
return items
|
return items
|
||||||
|
|
||||||
def collect(self):
|
def collect(self):
|
||||||
for initialpart in self._initialparts:
|
for fspath, parts in self._initial_parts:
|
||||||
self.trace("processing argument", initialpart)
|
self.trace("processing argument", (fspath, parts))
|
||||||
self.trace.root.indent += 1
|
self.trace.root.indent += 1
|
||||||
try:
|
try:
|
||||||
yield from self._collect(initialpart)
|
yield from self._collect(fspath, parts)
|
||||||
except NoMatch:
|
except NoMatch:
|
||||||
report_arg = "::".join(map(str, initialpart))
|
report_arg = "::".join((str(fspath), *parts))
|
||||||
# we are inside a make_report hook so
|
# we are inside a make_report hook so
|
||||||
# we cannot directly pass through the exception
|
# we cannot directly pass through the exception
|
||||||
self._notfound.append((report_arg, sys.exc_info()[1]))
|
self._notfound.append((report_arg, sys.exc_info()[1]))
|
||||||
|
|
||||||
self.trace.root.indent -= 1
|
self.trace.root.indent -= 1
|
||||||
|
self._collection_node_cache1.clear()
|
||||||
|
self._collection_node_cache2.clear()
|
||||||
|
self._collection_node_cache3.clear()
|
||||||
|
self._collection_pkg_roots.clear()
|
||||||
|
|
||||||
def _collect(self, arg):
|
def _collect(self, argpath, names):
|
||||||
from _pytest.python import Package
|
from _pytest.python import Package
|
||||||
|
|
||||||
names = arg[:]
|
|
||||||
argpath = names.pop(0)
|
|
||||||
|
|
||||||
# Start with a Session root, and delve to argpath item (dir or file)
|
# Start with a Session root, and delve to argpath item (dir or file)
|
||||||
# and stack all Packages found on the way.
|
# and stack all Packages found on the way.
|
||||||
# No point in finding packages when collecting doctests
|
# No point in finding packages when collecting doctests
|
||||||
|
@ -539,18 +523,18 @@ class Session(nodes.FSCollector):
|
||||||
if parent.isdir():
|
if parent.isdir():
|
||||||
pkginit = parent.join("__init__.py")
|
pkginit = parent.join("__init__.py")
|
||||||
if pkginit.isfile():
|
if pkginit.isfile():
|
||||||
if pkginit not in self._node_cache:
|
if pkginit not in self._collection_node_cache1:
|
||||||
col = self._collectfile(pkginit, handle_dupes=False)
|
col = self._collectfile(pkginit, handle_dupes=False)
|
||||||
if col:
|
if col:
|
||||||
if isinstance(col[0], Package):
|
if isinstance(col[0], Package):
|
||||||
self._pkg_roots[parent] = col[0]
|
self._collection_pkg_roots[parent] = col[0]
|
||||||
# always store a list in the cache, matchnodes expects it
|
# always store a list in the cache, matchnodes expects it
|
||||||
self._node_cache[col[0].fspath] = [col[0]]
|
self._collection_node_cache1[col[0].fspath] = [col[0]]
|
||||||
|
|
||||||
# If it's a directory argument, recurse and look for any Subpackages.
|
# If it's a directory argument, recurse and look for any Subpackages.
|
||||||
# Let the Package collector deal with subnodes, don't collect here.
|
# Let the Package collector deal with subnodes, don't collect here.
|
||||||
if argpath.check(dir=1):
|
if argpath.check(dir=1):
|
||||||
assert not names, "invalid arg {!r}".format(arg)
|
assert not names, "invalid arg {!r}".format((argpath, names))
|
||||||
|
|
||||||
seen_dirs = set()
|
seen_dirs = set()
|
||||||
for path in argpath.visit(
|
for path in argpath.visit(
|
||||||
|
@ -565,28 +549,28 @@ class Session(nodes.FSCollector):
|
||||||
for x in self._collectfile(pkginit):
|
for x in self._collectfile(pkginit):
|
||||||
yield x
|
yield x
|
||||||
if isinstance(x, Package):
|
if isinstance(x, Package):
|
||||||
self._pkg_roots[dirpath] = x
|
self._collection_pkg_roots[dirpath] = x
|
||||||
if dirpath in self._pkg_roots:
|
if dirpath in self._collection_pkg_roots:
|
||||||
# Do not collect packages here.
|
# Do not collect packages here.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for x in self._collectfile(path):
|
for x in self._collectfile(path):
|
||||||
key = (type(x), x.fspath)
|
key = (type(x), x.fspath)
|
||||||
if key in self._node_cache:
|
if key in self._collection_node_cache2:
|
||||||
yield self._node_cache[key]
|
yield self._collection_node_cache2[key]
|
||||||
else:
|
else:
|
||||||
self._node_cache[key] = x
|
self._collection_node_cache2[key] = x
|
||||||
yield x
|
yield x
|
||||||
else:
|
else:
|
||||||
assert argpath.check(file=1)
|
assert argpath.check(file=1)
|
||||||
|
|
||||||
if argpath in self._node_cache:
|
if argpath in self._collection_node_cache1:
|
||||||
col = self._node_cache[argpath]
|
col = self._collection_node_cache1[argpath]
|
||||||
else:
|
else:
|
||||||
collect_root = self._pkg_roots.get(argpath.dirname, self)
|
collect_root = self._collection_pkg_roots.get(argpath.dirname, self)
|
||||||
col = collect_root._collectfile(argpath, handle_dupes=False)
|
col = collect_root._collectfile(argpath, handle_dupes=False)
|
||||||
if col:
|
if col:
|
||||||
self._node_cache[argpath] = col
|
self._collection_node_cache1[argpath] = col
|
||||||
m = self.matchnodes(col, names)
|
m = self.matchnodes(col, names)
|
||||||
# If __init__.py was the only file requested, then the matched node will be
|
# If __init__.py was the only file requested, then the matched node will be
|
||||||
# the corresponding Package, and the first yielded item will be the __init__
|
# the corresponding Package, and the first yielded item will be the __init__
|
||||||
|
@ -625,19 +609,6 @@ class Session(nodes.FSCollector):
|
||||||
|
|
||||||
return ihook.pytest_collect_file(path=path, parent=self)
|
return ihook.pytest_collect_file(path=path, parent=self)
|
||||||
|
|
||||||
def _recurse(self, dirpath):
|
|
||||||
if dirpath.basename == "__pycache__":
|
|
||||||
return False
|
|
||||||
ihook = self.gethookproxy(dirpath.dirpath())
|
|
||||||
if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
|
|
||||||
return False
|
|
||||||
for pat in self._norecursepatterns:
|
|
||||||
if dirpath.check(fnmatch=pat):
|
|
||||||
return False
|
|
||||||
ihook = self.gethookproxy(dirpath)
|
|
||||||
ihook.pytest_collect_directory(path=dirpath, parent=self)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _visit_filter(f):
|
def _visit_filter(f):
|
||||||
return f.check(file=1)
|
return f.check(file=1)
|
||||||
|
@ -660,19 +631,19 @@ class Session(nodes.FSCollector):
|
||||||
|
|
||||||
def _parsearg(self, arg):
|
def _parsearg(self, arg):
|
||||||
""" return (fspath, names) tuple after checking the file exists. """
|
""" return (fspath, names) tuple after checking the file exists. """
|
||||||
parts = str(arg).split("::")
|
strpath, *parts = str(arg).split("::")
|
||||||
if self.config.option.pyargs:
|
if self.config.option.pyargs:
|
||||||
parts[0] = self._tryconvertpyarg(parts[0])
|
strpath = self._tryconvertpyarg(strpath)
|
||||||
relpath = parts[0].replace("/", os.sep)
|
relpath = strpath.replace("/", os.sep)
|
||||||
path = self.config.invocation_dir.join(relpath, abs=True)
|
fspath = self.config.invocation_dir.join(relpath, abs=True)
|
||||||
if not path.check():
|
if not fspath.check():
|
||||||
if self.config.option.pyargs:
|
if self.config.option.pyargs:
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
"file or package not found: " + arg + " (missing __init__.py?)"
|
"file or package not found: " + arg + " (missing __init__.py?)"
|
||||||
)
|
)
|
||||||
raise UsageError("file not found: " + arg)
|
raise UsageError("file not found: " + arg)
|
||||||
parts[0] = path.realpath()
|
fspath = fspath.realpath()
|
||||||
return parts
|
return (fspath, parts)
|
||||||
|
|
||||||
def matchnodes(self, matching, names):
|
def matchnodes(self, matching, names):
|
||||||
self.trace("matchnodes", matching, names)
|
self.trace("matchnodes", matching, names)
|
||||||
|
@ -699,11 +670,11 @@ class Session(nodes.FSCollector):
|
||||||
continue
|
continue
|
||||||
assert isinstance(node, nodes.Collector)
|
assert isinstance(node, nodes.Collector)
|
||||||
key = (type(node), node.nodeid)
|
key = (type(node), node.nodeid)
|
||||||
if key in self._node_cache:
|
if key in self._collection_node_cache3:
|
||||||
rep = self._node_cache[key]
|
rep = self._collection_node_cache3[key]
|
||||||
else:
|
else:
|
||||||
rep = collect_one_node(node)
|
rep = collect_one_node(node)
|
||||||
self._node_cache[key] = rep
|
self._collection_node_cache3[key] = rep
|
||||||
if rep.passed:
|
if rep.passed:
|
||||||
has_matched = False
|
has_matched = False
|
||||||
for x in rep.result:
|
for x in rep.result:
|
||||||
|
|
|
@ -52,7 +52,8 @@ def pytest_addoption(parser):
|
||||||
"-k 'not test_method and not test_other' will eliminate the matches. "
|
"-k 'not test_method and not test_other' will eliminate the matches. "
|
||||||
"Additionally keywords are matched to classes and functions "
|
"Additionally keywords are matched to classes and functions "
|
||||||
"containing extra names in their 'extra_keyword_matches' set, "
|
"containing extra names in their 'extra_keyword_matches' set, "
|
||||||
"as well as functions which have names assigned directly to them.",
|
"as well as functions which have names assigned directly to them. "
|
||||||
|
"The matching is case-insensitive.",
|
||||||
)
|
)
|
||||||
|
|
||||||
group._addoption(
|
group._addoption(
|
||||||
|
|
|
@ -57,7 +57,15 @@ class KeywordMapping:
|
||||||
return cls(mapped_names)
|
return cls(mapped_names)
|
||||||
|
|
||||||
def __getitem__(self, subname):
|
def __getitem__(self, subname):
|
||||||
for name in self._names:
|
"""Return whether subname is included within stored names.
|
||||||
|
|
||||||
|
The string inclusion check is case-insensitive.
|
||||||
|
|
||||||
|
"""
|
||||||
|
subname = subname.lower()
|
||||||
|
names = (name.lower() for name in self._names)
|
||||||
|
|
||||||
|
for name in names:
|
||||||
if subname in name:
|
if subname in name:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -2,13 +2,16 @@ import inspect
|
||||||
import warnings
|
import warnings
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from collections.abc import MutableMapping
|
from collections.abc import MutableMapping
|
||||||
|
from typing import Iterable
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
|
from .._code.source import getfslineno
|
||||||
from ..compat import ascii_escaped
|
from ..compat import ascii_escaped
|
||||||
from ..compat import ATTRS_EQ_FIELD
|
|
||||||
from ..compat import getfslineno
|
|
||||||
from ..compat import NOTSET
|
from ..compat import NOTSET
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.warning_types import PytestUnknownMarkWarning
|
from _pytest.warning_types import PytestUnknownMarkWarning
|
||||||
|
@ -144,7 +147,15 @@ class Mark:
|
||||||
#: keyword arguments of the mark decorator
|
#: keyword arguments of the mark decorator
|
||||||
kwargs = attr.ib() # Dict[str, object]
|
kwargs = attr.ib() # Dict[str, object]
|
||||||
|
|
||||||
def combined_with(self, other):
|
#: source Mark for ids with parametrize Marks
|
||||||
|
_param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False)
|
||||||
|
#: resolved/generated ids with parametrize Marks
|
||||||
|
_param_ids_generated = attr.ib(type=Optional[List[str]], default=None, repr=False)
|
||||||
|
|
||||||
|
def _has_param_ids(self):
|
||||||
|
return "ids" in self.kwargs or len(self.args) >= 4
|
||||||
|
|
||||||
|
def combined_with(self, other: "Mark") -> "Mark":
|
||||||
"""
|
"""
|
||||||
:param other: the mark to combine with
|
:param other: the mark to combine with
|
||||||
:type other: Mark
|
:type other: Mark
|
||||||
|
@ -153,8 +164,20 @@ class Mark:
|
||||||
combines by appending args and merging the mappings
|
combines by appending args and merging the mappings
|
||||||
"""
|
"""
|
||||||
assert self.name == other.name
|
assert self.name == other.name
|
||||||
|
|
||||||
|
# Remember source of ids with parametrize Marks.
|
||||||
|
param_ids_from = None # type: Optional[Mark]
|
||||||
|
if self.name == "parametrize":
|
||||||
|
if other._has_param_ids():
|
||||||
|
param_ids_from = other
|
||||||
|
elif self._has_param_ids():
|
||||||
|
param_ids_from = self
|
||||||
|
|
||||||
return Mark(
|
return Mark(
|
||||||
self.name, self.args + other.args, dict(self.kwargs, **other.kwargs)
|
self.name,
|
||||||
|
self.args + other.args,
|
||||||
|
dict(self.kwargs, **other.kwargs),
|
||||||
|
param_ids_from=param_ids_from,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -249,7 +272,7 @@ def get_unpacked_marks(obj):
|
||||||
return normalize_mark_list(mark_list)
|
return normalize_mark_list(mark_list)
|
||||||
|
|
||||||
|
|
||||||
def normalize_mark_list(mark_list):
|
def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]:
|
||||||
"""
|
"""
|
||||||
normalizes marker decorating helpers to mark objects
|
normalizes marker decorating helpers to mark objects
|
||||||
|
|
||||||
|
@ -325,6 +348,7 @@ class MarkGenerator:
|
||||||
"custom marks to avoid this warning - for details, see "
|
"custom marks to avoid this warning - for details, see "
|
||||||
"https://docs.pytest.org/en/latest/mark.html" % name,
|
"https://docs.pytest.org/en/latest/mark.html" % name,
|
||||||
PytestUnknownMarkWarning,
|
PytestUnknownMarkWarning,
|
||||||
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
return MarkDecorator(Mark(name, (), {}))
|
return MarkDecorator(Mark(name, (), {}))
|
||||||
|
@ -368,35 +392,3 @@ class NodeKeywords(MutableMapping):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<NodeKeywords for node {}>".format(self.node)
|
return "<NodeKeywords for node {}>".format(self.node)
|
||||||
|
|
||||||
|
|
||||||
# mypy cannot find this overload, remove when on attrs>=19.2
|
|
||||||
@attr.s(hash=False, **{ATTRS_EQ_FIELD: False}) # type: ignore
|
|
||||||
class NodeMarkers:
|
|
||||||
"""
|
|
||||||
internal structure for storing marks belonging to a node
|
|
||||||
|
|
||||||
..warning::
|
|
||||||
|
|
||||||
unstable api
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
own_markers = attr.ib(default=attr.Factory(list))
|
|
||||||
|
|
||||||
def update(self, add_markers):
|
|
||||||
"""update the own markers
|
|
||||||
"""
|
|
||||||
self.own_markers.extend(add_markers)
|
|
||||||
|
|
||||||
def find(self, name):
|
|
||||||
"""
|
|
||||||
find markers in own nodes or parent nodes
|
|
||||||
needs a better place
|
|
||||||
"""
|
|
||||||
for mark in self.own_markers:
|
|
||||||
if mark.name == name:
|
|
||||||
yield mark
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self.own_markers)
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
|
@ -108,7 +109,7 @@ class MonkeyPatch:
|
||||||
self._savesyspath = None
|
self._savesyspath = None
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def context(self):
|
def context(self) -> Generator["MonkeyPatch", None, None]:
|
||||||
"""
|
"""
|
||||||
Context manager that returns a new :class:`MonkeyPatch` object which
|
Context manager that returns a new :class:`MonkeyPatch` object which
|
||||||
undoes any patching done inside the ``with`` block upon exit:
|
undoes any patching done inside the ``with`` block upon exit:
|
||||||
|
|
|
@ -15,10 +15,12 @@ import _pytest._code
|
||||||
from _pytest._code.code import ExceptionChainRepr
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import ReprExceptionInfo
|
from _pytest._code.code import ReprExceptionInfo
|
||||||
|
from _pytest._code.source import getfslineno
|
||||||
from _pytest.compat import cached_property
|
from _pytest.compat import cached_property
|
||||||
from _pytest.compat import getfslineno
|
|
||||||
from _pytest.compat import TYPE_CHECKING
|
from _pytest.compat import TYPE_CHECKING
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
from _pytest.config import PytestPluginManager
|
||||||
|
from _pytest.deprecated import NODE_USE_FROM_PARENT
|
||||||
from _pytest.fixtures import FixtureDef
|
from _pytest.fixtures import FixtureDef
|
||||||
from _pytest.fixtures import FixtureLookupError
|
from _pytest.fixtures import FixtureLookupError
|
||||||
from _pytest.fixtures import FixtureLookupErrorRepr
|
from _pytest.fixtures import FixtureLookupErrorRepr
|
||||||
|
@ -74,7 +76,16 @@ def ischildnode(baseid, nodeid):
|
||||||
return node_parts[: len(base_parts)] == base_parts
|
return node_parts[: len(base_parts)] == base_parts
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
class NodeMeta(type):
|
||||||
|
def __call__(self, *k, **kw):
|
||||||
|
warnings.warn(NODE_USE_FROM_PARENT.format(name=self.__name__), stacklevel=2)
|
||||||
|
return super().__call__(*k, **kw)
|
||||||
|
|
||||||
|
def _create(self, *k, **kw):
|
||||||
|
return super().__call__(*k, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
class Node(metaclass=NodeMeta):
|
||||||
""" base class for Collector and Item the test collection tree.
|
""" base class for Collector and Item the test collection tree.
|
||||||
Collector subclasses have children, Items are terminal nodes."""
|
Collector subclasses have children, Items are terminal nodes."""
|
||||||
|
|
||||||
|
@ -134,6 +145,24 @@ class Node:
|
||||||
if self.name != "()":
|
if self.name != "()":
|
||||||
self._nodeid += "::" + self.name
|
self._nodeid += "::" + self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_parent(cls, parent: "Node", **kw):
|
||||||
|
"""
|
||||||
|
Public Constructor for Nodes
|
||||||
|
|
||||||
|
This indirection got introduced in order to enable removing
|
||||||
|
the fragile logic from the node constructors.
|
||||||
|
|
||||||
|
Subclasses can use ``super().from_parent(...)`` when overriding the construction
|
||||||
|
|
||||||
|
:param parent: the parent node of this test Node
|
||||||
|
"""
|
||||||
|
if "config" in kw:
|
||||||
|
raise TypeError("config is not a valid argument for from_parent")
|
||||||
|
if "session" in kw:
|
||||||
|
raise TypeError("session is not a valid argument for from_parent")
|
||||||
|
return cls._create(parent=parent, **kw)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ihook(self):
|
def ihook(self):
|
||||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||||
|
@ -332,7 +361,9 @@ class Node:
|
||||||
return self._repr_failure_py(excinfo, style)
|
return self._repr_failure_py(excinfo, style)
|
||||||
|
|
||||||
|
|
||||||
def get_fslocation_from_item(item):
|
def get_fslocation_from_item(
|
||||||
|
item: "Item",
|
||||||
|
) -> Tuple[Union[str, py.path.local], Optional[int]]:
|
||||||
"""Tries to extract the actual location from an item, depending on available attributes:
|
"""Tries to extract the actual location from an item, depending on available attributes:
|
||||||
|
|
||||||
* "fslocation": a pair (path, lineno)
|
* "fslocation": a pair (path, lineno)
|
||||||
|
@ -341,9 +372,10 @@ def get_fslocation_from_item(item):
|
||||||
|
|
||||||
:rtype: a tuple of (str|LocalPath, int) with filename and line number.
|
:rtype: a tuple of (str|LocalPath, int) with filename and line number.
|
||||||
"""
|
"""
|
||||||
result = getattr(item, "location", None)
|
try:
|
||||||
if result is not None:
|
return item.location[:2]
|
||||||
return result[:2]
|
except AttributeError:
|
||||||
|
pass
|
||||||
obj = getattr(item, "obj", None)
|
obj = getattr(item, "obj", None)
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
return getfslineno(obj)
|
return getfslineno(obj)
|
||||||
|
@ -366,12 +398,14 @@ class Collector(Node):
|
||||||
|
|
||||||
def repr_failure(self, excinfo):
|
def repr_failure(self, excinfo):
|
||||||
""" represent a collection failure. """
|
""" represent a collection failure. """
|
||||||
if excinfo.errisinstance(self.CollectError):
|
if excinfo.errisinstance(self.CollectError) and not self.config.getoption(
|
||||||
|
"fulltrace", False
|
||||||
|
):
|
||||||
exc = excinfo.value
|
exc = excinfo.value
|
||||||
return str(exc.args[0])
|
return str(exc.args[0])
|
||||||
|
|
||||||
# Respect explicit tbstyle option, but default to "short"
|
# Respect explicit tbstyle option, but default to "short"
|
||||||
# (None._repr_failure_py defaults to "long" without "fulltrace" option).
|
# (_repr_failure_py uses "long" with "fulltrace" option always).
|
||||||
tbstyle = self.config.getoption("tbstyle", "auto")
|
tbstyle = self.config.getoption("tbstyle", "auto")
|
||||||
if tbstyle == "auto":
|
if tbstyle == "auto":
|
||||||
tbstyle = "short"
|
tbstyle = "short"
|
||||||
|
@ -393,6 +427,20 @@ def _check_initialpaths_for_relpath(session, fspath):
|
||||||
return fspath.relto(initial_path)
|
return fspath.relto(initial_path)
|
||||||
|
|
||||||
|
|
||||||
|
class FSHookProxy:
|
||||||
|
def __init__(
|
||||||
|
self, fspath: py.path.local, pm: PytestPluginManager, remove_mods
|
||||||
|
) -> None:
|
||||||
|
self.fspath = fspath
|
||||||
|
self.pm = pm
|
||||||
|
self.remove_mods = remove_mods
|
||||||
|
|
||||||
|
def __getattr__(self, name: str):
|
||||||
|
x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
|
||||||
|
self.__dict__[name] = x
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
class FSCollector(Collector):
|
class FSCollector(Collector):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, fspath: py.path.local, parent=None, config=None, session=None, nodeid=None
|
self, fspath: py.path.local, parent=None, config=None, session=None, nodeid=None
|
||||||
|
@ -417,6 +465,42 @@ class FSCollector(Collector):
|
||||||
|
|
||||||
super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath)
|
super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath)
|
||||||
|
|
||||||
|
self._norecursepatterns = self.config.getini("norecursedirs")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_parent(cls, parent, *, fspath):
|
||||||
|
"""
|
||||||
|
The public constructor
|
||||||
|
"""
|
||||||
|
return super().from_parent(parent=parent, fspath=fspath)
|
||||||
|
|
||||||
|
def _gethookproxy(self, fspath: py.path.local):
|
||||||
|
# check if we have the common case of running
|
||||||
|
# hooks with all conftest.py files
|
||||||
|
pm = self.config.pluginmanager
|
||||||
|
my_conftestmodules = pm._getconftestmodules(fspath)
|
||||||
|
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
||||||
|
if remove_mods:
|
||||||
|
# one or more conftests are not in use at this fspath
|
||||||
|
proxy = FSHookProxy(fspath, pm, remove_mods)
|
||||||
|
else:
|
||||||
|
# all plugins are active for this fspath
|
||||||
|
proxy = self.config.hook
|
||||||
|
return proxy
|
||||||
|
|
||||||
|
def _recurse(self, dirpath: py.path.local) -> bool:
|
||||||
|
if dirpath.basename == "__pycache__":
|
||||||
|
return False
|
||||||
|
ihook = self._gethookproxy(dirpath.dirpath())
|
||||||
|
if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
|
||||||
|
return False
|
||||||
|
for pat in self._norecursepatterns:
|
||||||
|
if dirpath.check(fnmatch=pat):
|
||||||
|
return False
|
||||||
|
ihook = self._gethookproxy(dirpath)
|
||||||
|
ihook.pytest_collect_directory(path=dirpath, parent=self)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class File(FSCollector):
|
class File(FSCollector):
|
||||||
""" base class for collecting tests from a file. """
|
""" base class for collecting tests from a file. """
|
||||||
|
|
|
@ -29,12 +29,17 @@ from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.capture import MultiCapture
|
from _pytest.capture import MultiCapture
|
||||||
from _pytest.capture import SysCapture
|
from _pytest.capture import SysCapture
|
||||||
from _pytest.compat import TYPE_CHECKING
|
from _pytest.compat import TYPE_CHECKING
|
||||||
|
from _pytest.config import _PluggyPlugin
|
||||||
|
from _pytest.config import ExitCode
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.main import ExitCode
|
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
from _pytest.nodes import Collector
|
||||||
|
from _pytest.nodes import Item
|
||||||
from _pytest.pathlib import Path
|
from _pytest.pathlib import Path
|
||||||
|
from _pytest.python import Module
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
|
from _pytest.tmpdir import TempdirFactory
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
@ -408,8 +413,8 @@ class RunResult:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ret: Union[int, ExitCode],
|
ret: Union[int, ExitCode],
|
||||||
outlines: Sequence[str],
|
outlines: List[str],
|
||||||
errlines: Sequence[str],
|
errlines: List[str],
|
||||||
duration: float,
|
duration: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
|
@ -534,13 +539,15 @@ class Testdir:
|
||||||
class TimeoutExpired(Exception):
|
class TimeoutExpired(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self, request, tmpdir_factory):
|
def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> None:
|
||||||
self.request = request
|
self.request = request
|
||||||
self._mod_collections = WeakKeyDictionary()
|
self._mod_collections = (
|
||||||
|
WeakKeyDictionary()
|
||||||
|
) # type: WeakKeyDictionary[Module, List[Union[Item, Collector]]]
|
||||||
name = request.function.__name__
|
name = request.function.__name__
|
||||||
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
|
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
|
||||||
self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True)
|
self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True)
|
||||||
self.plugins = []
|
self.plugins = [] # type: List[Union[str, _PluggyPlugin]]
|
||||||
self._cwd_snapshot = CwdSnapshot()
|
self._cwd_snapshot = CwdSnapshot()
|
||||||
self._sys_path_snapshot = SysPathsSnapshot()
|
self._sys_path_snapshot = SysPathsSnapshot()
|
||||||
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
|
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
|
||||||
|
@ -554,10 +561,12 @@ class Testdir:
|
||||||
mp.delenv("TOX_ENV_DIR", raising=False)
|
mp.delenv("TOX_ENV_DIR", raising=False)
|
||||||
# Discard outer pytest options.
|
# Discard outer pytest options.
|
||||||
mp.delenv("PYTEST_ADDOPTS", raising=False)
|
mp.delenv("PYTEST_ADDOPTS", raising=False)
|
||||||
|
# Ensure no user config is used.
|
||||||
# Environment (updates) for inner runs.
|
|
||||||
tmphome = str(self.tmpdir)
|
tmphome = str(self.tmpdir)
|
||||||
self._env_run_update = {"HOME": tmphome, "USERPROFILE": tmphome}
|
mp.setenv("HOME", tmphome)
|
||||||
|
mp.setenv("USERPROFILE", tmphome)
|
||||||
|
# Do not use colors for inner runs by default.
|
||||||
|
mp.setenv("PY_COLORS", "0")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Testdir {!r}>".format(self.tmpdir)
|
return "<Testdir {!r}>".format(self.tmpdir)
|
||||||
|
@ -601,14 +610,14 @@ class Testdir:
|
||||||
"""
|
"""
|
||||||
self.tmpdir.chdir()
|
self.tmpdir.chdir()
|
||||||
|
|
||||||
def _makefile(self, ext, args, kwargs, encoding="utf-8"):
|
def _makefile(self, ext, lines, files, encoding="utf-8"):
|
||||||
items = list(kwargs.items())
|
items = list(files.items())
|
||||||
|
|
||||||
def to_text(s):
|
def to_text(s):
|
||||||
return s.decode(encoding) if isinstance(s, bytes) else str(s)
|
return s.decode(encoding) if isinstance(s, bytes) else str(s)
|
||||||
|
|
||||||
if args:
|
if lines:
|
||||||
source = "\n".join(to_text(x) for x in args)
|
source = "\n".join(to_text(x) for x in lines)
|
||||||
basename = self.request.function.__name__
|
basename = self.request.function.__name__
|
||||||
items.insert(0, (basename, source))
|
items.insert(0, (basename, source))
|
||||||
|
|
||||||
|
@ -753,7 +762,7 @@ class Testdir:
|
||||||
:param arg: a :py:class:`py.path.local` instance of the file
|
:param arg: a :py:class:`py.path.local` instance of the file
|
||||||
|
|
||||||
"""
|
"""
|
||||||
session = Session(config)
|
session = Session.from_config(config)
|
||||||
assert "::" not in str(arg)
|
assert "::" not in str(arg)
|
||||||
p = py.path.local(arg)
|
p = py.path.local(arg)
|
||||||
config.hook.pytest_sessionstart(session=session)
|
config.hook.pytest_sessionstart(session=session)
|
||||||
|
@ -771,7 +780,7 @@ class Testdir:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
config = self.parseconfigure(path)
|
config = self.parseconfigure(path)
|
||||||
session = Session(config)
|
session = Session.from_config(config)
|
||||||
x = session.fspath.bestrelpath(path)
|
x = session.fspath.bestrelpath(path)
|
||||||
config.hook.pytest_sessionstart(session=session)
|
config.hook.pytest_sessionstart(session=session)
|
||||||
res = session.perform_collect([x], genitems=False)[0]
|
res = session.perform_collect([x], genitems=False)[0]
|
||||||
|
@ -863,12 +872,6 @@ class Testdir:
|
||||||
plugins = list(plugins)
|
plugins = list(plugins)
|
||||||
finalizers = []
|
finalizers = []
|
||||||
try:
|
try:
|
||||||
# Do not load user config (during runs only).
|
|
||||||
mp_run = MonkeyPatch()
|
|
||||||
for k, v in self._env_run_update.items():
|
|
||||||
mp_run.setenv(k, v)
|
|
||||||
finalizers.append(mp_run.undo)
|
|
||||||
|
|
||||||
# Any sys.module or sys.path changes done while running pytest
|
# Any sys.module or sys.path changes done while running pytest
|
||||||
# inline should be reverted after the test run completes to avoid
|
# inline should be reverted after the test run completes to avoid
|
||||||
# clashing with later inline tests run within the same pytest test,
|
# clashing with later inline tests run within the same pytest test,
|
||||||
|
@ -1064,7 +1067,9 @@ class Testdir:
|
||||||
self.config = config = self.parseconfigure(path, *configargs)
|
self.config = config = self.parseconfigure(path, *configargs)
|
||||||
return self.getnode(config, path)
|
return self.getnode(config, path)
|
||||||
|
|
||||||
def collect_by_name(self, modcol, name):
|
def collect_by_name(
|
||||||
|
self, modcol: Module, name: str
|
||||||
|
) -> Optional[Union[Item, Collector]]:
|
||||||
"""Return the collection node for name from the module collection.
|
"""Return the collection node for name from the module collection.
|
||||||
|
|
||||||
This will search a module collection node for a collection node
|
This will search a module collection node for a collection node
|
||||||
|
@ -1073,13 +1078,13 @@ class Testdir:
|
||||||
:param modcol: a module collection node; see :py:meth:`getmodulecol`
|
:param modcol: a module collection node; see :py:meth:`getmodulecol`
|
||||||
|
|
||||||
:param name: the name of the node to return
|
:param name: the name of the node to return
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if modcol not in self._mod_collections:
|
if modcol not in self._mod_collections:
|
||||||
self._mod_collections[modcol] = list(modcol.collect())
|
self._mod_collections[modcol] = list(modcol.collect())
|
||||||
for colitem in self._mod_collections[modcol]:
|
for colitem in self._mod_collections[modcol]:
|
||||||
if colitem.name == name:
|
if colitem.name == name:
|
||||||
return colitem
|
return colitem
|
||||||
|
return None
|
||||||
|
|
||||||
def popen(
|
def popen(
|
||||||
self,
|
self,
|
||||||
|
@ -1101,7 +1106,6 @@ class Testdir:
|
||||||
env["PYTHONPATH"] = os.pathsep.join(
|
env["PYTHONPATH"] = os.pathsep.join(
|
||||||
filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
|
filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
|
||||||
)
|
)
|
||||||
env.update(self._env_run_update)
|
|
||||||
kw["env"] = env
|
kw["env"] = env
|
||||||
|
|
||||||
if stdin is Testdir.CLOSE_STDIN:
|
if stdin is Testdir.CLOSE_STDIN:
|
||||||
|
@ -1273,11 +1277,7 @@ class Testdir:
|
||||||
pytest.skip("pexpect.spawn not available")
|
pytest.skip("pexpect.spawn not available")
|
||||||
logfile = self.tmpdir.join("spawn.out").open("wb")
|
logfile = self.tmpdir.join("spawn.out").open("wb")
|
||||||
|
|
||||||
# Do not load user config.
|
child = pexpect.spawn(cmd, logfile=logfile)
|
||||||
env = os.environ.copy()
|
|
||||||
env.update(self._env_run_update)
|
|
||||||
|
|
||||||
child = pexpect.spawn(cmd, logfile=logfile, env=env)
|
|
||||||
self.request.addfinalizer(logfile.close)
|
self.request.addfinalizer(logfile.close)
|
||||||
child.timeout = expect_timeout
|
child.timeout = expect_timeout
|
||||||
return child
|
return child
|
||||||
|
@ -1318,49 +1318,32 @@ class LineMatcher:
|
||||||
|
|
||||||
The constructor takes a list of lines without their trailing newlines, i.e.
|
The constructor takes a list of lines without their trailing newlines, i.e.
|
||||||
``text.splitlines()``.
|
``text.splitlines()``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, lines):
|
def __init__(self, lines: List[str]) -> None:
|
||||||
self.lines = lines
|
self.lines = lines
|
||||||
self._log_output = []
|
self._log_output = [] # type: List[str]
|
||||||
|
|
||||||
def str(self):
|
def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]:
|
||||||
"""Return the entire original text."""
|
|
||||||
return "\n".join(self.lines)
|
|
||||||
|
|
||||||
def _getlines(self, lines2):
|
|
||||||
if isinstance(lines2, str):
|
if isinstance(lines2, str):
|
||||||
lines2 = Source(lines2)
|
lines2 = Source(lines2)
|
||||||
if isinstance(lines2, Source):
|
if isinstance(lines2, Source):
|
||||||
lines2 = lines2.strip().lines
|
lines2 = lines2.strip().lines
|
||||||
return lines2
|
return lines2
|
||||||
|
|
||||||
def fnmatch_lines_random(self, lines2):
|
def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
|
||||||
"""Check lines exist in the output using in any order.
|
"""Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).
|
||||||
|
|
||||||
Lines are checked using ``fnmatch.fnmatch``. The argument is a list of
|
|
||||||
lines which have to occur in the output, in any order.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._match_lines_random(lines2, fnmatch)
|
self._match_lines_random(lines2, fnmatch)
|
||||||
|
|
||||||
def re_match_lines_random(self, lines2):
|
def re_match_lines_random(self, lines2: Sequence[str]) -> None:
|
||||||
"""Check lines exist in the output using ``re.match``, in any order.
|
"""Check lines exist in the output in any order (using :func:`python:re.match`).
|
||||||
|
|
||||||
The argument is a list of lines which have to occur in the output, in
|
|
||||||
any order.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._match_lines_random(lines2, lambda name, pat: re.match(pat, name))
|
self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
|
||||||
|
|
||||||
def _match_lines_random(self, lines2, match_func):
|
def _match_lines_random(
|
||||||
"""Check lines exist in the output.
|
self, lines2: Sequence[str], match_func: Callable[[str, str], bool]
|
||||||
|
) -> None:
|
||||||
The argument is a list of lines which have to occur in the output, in
|
|
||||||
any order. Each line can contain glob whildcards.
|
|
||||||
|
|
||||||
"""
|
|
||||||
lines2 = self._getlines(lines2)
|
lines2 = self._getlines(lines2)
|
||||||
for line in lines2:
|
for line in lines2:
|
||||||
for x in self.lines:
|
for x in self.lines:
|
||||||
|
@ -1371,46 +1354,67 @@ class LineMatcher:
|
||||||
self._log("line %r not found in output" % line)
|
self._log("line %r not found in output" % line)
|
||||||
raise ValueError(self._log_text)
|
raise ValueError(self._log_text)
|
||||||
|
|
||||||
def get_lines_after(self, fnline):
|
def get_lines_after(self, fnline: str) -> Sequence[str]:
|
||||||
"""Return all lines following the given line in the text.
|
"""Return all lines following the given line in the text.
|
||||||
|
|
||||||
The given line can contain glob wildcards.
|
The given line can contain glob wildcards.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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("line %r not found in output" % fnline)
|
raise ValueError("line %r not found in output" % fnline)
|
||||||
|
|
||||||
def _log(self, *args):
|
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))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _log_text(self):
|
def _log_text(self) -> str:
|
||||||
return "\n".join(self._log_output)
|
return "\n".join(self._log_output)
|
||||||
|
|
||||||
def fnmatch_lines(self, lines2):
|
def fnmatch_lines(
|
||||||
"""Search captured text for matching lines using ``fnmatch.fnmatch``.
|
self, lines2: Sequence[str], *, consecutive: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""Check lines exist in the output (using :func:`python:fnmatch.fnmatch`).
|
||||||
|
|
||||||
The argument is a list of lines which have to match and can use glob
|
The argument is a list of lines which have to match and can use glob
|
||||||
wildcards. If they do not match a pytest.fail() is called. The
|
wildcards. If they do not match a pytest.fail() is called. The
|
||||||
matches and non-matches are also shown as part of the error message.
|
matches and non-matches are also shown as part of the error message.
|
||||||
|
|
||||||
|
:param lines2: string patterns to match.
|
||||||
|
:param consecutive: match lines consecutive?
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._match_lines(lines2, fnmatch, "fnmatch")
|
self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
|
||||||
|
|
||||||
def re_match_lines(self, lines2):
|
def re_match_lines(
|
||||||
"""Search captured text for matching lines using ``re.match``.
|
self, lines2: Sequence[str], *, consecutive: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""Check lines exist in the output (using :func:`python:re.match`).
|
||||||
|
|
||||||
The argument is a list of lines which have to match using ``re.match``.
|
The argument is a list of lines which have to match using ``re.match``.
|
||||||
If they do not match a pytest.fail() is called.
|
If they do not match a pytest.fail() is called.
|
||||||
|
|
||||||
The matches and non-matches are also shown as part of the error message.
|
The matches and non-matches are also shown as part of the error message.
|
||||||
|
|
||||||
|
:param lines2: string patterns to match.
|
||||||
|
:param consecutive: match lines consecutively?
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match")
|
self._match_lines(
|
||||||
|
lines2,
|
||||||
|
lambda name, pat: bool(re.match(pat, name)),
|
||||||
|
"re.match",
|
||||||
|
consecutive=consecutive,
|
||||||
|
)
|
||||||
|
|
||||||
def _match_lines(self, lines2, match_func, match_nickname):
|
def _match_lines(
|
||||||
|
self,
|
||||||
|
lines2: Sequence[str],
|
||||||
|
match_func: Callable[[str, str], bool],
|
||||||
|
match_nickname: str,
|
||||||
|
*,
|
||||||
|
consecutive: bool = False
|
||||||
|
) -> None:
|
||||||
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
|
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
|
||||||
|
|
||||||
:param list[str] lines2: list of string patterns to match. The actual
|
:param list[str] lines2: list of string patterns to match. The actual
|
||||||
|
@ -1420,28 +1424,40 @@ class LineMatcher:
|
||||||
pattern
|
pattern
|
||||||
:param str match_nickname: the nickname for the match function that
|
:param str match_nickname: the nickname for the match function that
|
||||||
will be logged to stdout when a match occurs
|
will be logged to stdout when a match occurs
|
||||||
|
:param consecutive: match lines consecutively?
|
||||||
"""
|
"""
|
||||||
assert isinstance(lines2, collections.abc.Sequence)
|
if not isinstance(lines2, collections.abc.Sequence):
|
||||||
|
raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__))
|
||||||
lines2 = self._getlines(lines2)
|
lines2 = self._getlines(lines2)
|
||||||
lines1 = self.lines[:]
|
lines1 = self.lines[:]
|
||||||
nextline = None
|
nextline = None
|
||||||
extralines = []
|
extralines = []
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
wnick = len(match_nickname) + 1
|
wnick = len(match_nickname) + 1
|
||||||
|
started = False
|
||||||
for line in lines2:
|
for line in lines2:
|
||||||
nomatchprinted = False
|
nomatchprinted = False
|
||||||
while lines1:
|
while lines1:
|
||||||
nextline = lines1.pop(0)
|
nextline = lines1.pop(0)
|
||||||
if line == nextline:
|
if line == nextline:
|
||||||
self._log("exact match:", repr(line))
|
self._log("exact match:", repr(line))
|
||||||
|
started = True
|
||||||
break
|
break
|
||||||
elif match_func(nextline, line):
|
elif match_func(nextline, line):
|
||||||
self._log("%s:" % 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)
|
||||||
)
|
)
|
||||||
|
started = True
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
if consecutive and started:
|
||||||
|
msg = "no consecutive match: {!r}".format(line)
|
||||||
|
self._log(msg)
|
||||||
|
self._log(
|
||||||
|
"{:>{width}}".format("with:", width=wnick), repr(nextline)
|
||||||
|
)
|
||||||
|
self._fail(msg)
|
||||||
if not nomatchprinted:
|
if not nomatchprinted:
|
||||||
self._log(
|
self._log(
|
||||||
"{:>{width}}".format("nomatch:", width=wnick), repr(line)
|
"{:>{width}}".format("nomatch:", width=wnick), repr(line)
|
||||||
|
@ -1455,7 +1471,7 @@ class LineMatcher:
|
||||||
self._fail(msg)
|
self._fail(msg)
|
||||||
self._log_output = []
|
self._log_output = []
|
||||||
|
|
||||||
def no_fnmatch_line(self, pat):
|
def no_fnmatch_line(self, pat: str) -> None:
|
||||||
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
|
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
|
||||||
|
|
||||||
:param str pat: the pattern to match lines.
|
:param str pat: the pattern to match lines.
|
||||||
|
@ -1463,15 +1479,19 @@ class LineMatcher:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._no_match_line(pat, fnmatch, "fnmatch")
|
self._no_match_line(pat, fnmatch, "fnmatch")
|
||||||
|
|
||||||
def no_re_match_line(self, pat):
|
def no_re_match_line(self, pat: str) -> None:
|
||||||
"""Ensure captured lines do not match the given pattern, using ``re.match``.
|
"""Ensure captured lines do not match the given pattern, using ``re.match``.
|
||||||
|
|
||||||
:param str pat: the regular expression to match lines.
|
:param str pat: the regular expression to match lines.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._no_match_line(pat, lambda name, pat: re.match(pat, name), "re.match")
|
self._no_match_line(
|
||||||
|
pat, lambda name, pat: bool(re.match(pat, name)), "re.match"
|
||||||
|
)
|
||||||
|
|
||||||
def _no_match_line(self, pat, match_func, match_nickname):
|
def _no_match_line(
|
||||||
|
self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
|
||||||
|
) -> None:
|
||||||
"""Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``
|
"""Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``
|
||||||
|
|
||||||
:param str pat: the pattern to match lines
|
:param str pat: the pattern to match lines
|
||||||
|
@ -1492,8 +1512,12 @@ class LineMatcher:
|
||||||
self._log("{:>{width}}".format("and:", width=wnick), repr(line))
|
self._log("{:>{width}}".format("and:", width=wnick), repr(line))
|
||||||
self._log_output = []
|
self._log_output = []
|
||||||
|
|
||||||
def _fail(self, msg):
|
def _fail(self, msg: str) -> None:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
log_text = self._log_text
|
log_text = self._log_text
|
||||||
self._log_output = []
|
self._log_output = []
|
||||||
pytest.fail(log_text)
|
pytest.fail(log_text)
|
||||||
|
|
||||||
|
def str(self) -> str:
|
||||||
|
"""Return the entire original text."""
|
||||||
|
return "\n".join(self.lines)
|
||||||
|
|
|
@ -9,8 +9,9 @@ from collections import Counter
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from textwrap import dedent
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
@ -20,10 +21,11 @@ import _pytest
|
||||||
from _pytest import fixtures
|
from _pytest import fixtures
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest._code import filter_traceback
|
from _pytest._code import filter_traceback
|
||||||
|
from _pytest._code.code import ExceptionInfo
|
||||||
|
from _pytest._code.source import getfslineno
|
||||||
from _pytest.compat import ascii_escaped
|
from _pytest.compat import ascii_escaped
|
||||||
from _pytest.compat import get_default_arg_names
|
from _pytest.compat import get_default_arg_names
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import getfslineno
|
|
||||||
from _pytest.compat import getimfunc
|
from _pytest.compat import getimfunc
|
||||||
from _pytest.compat import getlocation
|
from _pytest.compat import getlocation
|
||||||
from _pytest.compat import is_generator
|
from _pytest.compat import is_generator
|
||||||
|
@ -35,9 +37,10 @@ from _pytest.compat import safe_isclass
|
||||||
from _pytest.compat import STRING_TYPES
|
from _pytest.compat import STRING_TYPES
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.deprecated import FUNCARGNAMES
|
from _pytest.deprecated import FUNCARGNAMES
|
||||||
from _pytest.main import FSHookProxy
|
|
||||||
from _pytest.mark import MARK_GEN
|
from _pytest.mark import MARK_GEN
|
||||||
|
from _pytest.mark import ParameterSet
|
||||||
from _pytest.mark.structures import get_unpacked_marks
|
from _pytest.mark.structures import get_unpacked_marks
|
||||||
|
from _pytest.mark.structures import Mark
|
||||||
from _pytest.mark.structures import normalize_mark_list
|
from _pytest.mark.structures import normalize_mark_list
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
|
@ -124,7 +127,7 @@ def pytest_cmdline_main(config):
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
||||||
metafunc.parametrize(*marker.args, **marker.kwargs)
|
metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
|
@ -147,27 +150,30 @@ def pytest_configure(config):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(trylast=True)
|
def async_warn(nodeid: str) -> None:
|
||||||
def pytest_pyfunc_call(pyfuncitem):
|
msg = "async def functions are not natively supported and have been skipped.\n"
|
||||||
def async_warn():
|
msg += (
|
||||||
msg = "async def functions are not natively supported and have been skipped.\n"
|
"You need to install a suitable plugin for your async framework, for example:\n"
|
||||||
msg += "You need to install a suitable plugin for your async framework, for example:\n"
|
)
|
||||||
msg += " - pytest-asyncio\n"
|
msg += " - pytest-asyncio\n"
|
||||||
msg += " - pytest-trio\n"
|
msg += " - pytest-trio\n"
|
||||||
msg += " - pytest-tornasync"
|
msg += " - pytest-tornasync"
|
||||||
warnings.warn(PytestUnhandledCoroutineWarning(msg.format(pyfuncitem.nodeid)))
|
warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))
|
||||||
skip(msg="async def function and no async plugin installed (see warnings)")
|
skip(msg="async def function and no async plugin installed (see warnings)")
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl(trylast=True)
|
||||||
|
def pytest_pyfunc_call(pyfuncitem: "Function"):
|
||||||
testfunction = pyfuncitem.obj
|
testfunction = pyfuncitem.obj
|
||||||
if iscoroutinefunction(testfunction) or (
|
if iscoroutinefunction(testfunction) or (
|
||||||
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction)
|
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction)
|
||||||
):
|
):
|
||||||
async_warn()
|
async_warn(pyfuncitem.nodeid)
|
||||||
funcargs = pyfuncitem.funcargs
|
funcargs = pyfuncitem.funcargs
|
||||||
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
|
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
|
||||||
result = testfunction(**testargs)
|
result = testfunction(**testargs)
|
||||||
if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
|
if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
|
||||||
async_warn()
|
async_warn(pyfuncitem.nodeid)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -190,8 +196,8 @@ def path_matches_patterns(path, patterns):
|
||||||
|
|
||||||
def pytest_pycollect_makemodule(path, parent):
|
def pytest_pycollect_makemodule(path, parent):
|
||||||
if path.basename == "__init__.py":
|
if path.basename == "__init__.py":
|
||||||
return Package(path, parent)
|
return Package.from_parent(parent, fspath=path)
|
||||||
return Module(path, parent)
|
return Module.from_parent(parent, fspath=path)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(hookwrapper=True)
|
||||||
|
@ -203,7 +209,7 @@ def pytest_pycollect_makeitem(collector, name, obj):
|
||||||
# nothing was collected elsewhere, let's do it here
|
# nothing was collected elsewhere, let's do it here
|
||||||
if safe_isclass(obj):
|
if safe_isclass(obj):
|
||||||
if collector.istestclass(obj, name):
|
if collector.istestclass(obj, name):
|
||||||
outcome.force_result(Class(name, parent=collector))
|
outcome.force_result(Class.from_parent(collector, name=name, obj=obj))
|
||||||
elif collector.istestfunction(obj, name):
|
elif collector.istestfunction(obj, name):
|
||||||
# mock seems to store unbound methods (issue473), normalize it
|
# mock seems to store unbound methods (issue473), normalize it
|
||||||
obj = getattr(obj, "__func__", obj)
|
obj = getattr(obj, "__func__", obj)
|
||||||
|
@ -222,7 +228,7 @@ def pytest_pycollect_makeitem(collector, name, obj):
|
||||||
)
|
)
|
||||||
elif getattr(obj, "__test__", True):
|
elif getattr(obj, "__test__", True):
|
||||||
if is_generator(obj):
|
if is_generator(obj):
|
||||||
res = Function(name, parent=collector)
|
res = Function.from_parent(collector, name=name)
|
||||||
reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
|
reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
|
||||||
name=name
|
name=name
|
||||||
)
|
)
|
||||||
|
@ -387,8 +393,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
cls = clscol and clscol.obj or None
|
cls = clscol and clscol.obj or None
|
||||||
fm = self.session._fixturemanager
|
fm = self.session._fixturemanager
|
||||||
|
|
||||||
definition = FunctionDefinition(name=name, parent=self, callobj=funcobj)
|
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
|
||||||
fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls)
|
fixtureinfo = definition._fixtureinfo
|
||||||
|
|
||||||
metafunc = Metafunc(
|
metafunc = Metafunc(
|
||||||
definition, fixtureinfo, self.config, cls=cls, module=module
|
definition, fixtureinfo, self.config, cls=cls, module=module
|
||||||
|
@ -402,7 +408,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
|
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
|
||||||
|
|
||||||
if not metafunc._calls:
|
if not metafunc._calls:
|
||||||
yield Function(name, parent=self, fixtureinfo=fixtureinfo)
|
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
|
||||||
else:
|
else:
|
||||||
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
|
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
|
||||||
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
||||||
|
@ -414,9 +420,9 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
|
|
||||||
for callspec in metafunc._calls:
|
for callspec in metafunc._calls:
|
||||||
subname = "{}[{}]".format(name, callspec.id)
|
subname = "{}[{}]".format(name, callspec.id)
|
||||||
yield Function(
|
yield Function.from_parent(
|
||||||
|
self,
|
||||||
name=subname,
|
name=subname,
|
||||||
parent=self,
|
|
||||||
callspec=callspec,
|
callspec=callspec,
|
||||||
callobj=funcobj,
|
callobj=funcobj,
|
||||||
fixtureinfo=fixtureinfo,
|
fixtureinfo=fixtureinfo,
|
||||||
|
@ -499,9 +505,7 @@ class Module(nodes.File, PyCollector):
|
||||||
try:
|
try:
|
||||||
mod = self.fspath.pyimport(ensuresyspath=importmode)
|
mod = self.fspath.pyimport(ensuresyspath=importmode)
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
raise self.CollectError(
|
raise self.CollectError(ExceptionInfo.from_current().getrepr(style="short"))
|
||||||
_pytest._code.ExceptionInfo.from_current().getrepr(style="short")
|
|
||||||
)
|
|
||||||
except self.fspath.ImportMismatchError:
|
except self.fspath.ImportMismatchError:
|
||||||
e = sys.exc_info()[1]
|
e = sys.exc_info()[1]
|
||||||
raise self.CollectError(
|
raise self.CollectError(
|
||||||
|
@ -514,8 +518,6 @@ class Module(nodes.File, PyCollector):
|
||||||
"unique basename for your test file modules" % e.args
|
"unique basename for your test file modules" % e.args
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from _pytest._code.code import ExceptionInfo
|
|
||||||
|
|
||||||
exc_info = ExceptionInfo.from_current()
|
exc_info = ExceptionInfo.from_current()
|
||||||
if self.config.getoption("verbose") < 2:
|
if self.config.getoption("verbose") < 2:
|
||||||
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
|
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
|
||||||
|
@ -545,15 +547,23 @@ class Module(nodes.File, PyCollector):
|
||||||
|
|
||||||
|
|
||||||
class Package(Module):
|
class Package(Module):
|
||||||
def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
fspath: py.path.local,
|
||||||
|
parent: nodes.Collector,
|
||||||
|
# NOTE: following args are unused:
|
||||||
|
config=None,
|
||||||
|
session=None,
|
||||||
|
nodeid=None,
|
||||||
|
) -> None:
|
||||||
|
# NOTE: could be just the following, but kept as-is for compat.
|
||||||
|
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
||||||
session = parent.session
|
session = parent.session
|
||||||
nodes.FSCollector.__init__(
|
nodes.FSCollector.__init__(
|
||||||
self, fspath, parent=parent, config=config, session=session, nodeid=nodeid
|
self, fspath, parent=parent, config=config, session=session, nodeid=nodeid
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = fspath.dirname
|
self.name = fspath.dirname
|
||||||
self.trace = session.trace
|
|
||||||
self._norecursepatterns = session._norecursepatterns
|
|
||||||
self.fspath = fspath
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
# not using fixtures to call setup_module here because autouse fixtures
|
# not using fixtures to call setup_module here because autouse fixtures
|
||||||
|
@ -571,32 +581,8 @@ class Package(Module):
|
||||||
func = partial(_call_with_optional_argument, teardown_module, self.obj)
|
func = partial(_call_with_optional_argument, teardown_module, self.obj)
|
||||||
self.addfinalizer(func)
|
self.addfinalizer(func)
|
||||||
|
|
||||||
def _recurse(self, dirpath):
|
def gethookproxy(self, fspath: py.path.local):
|
||||||
if dirpath.basename == "__pycache__":
|
return super()._gethookproxy(fspath)
|
||||||
return False
|
|
||||||
ihook = self.gethookproxy(dirpath.dirpath())
|
|
||||||
if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
|
|
||||||
return
|
|
||||||
for pat in self._norecursepatterns:
|
|
||||||
if dirpath.check(fnmatch=pat):
|
|
||||||
return False
|
|
||||||
ihook = self.gethookproxy(dirpath)
|
|
||||||
ihook.pytest_collect_directory(path=dirpath, parent=self)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def gethookproxy(self, fspath):
|
|
||||||
# check if we have the common case of running
|
|
||||||
# hooks with all conftest.py filesall conftest.py
|
|
||||||
pm = self.config.pluginmanager
|
|
||||||
my_conftestmodules = pm._getconftestmodules(fspath)
|
|
||||||
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
|
||||||
if remove_mods:
|
|
||||||
# one or more conftests are not in use at this fspath
|
|
||||||
proxy = FSHookProxy(fspath, pm, remove_mods)
|
|
||||||
else:
|
|
||||||
# all plugins are active for this fspath
|
|
||||||
proxy = self.config.hook
|
|
||||||
return proxy
|
|
||||||
|
|
||||||
def _collectfile(self, path, handle_dupes=True):
|
def _collectfile(self, path, handle_dupes=True):
|
||||||
assert (
|
assert (
|
||||||
|
@ -632,7 +618,7 @@ class Package(Module):
|
||||||
if init_module.check(file=1) and path_matches_patterns(
|
if init_module.check(file=1) and path_matches_patterns(
|
||||||
init_module, self.config.getini("python_files")
|
init_module, self.config.getini("python_files")
|
||||||
):
|
):
|
||||||
yield Module(init_module, self)
|
yield Module.from_parent(self, fspath=init_module)
|
||||||
pkg_prefixes = set()
|
pkg_prefixes = set()
|
||||||
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
|
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
|
||||||
# We will visit our own __init__.py file, in which case we skip it.
|
# We will visit our own __init__.py file, in which case we skip it.
|
||||||
|
@ -683,6 +669,13 @@ def _get_first_non_fixture_func(obj, names):
|
||||||
class Class(PyCollector):
|
class Class(PyCollector):
|
||||||
""" Collector for test methods. """
|
""" Collector for test methods. """
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_parent(cls, parent, *, name, obj=None):
|
||||||
|
"""
|
||||||
|
The public constructor
|
||||||
|
"""
|
||||||
|
return super().from_parent(name=name, parent=parent)
|
||||||
|
|
||||||
def collect(self):
|
def collect(self):
|
||||||
if not safe_getattr(self.obj, "__test__", True):
|
if not safe_getattr(self.obj, "__test__", True):
|
||||||
return []
|
return []
|
||||||
|
@ -708,7 +701,7 @@ class Class(PyCollector):
|
||||||
self._inject_setup_class_fixture()
|
self._inject_setup_class_fixture()
|
||||||
self._inject_setup_method_fixture()
|
self._inject_setup_method_fixture()
|
||||||
|
|
||||||
return [Instance(name="()", parent=self)]
|
return [Instance.from_parent(self, name="()")]
|
||||||
|
|
||||||
def _inject_setup_class_fixture(self):
|
def _inject_setup_class_fixture(self):
|
||||||
"""Injects a hidden autouse, class scoped fixture into the collected class object
|
"""Injects a hidden autouse, class scoped fixture into the collected class object
|
||||||
|
@ -778,45 +771,6 @@ class Instance(PyCollector):
|
||||||
return self.obj
|
return self.obj
|
||||||
|
|
||||||
|
|
||||||
class FunctionMixin(PyobjMixin):
|
|
||||||
""" mixin for the code common to Function and Generator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
""" perform setup for this test function. """
|
|
||||||
if isinstance(self.parent, Instance):
|
|
||||||
self.parent.newinstance()
|
|
||||||
self.obj = self._getobj()
|
|
||||||
|
|
||||||
def _prunetraceback(self, excinfo):
|
|
||||||
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
|
||||||
code = _pytest._code.Code(get_real_func(self.obj))
|
|
||||||
path, firstlineno = code.path, code.firstlineno
|
|
||||||
traceback = excinfo.traceback
|
|
||||||
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
|
||||||
if ntraceback == traceback:
|
|
||||||
ntraceback = ntraceback.cut(path=path)
|
|
||||||
if ntraceback == traceback:
|
|
||||||
ntraceback = ntraceback.filter(filter_traceback)
|
|
||||||
if not ntraceback:
|
|
||||||
ntraceback = traceback
|
|
||||||
|
|
||||||
excinfo.traceback = ntraceback.filter()
|
|
||||||
# issue364: mark all but first and last frames to
|
|
||||||
# only show a single-line message for each frame
|
|
||||||
if self.config.getoption("tbstyle", "auto") == "auto":
|
|
||||||
if len(excinfo.traceback) > 2:
|
|
||||||
for entry in excinfo.traceback[1:-1]:
|
|
||||||
entry.set_repr_style("short")
|
|
||||||
|
|
||||||
def repr_failure(self, excinfo, outerr=None):
|
|
||||||
assert outerr is None, "XXX outerr usage is deprecated"
|
|
||||||
style = self.config.getoption("tbstyle", "auto")
|
|
||||||
if style == "auto":
|
|
||||||
style = "long"
|
|
||||||
return self._repr_failure_py(excinfo, style=style)
|
|
||||||
|
|
||||||
|
|
||||||
def hasinit(obj):
|
def hasinit(obj):
|
||||||
init = getattr(obj, "__init__", None)
|
init = getattr(obj, "__init__", None)
|
||||||
if init:
|
if init:
|
||||||
|
@ -835,8 +789,6 @@ class CallSpec2:
|
||||||
self.funcargs = {}
|
self.funcargs = {}
|
||||||
self._idlist = []
|
self._idlist = []
|
||||||
self.params = {}
|
self.params = {}
|
||||||
self._globalid = NOTSET
|
|
||||||
self._globalparam = NOTSET
|
|
||||||
self._arg2scopenum = {} # used for sorting parametrized resources
|
self._arg2scopenum = {} # used for sorting parametrized resources
|
||||||
self.marks = []
|
self.marks = []
|
||||||
self.indices = {}
|
self.indices = {}
|
||||||
|
@ -849,8 +801,6 @@ class CallSpec2:
|
||||||
cs.indices.update(self.indices)
|
cs.indices.update(self.indices)
|
||||||
cs._arg2scopenum.update(self._arg2scopenum)
|
cs._arg2scopenum.update(self._arg2scopenum)
|
||||||
cs._idlist = list(self._idlist)
|
cs._idlist = list(self._idlist)
|
||||||
cs._globalid = self._globalid
|
|
||||||
cs._globalparam = self._globalparam
|
|
||||||
return cs
|
return cs
|
||||||
|
|
||||||
def _checkargnotcontained(self, arg):
|
def _checkargnotcontained(self, arg):
|
||||||
|
@ -861,13 +811,11 @@ class CallSpec2:
|
||||||
try:
|
try:
|
||||||
return self.params[name]
|
return self.params[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if self._globalparam is NOTSET:
|
raise ValueError(name)
|
||||||
raise ValueError(name)
|
|
||||||
return self._globalparam
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
return "-".join(map(str, filter(None, self._idlist)))
|
return "-".join(map(str, self._idlist))
|
||||||
|
|
||||||
def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum, param_index):
|
def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum, param_index):
|
||||||
for arg, val in zip(argnames, valset):
|
for arg, val in zip(argnames, valset):
|
||||||
|
@ -922,7 +870,16 @@ class Metafunc:
|
||||||
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
||||||
return self.fixturenames
|
return self.fixturenames
|
||||||
|
|
||||||
def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None):
|
def parametrize(
|
||||||
|
self,
|
||||||
|
argnames,
|
||||||
|
argvalues,
|
||||||
|
indirect=False,
|
||||||
|
ids=None,
|
||||||
|
scope=None,
|
||||||
|
*,
|
||||||
|
_param_mark: Optional[Mark] = None
|
||||||
|
):
|
||||||
""" Add new invocations to the underlying test function using the list
|
""" Add new invocations to the underlying test function using the list
|
||||||
of argvalues for the given argnames. Parametrization is performed
|
of argvalues for the given argnames. Parametrization is performed
|
||||||
during the collection phase. If you need to setup expensive resources
|
during the collection phase. If you need to setup expensive resources
|
||||||
|
@ -945,13 +902,22 @@ class Metafunc:
|
||||||
function so that it can perform more expensive setups during the
|
function so that it can perform more expensive setups during the
|
||||||
setup phase of a test rather than at collection time.
|
setup phase of a test rather than at collection time.
|
||||||
|
|
||||||
:arg ids: list of string ids, or a callable.
|
:arg ids: sequence of (or generator for) ids for ``argvalues``,
|
||||||
If strings, each is corresponding to the argvalues so that they are
|
or a callable to return part of the id for each argvalue.
|
||||||
part of the test id. If None is given as id of specific test, the
|
|
||||||
automatically generated id for that argument will be used.
|
With sequences (and generators like ``itertools.count()``) the
|
||||||
If callable, it should take one argument (a single argvalue) and return
|
returned ids should be of type ``string``, ``int``, ``float``,
|
||||||
a string or return None. If None, the automatically generated id for that
|
``bool``, or ``None``.
|
||||||
argument will be used.
|
They are mapped to the corresponding index in ``argvalues``.
|
||||||
|
``None`` means to use the auto-generated id.
|
||||||
|
|
||||||
|
If it is a callable it will be called for each entry in
|
||||||
|
``argvalues``, and the return value is used as part of the
|
||||||
|
auto-generated id for the whole set (where parts are joined with
|
||||||
|
dashes ("-")).
|
||||||
|
This is useful to provide more specific ids for certain items, e.g.
|
||||||
|
dates. Returning ``None`` will use an auto-generated id.
|
||||||
|
|
||||||
If no ids are provided they will be generated automatically from
|
If no ids are provided they will be generated automatically from
|
||||||
the argvalues.
|
the argvalues.
|
||||||
|
|
||||||
|
@ -961,7 +927,6 @@ class Metafunc:
|
||||||
to set a dynamic scope using test context or configuration.
|
to set a dynamic scope using test context or configuration.
|
||||||
"""
|
"""
|
||||||
from _pytest.fixtures import scope2index
|
from _pytest.fixtures import scope2index
|
||||||
from _pytest.mark import ParameterSet
|
|
||||||
|
|
||||||
argnames, parameters = ParameterSet._for_parametrize(
|
argnames, parameters = ParameterSet._for_parametrize(
|
||||||
argnames,
|
argnames,
|
||||||
|
@ -985,8 +950,20 @@ class Metafunc:
|
||||||
|
|
||||||
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
||||||
|
|
||||||
|
self._validate_explicit_parameters(argnames, indirect)
|
||||||
|
|
||||||
|
# Use any already (possibly) generated ids with parametrize Marks.
|
||||||
|
if _param_mark and _param_mark._param_ids_from:
|
||||||
|
generated_ids = _param_mark._param_ids_from._param_ids_generated
|
||||||
|
if generated_ids is not None:
|
||||||
|
ids = generated_ids
|
||||||
|
|
||||||
ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition)
|
ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition)
|
||||||
|
|
||||||
|
# Store used (possibly generated) ids with parametrize Marks.
|
||||||
|
if _param_mark and _param_mark._param_ids_from and generated_ids is None:
|
||||||
|
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
|
||||||
|
|
||||||
scopenum = scope2index(
|
scopenum = scope2index(
|
||||||
scope, descr="parametrize() call in {}".format(self.function.__name__)
|
scope, descr="parametrize() call in {}".format(self.function.__name__)
|
||||||
)
|
)
|
||||||
|
@ -1010,7 +987,9 @@ class Metafunc:
|
||||||
newcalls.append(newcallspec)
|
newcalls.append(newcallspec)
|
||||||
self._calls = newcalls
|
self._calls = newcalls
|
||||||
|
|
||||||
def _resolve_arg_ids(self, argnames, ids, parameters, item):
|
def _resolve_arg_ids(
|
||||||
|
self, argnames: List[str], ids, parameters: List[ParameterSet], item: nodes.Item
|
||||||
|
):
|
||||||
"""Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
|
"""Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
|
||||||
to ``parametrize``.
|
to ``parametrize``.
|
||||||
|
|
||||||
|
@ -1021,28 +1000,49 @@ class Metafunc:
|
||||||
:rtype: List[str]
|
:rtype: List[str]
|
||||||
:return: the list of ids for each argname given
|
:return: the list of ids for each argname given
|
||||||
"""
|
"""
|
||||||
from _pytest._io.saferepr import saferepr
|
|
||||||
|
|
||||||
idfn = None
|
idfn = None
|
||||||
if callable(ids):
|
if callable(ids):
|
||||||
idfn = ids
|
idfn = ids
|
||||||
ids = None
|
ids = None
|
||||||
if ids:
|
if ids:
|
||||||
func_name = self.function.__name__
|
func_name = self.function.__name__
|
||||||
if len(ids) != len(parameters):
|
ids = self._validate_ids(ids, parameters, func_name)
|
||||||
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
|
|
||||||
fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False)
|
|
||||||
for id_value in ids:
|
|
||||||
if id_value is not None and not isinstance(id_value, str):
|
|
||||||
msg = "In {}: ids must be list of strings, found: {} (type: {!r})"
|
|
||||||
fail(
|
|
||||||
msg.format(func_name, saferepr(id_value), type(id_value)),
|
|
||||||
pytrace=False,
|
|
||||||
)
|
|
||||||
ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item)
|
ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item)
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def _resolve_arg_value_types(self, argnames, indirect):
|
def _validate_ids(self, ids, parameters, func_name):
|
||||||
|
try:
|
||||||
|
len(ids)
|
||||||
|
except TypeError:
|
||||||
|
try:
|
||||||
|
it = iter(ids)
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError("ids must be a callable, sequence or generator")
|
||||||
|
else:
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
new_ids = list(itertools.islice(it, len(parameters)))
|
||||||
|
else:
|
||||||
|
new_ids = list(ids)
|
||||||
|
|
||||||
|
if len(new_ids) != len(parameters):
|
||||||
|
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
|
||||||
|
fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False)
|
||||||
|
for idx, id_value in enumerate(new_ids):
|
||||||
|
if id_value is not None:
|
||||||
|
if isinstance(id_value, (float, int, bool)):
|
||||||
|
new_ids[idx] = str(id_value)
|
||||||
|
elif not isinstance(id_value, str):
|
||||||
|
from _pytest._io.saferepr import saferepr
|
||||||
|
|
||||||
|
msg = "In {}: ids must be list of string/float/int/bool, found: {} (type: {!r}) at index {}"
|
||||||
|
fail(
|
||||||
|
msg.format(func_name, saferepr(id_value), type(id_value), idx),
|
||||||
|
pytrace=False,
|
||||||
|
)
|
||||||
|
return new_ids
|
||||||
|
|
||||||
|
def _resolve_arg_value_types(self, argnames: List[str], indirect) -> Dict[str, str]:
|
||||||
"""Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg"
|
"""Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg"
|
||||||
to the function, based on the ``indirect`` parameter of the parametrized() call.
|
to the function, based on the ``indirect`` parameter of the parametrized() call.
|
||||||
|
|
||||||
|
@ -1104,6 +1104,37 @@ class Metafunc:
|
||||||
pytrace=False,
|
pytrace=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _validate_explicit_parameters(self, argnames, indirect):
|
||||||
|
"""
|
||||||
|
The argnames in *parametrize* should either be declared explicitly via
|
||||||
|
indirect list or in the function signature
|
||||||
|
|
||||||
|
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
||||||
|
:param indirect: same ``indirect`` parameter of ``parametrize()``.
|
||||||
|
:raise ValueError: if validation fails
|
||||||
|
"""
|
||||||
|
if isinstance(indirect, bool) and indirect is True:
|
||||||
|
return
|
||||||
|
parametrized_argnames = list()
|
||||||
|
funcargnames = _pytest.compat.getfuncargnames(self.function)
|
||||||
|
if isinstance(indirect, Sequence):
|
||||||
|
for arg in argnames:
|
||||||
|
if arg not in indirect:
|
||||||
|
parametrized_argnames.append(arg)
|
||||||
|
elif indirect is False:
|
||||||
|
parametrized_argnames = argnames
|
||||||
|
|
||||||
|
usefixtures = fixtures.get_use_fixtures_for_node(self.definition)
|
||||||
|
|
||||||
|
for arg in parametrized_argnames:
|
||||||
|
if arg not in funcargnames and arg not in usefixtures:
|
||||||
|
func_name = self.function.__name__
|
||||||
|
msg = (
|
||||||
|
'In function "{func_name}":\n'
|
||||||
|
'Parameter "{arg}" should be declared explicitly via indirect or in function itself'
|
||||||
|
).format(func_name=func_name, arg=arg)
|
||||||
|
fail(msg, pytrace=False)
|
||||||
|
|
||||||
|
|
||||||
def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||||
"""Find the most appropriate scope for a parametrized call based on its arguments.
|
"""Find the most appropriate scope for a parametrized call based on its arguments.
|
||||||
|
@ -1155,8 +1186,7 @@ def _idval(val, argname, idx, idfn, item, config):
|
||||||
if generated_id is not None:
|
if generated_id is not None:
|
||||||
val = generated_id
|
val = generated_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# See issue https://github.com/pytest-dev/pytest/issues/2169
|
msg = "{}: error raised while trying to determine id of parameter '{}' at position {}"
|
||||||
msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n"
|
|
||||||
msg = msg.format(item.nodeid, argname, idx)
|
msg = msg.format(item.nodeid, argname, idx)
|
||||||
raise ValueError(msg) from e
|
raise ValueError(msg) from e
|
||||||
elif config:
|
elif config:
|
||||||
|
@ -1246,7 +1276,7 @@ def _show_fixtures_per_test(config, session):
|
||||||
else:
|
else:
|
||||||
funcargspec = argname
|
funcargspec = argname
|
||||||
tw.line(funcargspec, green=True)
|
tw.line(funcargspec, green=True)
|
||||||
fixture_doc = fixture_def.func.__doc__
|
fixture_doc = inspect.getdoc(fixture_def.func)
|
||||||
if fixture_doc:
|
if fixture_doc:
|
||||||
write_docstring(tw, fixture_doc)
|
write_docstring(tw, fixture_doc)
|
||||||
else:
|
else:
|
||||||
|
@ -1331,7 +1361,7 @@ def _showfixtures_main(config, session):
|
||||||
tw.write(" -- %s" % bestrel, yellow=True)
|
tw.write(" -- %s" % bestrel, yellow=True)
|
||||||
tw.write("\n")
|
tw.write("\n")
|
||||||
loc = getlocation(fixturedef.func, curdir)
|
loc = getlocation(fixturedef.func, curdir)
|
||||||
doc = fixturedef.func.__doc__ or ""
|
doc = inspect.getdoc(fixturedef.func)
|
||||||
if doc:
|
if doc:
|
||||||
write_docstring(tw, doc)
|
write_docstring(tw, doc)
|
||||||
else:
|
else:
|
||||||
|
@ -1340,21 +1370,11 @@ def _showfixtures_main(config, session):
|
||||||
|
|
||||||
|
|
||||||
def write_docstring(tw, doc, indent=" "):
|
def write_docstring(tw, doc, indent=" "):
|
||||||
doc = doc.rstrip()
|
for line in doc.split("\n"):
|
||||||
if "\n" in doc:
|
tw.write(indent + line + "\n")
|
||||||
firstline, rest = doc.split("\n", 1)
|
|
||||||
else:
|
|
||||||
firstline, rest = doc, ""
|
|
||||||
|
|
||||||
if firstline.strip():
|
|
||||||
tw.line(indent + firstline.strip())
|
|
||||||
|
|
||||||
if rest:
|
|
||||||
for line in dedent(rest).split("\n"):
|
|
||||||
tw.write(indent + line + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
class Function(FunctionMixin, nodes.Item):
|
class Function(PyobjMixin, nodes.Item):
|
||||||
""" a Function Item is responsible for setting up and executing a
|
""" a Function Item is responsible for setting up and executing a
|
||||||
Python test function.
|
Python test function.
|
||||||
"""
|
"""
|
||||||
|
@ -1420,6 +1440,13 @@ class Function(FunctionMixin, nodes.Item):
|
||||||
#: .. versionadded:: 3.0
|
#: .. versionadded:: 3.0
|
||||||
self.originalname = originalname
|
self.originalname = originalname
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
|
||||||
|
"""
|
||||||
|
The public constructor
|
||||||
|
"""
|
||||||
|
return super().from_parent(parent=parent, **kw)
|
||||||
|
|
||||||
def _initrequest(self):
|
def _initrequest(self):
|
||||||
self.funcargs = {}
|
self.funcargs = {}
|
||||||
self._request = fixtures.FixtureRequest(self)
|
self._request = fixtures.FixtureRequest(self)
|
||||||
|
@ -1451,10 +1478,40 @@ class Function(FunctionMixin, nodes.Item):
|
||||||
""" execute the underlying test function. """
|
""" execute the underlying test function. """
|
||||||
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
||||||
|
|
||||||
def setup(self):
|
def setup(self) -> None:
|
||||||
super().setup()
|
if isinstance(self.parent, Instance):
|
||||||
|
self.parent.newinstance()
|
||||||
|
self.obj = self._getobj()
|
||||||
fixtures.fillfixtures(self)
|
fixtures.fillfixtures(self)
|
||||||
|
|
||||||
|
def _prunetraceback(self, excinfo: ExceptionInfo) -> None:
|
||||||
|
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
||||||
|
code = _pytest._code.Code(get_real_func(self.obj))
|
||||||
|
path, firstlineno = code.path, code.firstlineno
|
||||||
|
traceback = excinfo.traceback
|
||||||
|
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
||||||
|
if ntraceback == traceback:
|
||||||
|
ntraceback = ntraceback.cut(path=path)
|
||||||
|
if ntraceback == traceback:
|
||||||
|
ntraceback = ntraceback.filter(filter_traceback)
|
||||||
|
if not ntraceback:
|
||||||
|
ntraceback = traceback
|
||||||
|
|
||||||
|
excinfo.traceback = ntraceback.filter()
|
||||||
|
# issue364: mark all but first and last frames to
|
||||||
|
# only show a single-line message for each frame
|
||||||
|
if self.config.getoption("tbstyle", "auto") == "auto":
|
||||||
|
if len(excinfo.traceback) > 2:
|
||||||
|
for entry in excinfo.traceback[1:-1]:
|
||||||
|
entry.set_repr_style("short")
|
||||||
|
|
||||||
|
def repr_failure(self, excinfo, outerr=None):
|
||||||
|
assert outerr is None, "XXX outerr usage is deprecated"
|
||||||
|
style = self.config.getoption("tbstyle", "auto")
|
||||||
|
if style == "auto":
|
||||||
|
style = "long"
|
||||||
|
return self._repr_failure_py(excinfo, style=style)
|
||||||
|
|
||||||
|
|
||||||
class FunctionDefinition(Function):
|
class FunctionDefinition(Function):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -685,7 +685,7 @@ def raises( # noqa: F811
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
for exc in filterfalse(
|
for exc in filterfalse(
|
||||||
inspect.isclass, always_iterable(expected_exception, BASE_TYPE)
|
inspect.isclass, always_iterable(expected_exception, BASE_TYPE) # type: ignore[arg-type] # noqa: F821
|
||||||
):
|
):
|
||||||
msg = "exceptions must be derived from BaseException, not %s"
|
msg = "exceptions must be derived from BaseException, not %s"
|
||||||
raise TypeError(msg % type(exc))
|
raise TypeError(msg % type(exc))
|
||||||
|
|
|
@ -6,6 +6,7 @@ from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
import attr
|
||||||
import py
|
import py
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionChainRepr
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
|
@ -375,8 +376,8 @@ def _report_to_json(report):
|
||||||
entry_data["data"][key] = value.__dict__.copy()
|
entry_data["data"][key] = value.__dict__.copy()
|
||||||
return entry_data
|
return entry_data
|
||||||
|
|
||||||
def serialize_repr_traceback(reprtraceback):
|
def serialize_repr_traceback(reprtraceback: ReprTraceback):
|
||||||
result = reprtraceback.__dict__.copy()
|
result = attr.asdict(reprtraceback)
|
||||||
result["reprentries"] = [
|
result["reprentries"] = [
|
||||||
serialize_repr_entry(x) for x in reprtraceback.reprentries
|
serialize_repr_entry(x) for x in reprtraceback.reprentries
|
||||||
]
|
]
|
||||||
|
@ -384,7 +385,7 @@ def _report_to_json(report):
|
||||||
|
|
||||||
def serialize_repr_crash(reprcrash: Optional[ReprFileLocation]):
|
def serialize_repr_crash(reprcrash: Optional[ReprFileLocation]):
|
||||||
if reprcrash is not None:
|
if reprcrash is not None:
|
||||||
return reprcrash.__dict__.copy()
|
return attr.asdict(reprcrash)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ def pytest_addoption(parser):
|
||||||
default=None,
|
default=None,
|
||||||
metavar="N",
|
metavar="N",
|
||||||
help="show N slowest setup/test durations (N=0 for all).",
|
help="show N slowest setup/test durations (N=0 for all).",
|
||||||
),
|
)
|
||||||
|
|
||||||
|
|
||||||
def pytest_terminal_summary(terminalreporter):
|
def pytest_terminal_summary(terminalreporter):
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue