Compare commits
73 Commits
7.2.0
...
should_do_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b31db4809b | ||
|
|
f6adebb990 | ||
|
|
b90e7b84d0 | ||
|
|
19807ab79a | ||
|
|
3e52124185 | ||
|
|
1eca228bd5 | ||
|
|
cab02e67d7 | ||
|
|
64dbc7a0a1 | ||
|
|
60d992677d | ||
|
|
0079decf29 | ||
|
|
3a58fc2d44 | ||
|
|
39b6bb551c | ||
|
|
9fbd67dd4b | ||
|
|
eca93db05b | ||
|
|
fb701b538c | ||
|
|
314e623304 | ||
|
|
62e75c7d55 | ||
|
|
fd30759d94 | ||
|
|
eb984a717a | ||
|
|
54f0fb3c63 | ||
|
|
49a4ed14cf | ||
|
|
f513d33d5a | ||
|
|
857e34ef85 | ||
|
|
99dfc19fe6 | ||
|
|
56544c11b5 | ||
|
|
7710e18b4c | ||
|
|
791b51d0fa | ||
|
|
bc4e70e048 | ||
|
|
b817aa457c | ||
|
|
66b28912ac | ||
|
|
cca029d55e | ||
|
|
d5466b3917 | ||
|
|
4fce29f15d | ||
|
|
69e3973d86 | ||
|
|
c842893b02 | ||
|
|
506b10d295 | ||
|
|
05061493cb | ||
|
|
f97f3dc3a3 | ||
|
|
3c31b0132f | ||
|
|
593178d909 | ||
|
|
54d5a63d14 | ||
|
|
b55e264a67 | ||
|
|
13d6114c0a | ||
|
|
b635e16d30 | ||
|
|
a092b3ab36 | ||
|
|
a006dabf6e | ||
|
|
aa7e9de91d | ||
|
|
6aec32163d | ||
|
|
2f33ea87c8 | ||
|
|
1ada62e237 | ||
|
|
50b232b0cb | ||
|
|
496196b15c | ||
|
|
0314b50c52 | ||
|
|
8e2de91bf8 | ||
|
|
692ab1160b | ||
|
|
549839bac5 | ||
|
|
646a46e5f4 | ||
|
|
f07017f91b | ||
|
|
a17d3b0c44 | ||
|
|
bbec1ce67f | ||
|
|
5a040aef97 | ||
|
|
c1d2168df6 | ||
|
|
bbe7cbae4a | ||
|
|
deae8f47f6 | ||
|
|
10f55f79af | ||
|
|
a6d244343f | ||
|
|
2b552c2240 | ||
|
|
54d7b9a08e | ||
|
|
6afc02abca | ||
|
|
e75e2d66a0 | ||
|
|
66db0b7522 | ||
|
|
3a68c08426 | ||
|
|
9e1804a6ee |
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -9,3 +9,9 @@ updates:
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
- dependency-type: indirect
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "03:00"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
7
.github/workflows/deploy.yml
vendored
7
.github/workflows/deploy.yml
vendored
@@ -23,13 +23,13 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.7"
|
||||
|
||||
@@ -43,9 +43,8 @@ jobs:
|
||||
python -m build
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.pypi_token }}
|
||||
|
||||
- name: Publish GitHub release notes
|
||||
|
||||
4
.github/workflows/prepare-release-pr.yml
vendored
4
.github/workflows/prepare-release-pr.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.8"
|
||||
|
||||
|
||||
6
.github/workflows/update-plugin-list.yml
vendored
6
.github/workflows/update-plugin-list.yml
vendored
@@ -20,12 +20,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6
|
||||
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
@@ -2,7 +2,7 @@ default_language_version:
|
||||
python: "3.10"
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==20.8b1]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -23,7 +23,7 @@ repos:
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v1.7.6
|
||||
rev: v2.0.0
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
@@ -31,7 +31,7 @@ repos:
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
@@ -39,26 +39,26 @@ repos:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v3.8.5
|
||||
rev: v3.9.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.1.0
|
||||
rev: v3.3.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.1.0
|
||||
rev: v2.2.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.10", "--include-version-classifiers"]
|
||||
args: ["--max-py-version=3.11", "--include-version-classifiers"]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.982
|
||||
rev: v0.991
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
@@ -2,9 +2,12 @@ version: 2
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: doc/en/requirements.txt
|
||||
- method: pip
|
||||
path: .
|
||||
# Install pytest first, then doc/en/requirements.txt.
|
||||
# This order is important to honor any pins in doc/en/requirements.txt
|
||||
# when the pinned library is also a dependency of pytest.
|
||||
- method: pip
|
||||
path: .
|
||||
- requirements: doc/en/requirements.txt
|
||||
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
|
||||
14
AUTHORS
14
AUTHORS
@@ -43,6 +43,7 @@ Ariel Pillemer
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
Ashish Kurmi
|
||||
Aviral Verma
|
||||
Aviv Palivoda
|
||||
Babak Keyvani
|
||||
@@ -57,6 +58,7 @@ Brian Maissy
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Cal Jacobson
|
||||
Cal Leeming
|
||||
Carl Friedrich Bolz
|
||||
Carlos Jenkins
|
||||
@@ -88,6 +90,7 @@ Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
Daniel Sánchez Castelló
|
||||
Daniel Valenzuela Zenteno
|
||||
Daniel Wandschneider
|
||||
Daniele Procida
|
||||
Danielle Jenkins
|
||||
@@ -182,8 +185,8 @@ Joseph Hunkeler
|
||||
Josh Karpel
|
||||
Joshua Bronson
|
||||
Jurko Gospodnetić
|
||||
Justyna Janczyszyn
|
||||
Justice Ndou
|
||||
Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Kamran Ahmad
|
||||
Karl O. Pinc
|
||||
@@ -222,6 +225,7 @@ Marcin Bachry
|
||||
Marco Gorelli
|
||||
Mark Abramowitz
|
||||
Mark Dickinson
|
||||
Marko Pacak
|
||||
Markus Unterwaditzer
|
||||
Martijn Faassen
|
||||
Martin Altmayer
|
||||
@@ -235,7 +239,6 @@ Matthias Hafner
|
||||
Maxim Filipenko
|
||||
Maximilian Cosmo Sitter
|
||||
mbyt
|
||||
Mickey Pashov
|
||||
Michael Aquilina
|
||||
Michael Birtwell
|
||||
Michael Droettboom
|
||||
@@ -244,6 +247,7 @@ Michael Krebs
|
||||
Michael Seifert
|
||||
Michal Wajszczuk
|
||||
Michał Zięba
|
||||
Mickey Pashov
|
||||
Mihai Capotă
|
||||
Mike Hoyle (hoylemd)
|
||||
Mike Lundy
|
||||
@@ -258,9 +262,9 @@ Niclas Olofsson
|
||||
Nicolas Delaby
|
||||
Nikolay Kondratyev
|
||||
Nipunn Koorapati
|
||||
Olga Matoula
|
||||
Oleg Pidsadnyi
|
||||
Oleg Sushchenko
|
||||
Olga Matoula
|
||||
Oliver Bestwalter
|
||||
Omar Kohl
|
||||
Omer Hadari
|
||||
@@ -276,6 +280,7 @@ Paweł Adamczak
|
||||
Pedro Algarvio
|
||||
Petter Strandmark
|
||||
Philipp Loose
|
||||
Pierre Sassoulas
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Piotr Helm
|
||||
@@ -286,8 +291,8 @@ Pulkit Goyal
|
||||
Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
Ralf Schmitt
|
||||
Ram Rachum
|
||||
Ralph Giles
|
||||
Ram Rachum
|
||||
Ran Benita
|
||||
Raphael Castaneda
|
||||
Raphael Pierzina
|
||||
@@ -372,6 +377,7 @@ Xixi Zhao
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Yoav Caspi
|
||||
Yusuke Kadowaki
|
||||
Yuval Shimon
|
||||
Zac Hatfield-Dodds
|
||||
Zachary Kneupper
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Update :class:`pytest.PytestUnhandledCoroutineWarning` to a deprecation; it will raise an error in pytest 8.
|
||||
@@ -1 +0,0 @@
|
||||
:data:`sys.stdin` now contains all expected methods of a file-like object when capture is enabled.
|
||||
@@ -1 +0,0 @@
|
||||
:class:`~pytest.PytestReturnNotNoneWarning` is now a subclass of :class:`~pytest.PytestRemovedIn8Warning`: the plan is to make returning non-``None`` from tests an error in the future.
|
||||
@@ -1,5 +0,0 @@
|
||||
``@pytest.mark.parametrize()`` (and similar functions) now accepts any ``Sequence[str]`` for the argument names,
|
||||
instead of just ``list[str]`` and ``tuple[str, ...]``.
|
||||
|
||||
(Note that ``str``, which is itself a ``Sequence[str]``, is still treated as a
|
||||
comma-delimited name list, as before).
|
||||
1
changelog/10226.improvement.rst
Normal file
1
changelog/10226.improvement.rst
Normal file
@@ -0,0 +1 @@
|
||||
If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||
@@ -1,3 +0,0 @@
|
||||
Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for
|
||||
type check and runtime purposes. Made `_pytest.doctest` use internal APIs
|
||||
to avoid circular imports.
|
||||
@@ -1 +0,0 @@
|
||||
Update information on writing plugins to use ``pyproject.toml`` instead of ``setup.py``.
|
||||
@@ -1 +0,0 @@
|
||||
The ``--no-showlocals`` flag has been added. This can be passed directly to tests to override ``--showlocals`` declared through ``addopts``.
|
||||
@@ -1 +0,0 @@
|
||||
Do not break into pdb when ``raise unittest.SkipTest()`` appears top-level in a file.
|
||||
@@ -1 +0,0 @@
|
||||
pytest no longer depends on the ``py`` library. ``pytest`` provides a vendored copy of ``py.error`` and ``py.path`` modules but will use the ``py`` library if it is installed. If you need other ``py.*`` modules, continue to install the deprecated ``py`` library separately, otherwise it can usually be removed as a dependency.
|
||||
1
changelog/10452.bugfix.rst
Normal file
1
changelog/10452.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
|
||||
1
changelog/10457.bugfix.rst
Normal file
1
changelog/10457.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
|
||||
1
changelog/10506.bugfix.rst
Normal file
1
changelog/10506.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
|
||||
1
changelog/10525.feature.rst
Normal file
1
changelog/10525.feature.rst
Normal file
@@ -0,0 +1 @@
|
||||
Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
|
||||
@@ -1 +0,0 @@
|
||||
Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expresed instead.
|
||||
@@ -1,4 +0,0 @@
|
||||
Deprecate configuring hook specs/impls using attributes/marks.
|
||||
|
||||
Instead use :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec`.
|
||||
For more details, see the :ref:`docs <legacy-path-hooks-deprecated>`.
|
||||
2
changelog/6267.improvement.rst
Normal file
2
changelog/6267.improvement.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
The full output of a test is no longer truncated if the truncation message would be longer than
|
||||
the hidden text. The line number shown has also been fixed.
|
||||
@@ -1 +0,0 @@
|
||||
A warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`.
|
||||
1
changelog/7431.feature.rst
Normal file
1
changelog/7431.feature.rst
Normal file
@@ -0,0 +1 @@
|
||||
``--log-disable`` CLI option added to disable individual loggers.
|
||||
@@ -1,5 +0,0 @@
|
||||
Marks are now inherited according to the full MRO in test classes. Previously, if a test class inherited from two or more classes, only marks from the first super-class would apply.
|
||||
|
||||
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
|
||||
|
||||
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`pytest.Node.iter_markers` instead.
|
||||
2
changelog/8141.feature.rst
Normal file
2
changelog/8141.feature.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
||||
The default behavior has changed to keep only directories for failed tests, equivalent to `tmp_path_retention_policy="failed"`.
|
||||
@@ -1,2 +0,0 @@
|
||||
Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
||||
enhance match comparison for :py:func:`_pytest._code.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
|
||||
@@ -1,2 +0,0 @@
|
||||
Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
|
||||
error. We now raise immediately with a more helpful message.
|
||||
@@ -1 +0,0 @@
|
||||
Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
|
||||
@@ -1 +0,0 @@
|
||||
The documentation is now built using Sphinx 5.x (up from 3.x previously).
|
||||
@@ -1 +0,0 @@
|
||||
Update documentation on how :func:`pytest.warns` affects :class:`DeprecationWarning`.
|
||||
@@ -1,3 +0,0 @@
|
||||
On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
||||
|
||||
:mod:`tomli`` is no longer a dependency on Python 3.11.
|
||||
@@ -1 +0,0 @@
|
||||
Display assertion message without escaped newline characters with ``-vv``.
|
||||
@@ -1 +0,0 @@
|
||||
Improved error message that is shown when no collector is found for a given file.
|
||||
@@ -1 +0,0 @@
|
||||
Some coloring has been added to the short test summary.
|
||||
@@ -1 +0,0 @@
|
||||
Ensure ``caplog.get_records(when)`` returns current/correct data after invoking ``caplog.clear()``.
|
||||
@@ -1 +0,0 @@
|
||||
Normalize the help description of all command-line options.
|
||||
@@ -1,10 +0,0 @@
|
||||
The functionality for running tests written for ``nose`` has been officially deprecated.
|
||||
|
||||
This includes:
|
||||
|
||||
* Plain ``setup`` and ``teardown`` functions and methods: this might catch users by surprise, as ``setup()`` and ``teardown()`` are not pytest idioms, but part of the ``nose`` support.
|
||||
* Setup/teardown using the `@with_setup <with-setup-nose>`_ decorator.
|
||||
|
||||
For more details, consult the :ref:`deprecation docs <nose-deprecation>`.
|
||||
|
||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||
@@ -1 +0,0 @@
|
||||
Added shell-style wildcard support to ``testpaths``.
|
||||
@@ -1 +0,0 @@
|
||||
Made ``_pytest.compat`` re-export ``importlib_metadata`` in the eyes of type checkers.
|
||||
@@ -1 +0,0 @@
|
||||
Fix default encoding warning (``EncodingWarning``) in ``cacheprovider``
|
||||
@@ -1 +0,0 @@
|
||||
Display full crash messages in ``short test summary info``, when runng in a CI environment.
|
||||
@@ -1,4 +0,0 @@
|
||||
Improve the error message when we attempt to access a fixture that has been
|
||||
torn down.
|
||||
Add an additional sentence to the docstring explaining when it's not a good
|
||||
idea to call getfixturevalue.
|
||||
@@ -1 +0,0 @@
|
||||
Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``.
|
||||
@@ -6,6 +6,7 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.2.0
|
||||
release-7.1.3
|
||||
release-7.1.2
|
||||
release-7.1.1
|
||||
|
||||
93
doc/en/announce/release-7.2.0.rst
Normal file
93
doc/en/announce/release-7.2.0.rst
Normal file
@@ -0,0 +1,93 @@
|
||||
pytest-7.2.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.2.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Aaron Berdy
|
||||
* Adam Turner
|
||||
* Albert Villanova del Moral
|
||||
* Alice Purcell
|
||||
* Anthony Sottile
|
||||
* Anton Yakutovich
|
||||
* Babak Keyvani
|
||||
* Brandon Chinn
|
||||
* Bruno Oliveira
|
||||
* Chanvin Xiao
|
||||
* Cheuk Ting Ho
|
||||
* Chris Wheeler
|
||||
* EmptyRabbit
|
||||
* Ezio Melotti
|
||||
* Florian Best
|
||||
* Florian Bruhin
|
||||
* Fredrik Berndtsson
|
||||
* Gabriel Landau
|
||||
* Gergely Kalmár
|
||||
* Hugo van Kemenade
|
||||
* James Gerity
|
||||
* John Litborn
|
||||
* Jon Parise
|
||||
* Kevin C
|
||||
* Kian Eliasi
|
||||
* MatthewFlamm
|
||||
* Miro Hrončok
|
||||
* Nate Meyvis
|
||||
* Neil Girdhar
|
||||
* Nhieuvu1802
|
||||
* Nipunn Koorapati
|
||||
* Ofek Lev
|
||||
* Paul Müller
|
||||
* Paul Reece
|
||||
* Pax
|
||||
* Pete Baughman
|
||||
* Peyman Salehi
|
||||
* Philipp A
|
||||
* Ran Benita
|
||||
* Robert O'Shea
|
||||
* Ronny Pfannschmidt
|
||||
* Rowin
|
||||
* Ruth Comer
|
||||
* Samuel Colvin
|
||||
* Samuel Gaist
|
||||
* Sandro Tosi
|
||||
* Shantanu
|
||||
* Simon K
|
||||
* Stephen Rosen
|
||||
* Sviatoslav Sydorenko
|
||||
* Tatiana Ovary
|
||||
* Thierry Moisan
|
||||
* Thomas Grainger
|
||||
* Tim Hoffmann
|
||||
* Tobias Diez
|
||||
* Tony Narlock
|
||||
* Vivaan Verma
|
||||
* Wolfremium
|
||||
* Zac Hatfield-Dodds
|
||||
* Zach OBrien
|
||||
* aizpurua23a
|
||||
* gresm
|
||||
* holesch
|
||||
* itxasos23
|
||||
* johnkangw
|
||||
* skhomuti
|
||||
* sommersoft
|
||||
* wodny
|
||||
* zx.qiu
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
Values can be any object handled by the json stdlib module.
|
||||
|
||||
capsys -- .../_pytest/capture.py:878
|
||||
capsys -- .../_pytest/capture.py:905
|
||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
@@ -51,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capsysbinary -- .../_pytest/capture.py:906
|
||||
capsysbinary -- .../_pytest/capture.py:933
|
||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsysbinary.readouterr()``
|
||||
@@ -69,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capfd -- .../_pytest/capture.py:934
|
||||
capfd -- .../_pytest/capture.py:961
|
||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -87,7 +87,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capfdbinary -- .../_pytest/capture.py:962
|
||||
capfdbinary -- .../_pytest/capture.py:989
|
||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfdbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:735
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1344
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1351
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -163,7 +163,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
record_testsuite_property("ARCH", "PPC")
|
||||
record_testsuite_property("STORAGE_TYPE", "CEPH")
|
||||
|
||||
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
|
||||
:param name:
|
||||
The property name.
|
||||
:param value:
|
||||
The property value. Will be converted to a string.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -193,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:487
|
||||
caplog -- .../_pytest/logging.py:491
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
@@ -228,16 +231,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
To undo modifications done by the fixture in a contained scope,
|
||||
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
||||
|
||||
recwarn -- .../_pytest/recwarn.py:29
|
||||
recwarn -- .../_pytest/recwarn.py:30
|
||||
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
||||
|
||||
See https://docs.python.org/library/how-to/capture-warnings.html for information
|
||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||
on warning categories.
|
||||
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:184
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:188
|
||||
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
||||
|
||||
tmp_path -- .../_pytest/tmpdir.py:199
|
||||
tmp_path -- .../_pytest/tmpdir.py:203
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
@@ -28,6 +28,149 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.2.0 (2022-10-23)
|
||||
=========================
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#10012 <https://github.com/pytest-dev/pytest/issues/10012>`_: Update :class:`pytest.PytestUnhandledCoroutineWarning` to a deprecation; it will raise an error in pytest 8.
|
||||
|
||||
|
||||
- `#10396 <https://github.com/pytest-dev/pytest/issues/10396>`_: pytest no longer depends on the ``py`` library. ``pytest`` provides a vendored copy of ``py.error`` and ``py.path`` modules but will use the ``py`` library if it is installed. If you need other ``py.*`` modules, continue to install the deprecated ``py`` library separately, otherwise it can usually be removed as a dependency.
|
||||
|
||||
|
||||
- `#4562 <https://github.com/pytest-dev/pytest/issues/4562>`_: Deprecate configuring hook specs/impls using attributes/marks.
|
||||
|
||||
Instead use :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec`.
|
||||
For more details, see the :ref:`docs <legacy-path-hooks-deprecated>`.
|
||||
|
||||
|
||||
- `#9886 <https://github.com/pytest-dev/pytest/issues/9886>`_: The functionality for running tests written for ``nose`` has been officially deprecated.
|
||||
|
||||
This includes:
|
||||
|
||||
* Plain ``setup`` and ``teardown`` functions and methods: this might catch users by surprise, as ``setup()`` and ``teardown()`` are not pytest idioms, but part of the ``nose`` support.
|
||||
* Setup/teardown using the `@with_setup <with-setup-nose>`_ decorator.
|
||||
|
||||
For more details, consult the :ref:`deprecation docs <nose-deprecation>`.
|
||||
|
||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||
|
||||
- `#7337 <https://github.com/pytest-dev/pytest/issues/7337>`_: A deprecation warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`. The plan is to make returning non-`None` from tests an error in the future.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#9897 <https://github.com/pytest-dev/pytest/issues/9897>`_: Added shell-style wildcard support to ``testpaths``.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10218 <https://github.com/pytest-dev/pytest/issues/10218>`_: ``@pytest.mark.parametrize()`` (and similar functions) now accepts any ``Sequence[str]`` for the argument names,
|
||||
instead of just ``list[str]`` and ``tuple[str, ...]``.
|
||||
|
||||
(Note that ``str``, which is itself a ``Sequence[str]``, is still treated as a
|
||||
comma-delimited name list, as before).
|
||||
|
||||
|
||||
- `#10381 <https://github.com/pytest-dev/pytest/issues/10381>`_: The ``--no-showlocals`` flag has been added. This can be passed directly to tests to override ``--showlocals`` declared through ``addopts``.
|
||||
|
||||
|
||||
- `#3426 <https://github.com/pytest-dev/pytest/issues/3426>`_: Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expressed instead.
|
||||
|
||||
|
||||
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
||||
enhance match comparison for :py:func:`_pytest._code.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
|
||||
|
||||
|
||||
- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
|
||||
error. We now raise immediately with a more helpful message.
|
||||
|
||||
|
||||
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
||||
|
||||
:mod:`tomli` is no longer a dependency on Python 3.11.
|
||||
|
||||
|
||||
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
|
||||
|
||||
|
||||
- `#9823 <https://github.com/pytest-dev/pytest/issues/9823>`_: Improved error message that is shown when no collector is found for a given file.
|
||||
|
||||
|
||||
- `#9873 <https://github.com/pytest-dev/pytest/issues/9873>`_: Some coloring has been added to the short test summary.
|
||||
|
||||
|
||||
- `#9883 <https://github.com/pytest-dev/pytest/issues/9883>`_: Normalize the help description of all command-line options.
|
||||
|
||||
|
||||
- `#9920 <https://github.com/pytest-dev/pytest/issues/9920>`_: Display full crash messages in ``short test summary info``, when running in a CI environment.
|
||||
|
||||
|
||||
- `#9987 <https://github.com/pytest-dev/pytest/issues/9987>`_: Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10150 <https://github.com/pytest-dev/pytest/issues/10150>`_: :data:`sys.stdin` now contains all expected methods of a file-like object when capture is enabled.
|
||||
|
||||
|
||||
- `#10382 <https://github.com/pytest-dev/pytest/issues/10382>`_: Do not break into pdb when ``raise unittest.SkipTest()`` appears top-level in a file.
|
||||
|
||||
|
||||
- `#7792 <https://github.com/pytest-dev/pytest/issues/7792>`_: Marks are now inherited according to the full MRO in test classes. Previously, if a test class inherited from two or more classes, only marks from the first super-class would apply.
|
||||
|
||||
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
|
||||
|
||||
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`pytest.Node.iter_markers` instead.
|
||||
|
||||
|
||||
- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
|
||||
|
||||
|
||||
- `#9877 <https://github.com/pytest-dev/pytest/issues/9877>`_: Ensure ``caplog.get_records(when)`` returns current/correct data after invoking ``caplog.clear()``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#10344 <https://github.com/pytest-dev/pytest/issues/10344>`_: Update information on writing plugins to use ``pyproject.toml`` instead of ``setup.py``.
|
||||
|
||||
|
||||
- `#9248 <https://github.com/pytest-dev/pytest/issues/9248>`_: The documentation is now built using Sphinx 5.x (up from 3.x previously).
|
||||
|
||||
|
||||
- `#9291 <https://github.com/pytest-dev/pytest/issues/9291>`_: Update documentation on how :func:`pytest.warns` affects :class:`DeprecationWarning`.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#10313 <https://github.com/pytest-dev/pytest/issues/10313>`_: Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for
|
||||
type check and runtime purposes. Made `_pytest.doctest` use internal APIs
|
||||
to avoid circular imports.
|
||||
|
||||
|
||||
- `#9906 <https://github.com/pytest-dev/pytest/issues/9906>`_: Made ``_pytest.compat`` re-export ``importlib_metadata`` in the eyes of type checkers.
|
||||
|
||||
|
||||
- `#9910 <https://github.com/pytest-dev/pytest/issues/9910>`_: Fix default encoding warning (``EncodingWarning``) in ``cacheprovider``
|
||||
|
||||
|
||||
- `#9984 <https://github.com/pytest-dev/pytest/issues/9984>`_: Improve the error message when we attempt to access a fixture that has been
|
||||
torn down.
|
||||
Add an additional sentence to the docstring explaining when it's not a good
|
||||
idea to call ``getfixturevalue``.
|
||||
|
||||
|
||||
pytest 7.1.3 (2022-08-31)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -246,9 +246,9 @@ You can ask which markers exist for your test suite - the list includes our just
|
||||
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures
|
||||
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.
|
||||
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.
|
||||
|
||||
|
||||
For an example on how to add and work with markers from a plugin, see
|
||||
@@ -438,9 +438,9 @@ The ``--markers`` option always gives you a list of available markers:
|
||||
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures
|
||||
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.
|
||||
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.
|
||||
|
||||
|
||||
.. _`passing callables to custom markers`:
|
||||
@@ -611,7 +611,7 @@ then you will see two tests skipped and two executed tests as expected:
|
||||
test_plat.py s.s. [100%]
|
||||
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [2] conftest.py:12: cannot run on platform linux
|
||||
SKIPPED [2] conftest.py:13: cannot run on platform linux
|
||||
======================= 2 passed, 2 skipped in 0.12s =======================
|
||||
|
||||
Note that if you specify a platform via the marker-command line option like this:
|
||||
|
||||
@@ -661,8 +661,7 @@ If we run this:
|
||||
|
||||
test_step.py:11: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
XFAIL test_step.py::TestUserHandling::test_deletion
|
||||
reason: previous test failed (test_modification)
|
||||
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
|
||||
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
|
||||
|
||||
We'll see that ``test_deletion`` was not executed because ``test_modification``
|
||||
@@ -896,6 +895,8 @@ here is a little example implemented via a local plugin:
|
||||
|
||||
import pytest
|
||||
|
||||
phase_report_key = StashKey[Dict[str, CollectReport]]()
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
@@ -903,10 +904,9 @@ here is a little example implemented via a local plugin:
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
|
||||
# set a report attribute for each phase of a call, which can
|
||||
# store test results for each phase of a call, which can
|
||||
# be "setup", "call", "teardown"
|
||||
|
||||
setattr(item, "rep_" + rep.when, rep)
|
||||
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -914,11 +914,11 @@ here is a little example implemented via a local plugin:
|
||||
yield
|
||||
# request.node is an "item" because we use the default
|
||||
# "function" scope
|
||||
if request.node.rep_setup.failed:
|
||||
print("setting up a test failed!", request.node.nodeid)
|
||||
elif request.node.rep_setup.passed:
|
||||
if request.node.rep_call.failed:
|
||||
print("executing test failed", request.node.nodeid)
|
||||
report = request.node.stash[phase_report_key]
|
||||
if report["setup"].failed:
|
||||
print("setting up a test failed or skipped", request.node.nodeid)
|
||||
elif ("call" not in report) or report["call"].failed:
|
||||
print("executing test failed or skipped", request.node.nodeid)
|
||||
|
||||
|
||||
if you then have failing tests:
|
||||
|
||||
@@ -50,8 +50,8 @@ Conventions for Python test discovery
|
||||
* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
|
||||
* From those files, collect test items:
|
||||
|
||||
* ``test`` prefixed test functions or methods outside of class
|
||||
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method)
|
||||
* ``test`` prefixed test functions or methods outside of class.
|
||||
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered.
|
||||
|
||||
For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
|
||||
|
||||
@@ -270,8 +270,8 @@ tox
|
||||
|
||||
Once you are done with your work and want to make sure that your actual
|
||||
package passes all tests you may want to look into :doc:`tox <tox:index>`, the
|
||||
virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`.
|
||||
tox helps you to setup virtualenv environments with pre-defined
|
||||
virtualenv test automation tool.
|
||||
``tox`` helps you to setup virtualenv environments with pre-defined
|
||||
dependencies and then executing a pre-configured test command with
|
||||
options. It will run tests against the installed package and not
|
||||
against your source code checkout, helping to detect packaging
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.1.3
|
||||
pytest 7.2.0
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ If you run this command for the first time, you can see the print statement:
|
||||
> assert mydata == 23
|
||||
E assert 42 == 23
|
||||
|
||||
test_caching.py:20: AssertionError
|
||||
test_caching.py:19: AssertionError
|
||||
-------------------------- Captured stdout setup ---------------------------
|
||||
running expensive computation...
|
||||
========================= short test summary info ==========================
|
||||
@@ -256,7 +256,7 @@ the cache and nothing will be printed:
|
||||
> assert mydata == 23
|
||||
E assert 42 == 23
|
||||
|
||||
test_caching.py:20: AssertionError
|
||||
test_caching.py:19: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_caching.py::test_function - assert 42 == 23
|
||||
1 failed in 0.12s
|
||||
|
||||
@@ -55,6 +55,13 @@ These options can also be customized through ``pytest.ini`` file:
|
||||
log_format = %(asctime)s %(levelname)s %(message)s
|
||||
log_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
Specific loggers can be disabled via ``--log-disable={logger_name}``.
|
||||
This argument can be passed multiple times:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --log-disable=main --log-disable=testing
|
||||
|
||||
Further it is possible to disable reporting of captured content (stdout,
|
||||
stderr and logs) on failed tests completely with:
|
||||
|
||||
|
||||
@@ -349,8 +349,7 @@ Example:
|
||||
test_example.py:14: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [1] test_example.py:22: skipping this test
|
||||
XFAIL test_example.py::test_xfail
|
||||
reason: xfailing this test
|
||||
XFAIL test_example.py::test_xfail - reason: xfailing this test
|
||||
XPASS test_example.py::test_xpass always xfail
|
||||
ERROR test_example.py::test_error - assert 0
|
||||
FAILED test_example.py::test_fail - assert 0
|
||||
|
||||
@@ -131,10 +131,13 @@ The default base temporary directory
|
||||
|
||||
Temporary directories are by default created as sub-directories of
|
||||
the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||
``NUM`` will be incremented with each test run. Moreover, entries older
|
||||
than 3 temporary directories will be removed.
|
||||
``NUM`` will be incremented with each test run.
|
||||
By default, only the directories of failed tests will be kept.
|
||||
Also only the last 3 directries will remain at most.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
|
||||
The number of entries currently cannot be changed, but using the ``--basetemp``
|
||||
Using the ``--basetemp``
|
||||
option will remove the directory before every run, effectively meaning the temporary directories
|
||||
of only the most recent run will be kept.
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ the ``self.db`` values in the traceback:
|
||||
E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
|
||||
E assert 0
|
||||
|
||||
test_unittest_db.py:10: AssertionError
|
||||
test_unittest_db.py:11: AssertionError
|
||||
___________________________ MyTest.test_method2 ____________________________
|
||||
|
||||
self = <test_unittest_db.MyTest testMethod=test_method2>
|
||||
@@ -167,7 +167,7 @@ the ``self.db`` values in the traceback:
|
||||
E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
|
||||
E assert 0
|
||||
|
||||
test_unittest_db.py:13: AssertionError
|
||||
test_unittest_db.py:14: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft...
|
||||
FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft...
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- Professionelles Testen für Python mit pytest, part of `enterPy <https://www.enterpy.de/>`__ (German), `October 28th <https://www.enterpy.de/veranstaltung-15409-se-0-professionelles-testen-fuer-python-mit-pytest.html>`__ (sold out) and `November 4th <https://www.enterpy.de/veranstaltung-15557-se-0-professionelles-testen-fuer-python-mit-pytest-zusatztermin.html>`__, online
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1723,6 +1723,40 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
directories when executing from the root directory.
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_count
|
||||
|
||||
|
||||
|
||||
How many sessions should we keep the `tmp_path` directories,
|
||||
according to `tmp_path_retention_policy`.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
tmp_path_retention_count = 3
|
||||
|
||||
Default: 3
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_policy
|
||||
|
||||
|
||||
|
||||
Controls which directories created by the `tmp_path` fixture are kept around,
|
||||
based on test outcome.
|
||||
|
||||
* `all`: retains directories for all tests, regardless of the outcome.
|
||||
* `failed`: retains directories only for tests with outcome `error` or `failed`.
|
||||
* `none`: directories are always removed after each test ends, regardless of the outcome.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
tmp_path_retention_policy = "all"
|
||||
|
||||
Default: failed
|
||||
|
||||
|
||||
.. confval:: usefixtures
|
||||
|
||||
List of fixtures that will be applied to all test functions; this is semantically the same to apply
|
||||
@@ -1759,12 +1793,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
$ pytest --help
|
||||
usage: pytest [options] [file_or_dir] [file_or_dir] [...]
|
||||
|
||||
Positional arguments:
|
||||
positional arguments:
|
||||
file_or_dir
|
||||
|
||||
General:
|
||||
general:
|
||||
-k EXPRESSION Only run tests which match the given substring
|
||||
expression. An expression is a python evaluatable
|
||||
expression. An expression is a Python evaluatable
|
||||
expression where all names are substring-matched
|
||||
against test names and their parent classes.
|
||||
Example: -k 'test_method or test_other' matches all
|
||||
@@ -1778,9 +1812,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
'extra_keyword_matches' set, as well as functions
|
||||
which have names assigned directly to them. The
|
||||
matching is case-insensitive.
|
||||
-m MARKEXPR Only run tests matching given mark expression.
|
||||
For example: -m 'mark1 and not mark2'.
|
||||
--markers Show markers (builtin, plugin and per-project ones)
|
||||
-m MARKEXPR Only run tests matching given mark expression. For
|
||||
example: -m 'mark1 and not mark2'.
|
||||
--markers show markers (builtin, plugin and per-project ones).
|
||||
-x, --exitfirst Exit instantly on first error or failed test
|
||||
--fixtures, --funcargs
|
||||
Show available fixtures, sorted by plugin appearance
|
||||
@@ -1790,18 +1824,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
KeyboardInterrupt
|
||||
--pdbcls=modulename:classname
|
||||
Specify a custom interactive Python debugger for use
|
||||
with --pdb. For example:
|
||||
with --pdb.For example:
|
||||
--pdbcls=IPython.terminal.debugger:TerminalPdb
|
||||
--trace Immediately break when running each test
|
||||
--capture=method Per-test capturing method: one of fd|sys|no|tee-sys.
|
||||
-s Shortcut for --capture=no.
|
||||
--capture=method Per-test capturing method: one of fd|sys|no|tee-sys
|
||||
-s Shortcut for --capture=no
|
||||
--runxfail Report the results of xfail tests as if they were
|
||||
not marked
|
||||
--lf, --last-failed Rerun only the tests that failed at the last run (or
|
||||
all if none failed)
|
||||
--ff, --failed-first Run all tests, but run the last failures first
|
||||
This may re-order tests and thus lead to repeated
|
||||
fixture setup/teardown
|
||||
--ff, --failed-first Run all tests, but run the last failures first. This
|
||||
may re-order tests and thus lead to repeated fixture
|
||||
setup/teardown.
|
||||
--nf, --new-first Run tests from new files first, then the rest of the
|
||||
tests sorted by file mtime
|
||||
--cache-show=[CACHESHOW]
|
||||
@@ -1815,11 +1849,10 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
test next time
|
||||
--sw-skip, --stepwise-skip
|
||||
Ignore the first failing test but stop on the next
|
||||
failing test.
|
||||
implicitly enables --stepwise.
|
||||
failing test. Implicitly enables --stepwise.
|
||||
|
||||
Reporting:
|
||||
--durations=N show N slowest setup/test durations (N=0 for all)
|
||||
--durations=N Show N slowest setup/test durations (N=0 for all)
|
||||
--durations-min=N Minimal duration in seconds for inclusion in slowest
|
||||
list. Default: 0.005.
|
||||
-v, --verbose Increase verbosity
|
||||
@@ -1836,8 +1869,10 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--disable-warnings, --disable-pytest-warnings
|
||||
Disable warnings summary
|
||||
-l, --showlocals Show locals in tracebacks (disabled by default)
|
||||
--no-showlocals Hide locals in tracebacks (negate --showlocals
|
||||
passed through addopts)
|
||||
--tb=style Traceback print mode
|
||||
(auto/long/short/line/native/no).
|
||||
(auto/long/short/line/native/no)
|
||||
--show-capture={no,stdout,stderr,log,all}
|
||||
Controls how captured stdout/stderr/log is shown on
|
||||
failed tests. Default: all.
|
||||
@@ -1863,15 +1898,14 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
-c file Load configuration from `file` instead of trying to
|
||||
locate one of the implicit configuration files
|
||||
--continue-on-collection-errors
|
||||
Force test execution even if collection errors
|
||||
occur
|
||||
Force test execution even if collection errors occur
|
||||
--rootdir=ROOTDIR Define root directory for tests. Can be relative
|
||||
path: 'root_dir', './root_dir',
|
||||
'root_dir/another_dir/'; absolute path:
|
||||
'/home/user/root_dir'; path with variables:
|
||||
'$HOME/root_dir'.
|
||||
|
||||
Collection:
|
||||
collection:
|
||||
--collect-only, --co Only collect tests, don't execute them
|
||||
--pyargs Try to interpret all arguments as Python packages
|
||||
--ignore=path Ignore path during collection (multi-allowed)
|
||||
@@ -1899,27 +1933,24 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
For a given doctest, continue to run after the first
|
||||
failure
|
||||
|
||||
Test session debugging and configuration:
|
||||
--basetemp=dir Base temporary directory for this test run. (Warning:
|
||||
this directory is removed if it exists.)
|
||||
test session debugging and configuration:
|
||||
--basetemp=dir Base temporary directory for this test run.
|
||||
(Warning: this directory is removed if it exists.)
|
||||
-V, --version Display pytest version and information about
|
||||
plugins. When given twice, also display information
|
||||
about plugins.
|
||||
-h, --help Show help message and configuration info
|
||||
-p name Early-load given plugin module name or entry point
|
||||
(multi-allowed)
|
||||
To avoid loading of plugins, use the `no:` prefix,
|
||||
e.g. `no:doctest`
|
||||
(multi-allowed). To avoid loading of plugins, use
|
||||
the `no:` prefix, e.g. `no:doctest`.
|
||||
--trace-config Trace considerations of conftest.py files
|
||||
--debug=[DEBUG_FILE_NAME]
|
||||
Store internal tracing debug information in this log
|
||||
file.
|
||||
This file is opened with 'w' and truncated as a
|
||||
result, care advised.
|
||||
Default: pytestdebug.log.
|
||||
file. This file is opened with 'w' and truncated as
|
||||
a result, care advised. Default: pytestdebug.log.
|
||||
-o OVERRIDE_INI, --override-ini=OVERRIDE_INI
|
||||
Override ini option with "option=value" style, e.g.
|
||||
`-o xfail_strict=True -o cache_dir=cache`
|
||||
`-o xfail_strict=True -o cache_dir=cache`.
|
||||
--assert=MODE Control assertion debugging tools.
|
||||
'plain' performs no assertion debugging.
|
||||
'rewrite' (the default) rewrites assert statements
|
||||
@@ -1930,11 +1961,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--setup-plan Show what fixtures and tests would be executed but
|
||||
don't execute anything
|
||||
|
||||
Logging:
|
||||
--log-level=LEVEL Level of messages to catch/display.
|
||||
Not set by default, so it depends on the root/parent
|
||||
log handler's effective level, where it is "WARNING"
|
||||
by default.
|
||||
logging:
|
||||
--log-level=LEVEL Level of messages to catch/display. Not set by
|
||||
default, so it depends on the root/parent log
|
||||
handler's effective level, where it is "WARNING" by
|
||||
default.
|
||||
--log-format=LOG_FORMAT
|
||||
Log format used by the logging module
|
||||
--log-date-format=LOG_DATE_FORMAT
|
||||
@@ -1963,7 +1994,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
Default marker for empty parametersets
|
||||
norecursedirs (args): Directory patterns to avoid for recursion
|
||||
testpaths (args): Directories to search for tests when no files or
|
||||
directories are given in the command line
|
||||
directories are given on the command line
|
||||
filterwarnings (linelist):
|
||||
Each line specifies a pattern for
|
||||
warnings.filterwarnings. Processed after
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
pallets-sphinx-themes
|
||||
pluggy>=1.0
|
||||
pygments-pytest>=2.2.0
|
||||
pygments-pytest>=2.3.0
|
||||
sphinx-removed-in>=0.2.0
|
||||
sphinx>=5,<6
|
||||
sphinxcontrib-trio
|
||||
sphinxcontrib-svg2pdfconverter
|
||||
# Pin packaging because it no longer handles 'latest' version, which
|
||||
# is the version that is assigned to the docs.
|
||||
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
|
||||
packaging <22
|
||||
|
||||
@@ -78,7 +78,7 @@ def iter_plugins():
|
||||
requires = "N/A"
|
||||
if info["requires_dist"]:
|
||||
for requirement in info["requires_dist"]:
|
||||
if requirement == "pytest" or "pytest " in requirement:
|
||||
if re.match(r"pytest(?![-.\w])", requirement):
|
||||
requires = requirement
|
||||
break
|
||||
releases = response.json()["releases"]
|
||||
@@ -90,7 +90,9 @@ def iter_plugins():
|
||||
last_release = release_date.strftime("%b %d, %Y")
|
||||
break
|
||||
name = f':pypi:`{info["name"]}`'
|
||||
summary = escape_rst(info["summary"].replace("\n", ""))
|
||||
summary = ""
|
||||
if info["summary"]:
|
||||
summary = escape_rst(info["summary"].replace("\n", ""))
|
||||
yield {
|
||||
"name": name,
|
||||
"summary": summary.strip(),
|
||||
|
||||
@@ -21,6 +21,7 @@ classifiers =
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Utilities
|
||||
@@ -95,7 +96,6 @@ mypy_path = src
|
||||
check_untyped_defs = True
|
||||
disallow_any_generics = True
|
||||
ignore_missing_imports = True
|
||||
no_implicit_optional = True
|
||||
show_error_codes = True
|
||||
strict_equality = True
|
||||
warn_redundant_casts = True
|
||||
|
||||
@@ -78,15 +78,15 @@ class FastFilesCompleter:
|
||||
|
||||
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
||||
# Only called on non option completions.
|
||||
if os.path.sep in prefix[1:]:
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
||||
if os.sep in prefix[1:]:
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.sep)
|
||||
else:
|
||||
prefix_dir = 0
|
||||
completion = []
|
||||
globbed = []
|
||||
if "*" not in prefix and "?" not in prefix:
|
||||
# We are on unix, otherwise no bash.
|
||||
if not prefix or prefix[-1] == os.path.sep:
|
||||
if not prefix or prefix[-1] == os.sep:
|
||||
globbed.extend(glob(prefix + ".*"))
|
||||
prefix += "*"
|
||||
globbed.extend(glob(prefix))
|
||||
|
||||
@@ -24,6 +24,7 @@ from stat import S_ISLNK
|
||||
from stat import S_ISREG
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import overload
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -146,7 +147,7 @@ class Visitor:
|
||||
self.fil = fil
|
||||
self.ignore = ignore
|
||||
self.breadthfirst = bf
|
||||
self.optsort = sort and sorted or (lambda x: x)
|
||||
self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
|
||||
|
||||
def gen(self, path):
|
||||
try:
|
||||
@@ -224,7 +225,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import pwd
|
||||
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid)
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
|
||||
return entry[0]
|
||||
|
||||
@property
|
||||
@@ -234,7 +235,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import grp
|
||||
|
||||
entry = error.checked_call(grp.getgrgid, self.gid)
|
||||
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
|
||||
return entry[0]
|
||||
|
||||
def isdir(self):
|
||||
@@ -252,7 +253,7 @@ def getuserid(user):
|
||||
import pwd
|
||||
|
||||
if not isinstance(user, int):
|
||||
user = pwd.getpwnam(user)[2]
|
||||
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
|
||||
return user
|
||||
|
||||
|
||||
@@ -260,7 +261,7 @@ def getgroupid(group):
|
||||
import grp
|
||||
|
||||
if not isinstance(group, int):
|
||||
group = grp.getgrnam(group)[2]
|
||||
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
|
||||
return group
|
||||
|
||||
|
||||
@@ -795,7 +796,7 @@ class LocalPath:
|
||||
kw = {"exists": 1}
|
||||
return Checkers(self)._evaluate(kw)
|
||||
|
||||
_patternchars = set("*?[" + os.path.sep)
|
||||
_patternchars = set("*?[" + os.sep)
|
||||
|
||||
def listdir(self, fil=None, sort=None):
|
||||
"""List directory contents, possibly filter by the given fil func
|
||||
@@ -1127,7 +1128,7 @@ class LocalPath:
|
||||
modfile = modfile[:-1]
|
||||
elif modfile.endswith("$py.class"):
|
||||
modfile = modfile[:-9] + ".py"
|
||||
if modfile.endswith(os.path.sep + "__init__.py"):
|
||||
if modfile.endswith(os.sep + "__init__.py"):
|
||||
if self.basename != "__init__.py":
|
||||
modfile = modfile[:-12]
|
||||
try:
|
||||
|
||||
@@ -180,7 +180,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
for initial_path in self.session._initialpaths:
|
||||
# Make something as c:/projects/my_project/path.py ->
|
||||
# ['c:', 'projects', 'my_project', 'path.py']
|
||||
parts = str(initial_path).split(os.path.sep)
|
||||
parts = str(initial_path).split(os.sep)
|
||||
# add 'path' to basenames to be checked.
|
||||
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
|
||||
|
||||
@@ -275,7 +275,12 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
|
||||
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
|
||||
if sys.version_info >= (3, 12):
|
||||
from importlib.resources.abc import TraversableResources
|
||||
else:
|
||||
from importlib.abc import TraversableResources
|
||||
|
||||
def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
|
||||
if sys.version_info < (3, 11):
|
||||
from importlib.readers import FileReader
|
||||
else:
|
||||
|
||||
@@ -38,9 +38,9 @@ def _truncate_explanation(
|
||||
"""Truncate given list of strings that makes up the assertion explanation.
|
||||
|
||||
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
||||
first. The remaining lines will be replaced by a usage message.
|
||||
first, taking the truncation explanation into account. The remaining lines
|
||||
will be replaced by a usage message.
|
||||
"""
|
||||
|
||||
if max_lines is None:
|
||||
max_lines = DEFAULT_MAX_LINES
|
||||
if max_chars is None:
|
||||
@@ -48,35 +48,56 @@ def _truncate_explanation(
|
||||
|
||||
# Check if truncation required
|
||||
input_char_count = len("".join(input_lines))
|
||||
if len(input_lines) <= max_lines and input_char_count <= max_chars:
|
||||
# The length of the truncation explanation depends on the number of lines
|
||||
# removed but is at least 68 characters:
|
||||
# The real value is
|
||||
# 64 (for the base message:
|
||||
# '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
|
||||
# )
|
||||
# + 1 (for plural)
|
||||
# + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
|
||||
# + 3 for the '...' added to the truncated line
|
||||
# But if there's more than 100 lines it's very likely that we're going to
|
||||
# truncate, so we don't need the exact value using log10.
|
||||
tolerable_max_chars = (
|
||||
max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
|
||||
)
|
||||
# The truncation explanation add two lines to the output
|
||||
tolerable_max_lines = max_lines + 2
|
||||
if (
|
||||
len(input_lines) <= tolerable_max_lines
|
||||
and input_char_count <= tolerable_max_chars
|
||||
):
|
||||
return input_lines
|
||||
|
||||
# Truncate first to max_lines, and then truncate to max_chars if max_chars
|
||||
# is exceeded.
|
||||
# Truncate first to max_lines, and then truncate to max_chars if necessary
|
||||
truncated_explanation = input_lines[:max_lines]
|
||||
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
|
||||
|
||||
# Add ellipsis to final line
|
||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
||||
|
||||
# Append useful message to explanation
|
||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||
truncated_line_count += 1 # Account for the part-truncated final line
|
||||
msg = "...Full output truncated"
|
||||
if truncated_line_count == 1:
|
||||
msg += f" ({truncated_line_count} line hidden)"
|
||||
truncated_char = True
|
||||
# We reevaluate the need to truncate chars following removal of some lines
|
||||
if len("".join(truncated_explanation)) > tolerable_max_chars:
|
||||
truncated_explanation = _truncate_by_char_count(
|
||||
truncated_explanation, max_chars
|
||||
)
|
||||
else:
|
||||
msg += f" ({truncated_line_count} lines hidden)"
|
||||
msg += f", {USAGE_MSG}"
|
||||
truncated_explanation.extend(["", str(msg)])
|
||||
return truncated_explanation
|
||||
truncated_char = False
|
||||
|
||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||
if truncated_explanation[-1]:
|
||||
# Add ellipsis and take into account part-truncated final line
|
||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
||||
if truncated_char:
|
||||
# It's possible that we did not remove any char from this line
|
||||
truncated_line_count += 1
|
||||
else:
|
||||
# Add proper ellipsis when we were able to fit a full line exactly
|
||||
truncated_explanation[-1] = "..."
|
||||
return truncated_explanation + [
|
||||
"",
|
||||
f"...Full output truncated ({truncated_line_count} line"
|
||||
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
||||
]
|
||||
|
||||
|
||||
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
|
||||
# Check if truncation required
|
||||
if len("".join(input_lines)) <= max_chars:
|
||||
return input_lines
|
||||
|
||||
# Find point at which input length exceeds total allowed length
|
||||
iterated_char_count = 0
|
||||
for iterated_index, input_line in enumerate(input_lines):
|
||||
|
||||
@@ -203,8 +203,7 @@ def determine_setup(
|
||||
else:
|
||||
cwd = Path.cwd()
|
||||
rootdir = get_common_ancestor([cwd, ancestor])
|
||||
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
|
||||
if is_fs_root:
|
||||
if is_fs_root(rootdir):
|
||||
rootdir = ancestor
|
||||
if rootdir_cmd_arg:
|
||||
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
|
||||
@@ -216,3 +215,11 @@ def determine_setup(
|
||||
)
|
||||
assert rootdir is not None
|
||||
return rootdir, inipath, inicfg or {}
|
||||
|
||||
|
||||
def is_fs_root(p: Path) -> bool:
|
||||
r"""
|
||||
Return True if the given path is pointing to the root of the
|
||||
file system ("/" on Unix and "C:\\" on Windows for example).
|
||||
"""
|
||||
return os.path.splitdrive(str(p))[1] == os.sep
|
||||
|
||||
@@ -58,6 +58,7 @@ from _pytest.mark import Mark
|
||||
from _pytest.mark import ParameterSet
|
||||
from _pytest.mark.structures import MarkDecorator
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
@@ -1129,6 +1130,10 @@ def pytest_fixture_setup(
|
||||
except TEST_OUTCOME:
|
||||
exc_info = sys.exc_info()
|
||||
assert exc_info[0] is not None
|
||||
if isinstance(
|
||||
exc_info[1], skip.Exception
|
||||
) and not fixturefunc.__name__.startswith("xunit_setup"):
|
||||
exc_info[1]._use_item_location = True # type: ignore[attr-defined]
|
||||
fixturedef.cached_result = (None, my_cache_key, exc_info)
|
||||
raise
|
||||
fixturedef.cached_result = (result, my_cache_key, None)
|
||||
|
||||
@@ -738,7 +738,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_report_header(
|
||||
def pytest_report_header( # type:ignore[empty-body]
|
||||
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
|
||||
) -> Union[str, List[str]]:
|
||||
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
||||
@@ -767,7 +767,7 @@ def pytest_report_header(
|
||||
"""
|
||||
|
||||
|
||||
def pytest_report_collectionfinish(
|
||||
def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
config: "Config",
|
||||
start_path: Path,
|
||||
startdir: "LEGACY_PATH",
|
||||
@@ -800,7 +800,7 @@ def pytest_report_collectionfinish(
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_report_teststatus(
|
||||
def pytest_report_teststatus( # type:ignore[empty-body]
|
||||
report: Union["CollectReport", "TestReport"], config: "Config"
|
||||
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
|
||||
"""Return result-category, shortletter and verbose word for status
|
||||
@@ -880,7 +880,9 @@ def pytest_warning_recorded(
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
|
||||
def pytest_markeval_namespace( # type:ignore[empty-body]
|
||||
config: "Config",
|
||||
) -> Dict[str, Any]:
|
||||
"""Called when constructing the globals dictionary used for
|
||||
evaluating string conditions in xfail/skipif markers.
|
||||
|
||||
|
||||
@@ -297,6 +297,13 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
default=None,
|
||||
help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
|
||||
)
|
||||
group.addoption(
|
||||
"--log-disable",
|
||||
action="append",
|
||||
default=[],
|
||||
dest="logger_disable",
|
||||
help="Disable a logger by name. Can be passed multipe times.",
|
||||
)
|
||||
|
||||
|
||||
_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
|
||||
@@ -594,6 +601,15 @@ class LoggingPlugin:
|
||||
get_option_ini(config, "log_auto_indent"),
|
||||
)
|
||||
self.log_cli_handler.setFormatter(log_cli_formatter)
|
||||
self._disable_loggers(loggers_to_disable=config.option.logger_disable)
|
||||
|
||||
def _disable_loggers(self, loggers_to_disable: List[str]) -> None:
|
||||
if not loggers_to_disable:
|
||||
return
|
||||
|
||||
for name in loggers_to_disable:
|
||||
logger = logging.getLogger(name)
|
||||
logger.disabled = True
|
||||
|
||||
def _create_formatter(self, log_format, log_date_format, auto_indent):
|
||||
# Color option doesn't exist if terminal plugin is disabled.
|
||||
|
||||
@@ -335,15 +335,26 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||
yield path
|
||||
|
||||
|
||||
def cleanup_dead_symlink(root: Path):
|
||||
for left_dir in root.iterdir():
|
||||
if left_dir.is_symlink():
|
||||
if not left_dir.resolve().exists():
|
||||
left_dir.unlink()
|
||||
|
||||
|
||||
def cleanup_numbered_dir(
|
||||
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
|
||||
) -> None:
|
||||
"""Cleanup for lock driven numbered directories."""
|
||||
if not root.exists():
|
||||
return
|
||||
for path in cleanup_candidates(root, prefix, keep):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
for path in root.glob("garbage-*"):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
|
||||
cleanup_dead_symlink(root)
|
||||
|
||||
|
||||
def make_numbered_dir_with_cleanup(
|
||||
root: Path,
|
||||
@@ -357,8 +368,10 @@ def make_numbered_dir_with_cleanup(
|
||||
for i in range(10):
|
||||
try:
|
||||
p = make_numbered_dir(root, prefix, mode)
|
||||
lock_path = create_cleanup_lock(p)
|
||||
register_cleanup_lock_removal(lock_path)
|
||||
# Only lock the current dir when keep is not 0
|
||||
if keep != 0:
|
||||
lock_path = create_cleanup_lock(p)
|
||||
register_cleanup_lock_removal(lock_path)
|
||||
except Exception as exc:
|
||||
e = exc
|
||||
else:
|
||||
@@ -544,8 +557,8 @@ def import_path(
|
||||
|
||||
if module_file.endswith((".pyc", ".pyo")):
|
||||
module_file = module_file[:-1]
|
||||
if module_file.endswith(os.path.sep + "__init__.py"):
|
||||
module_file = module_file[: -(len(os.path.sep + "__init__.py"))]
|
||||
if module_file.endswith(os.sep + "__init__.py"):
|
||||
module_file = module_file[: -(len(os.sep + "__init__.py"))]
|
||||
|
||||
try:
|
||||
is_same = _is_same(str(path), module_file)
|
||||
|
||||
@@ -403,8 +403,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
|
||||
def istestfunction(self, obj: object, name: str) -> bool:
|
||||
if self.funcnamefilter(name) or self.isnosetest(obj):
|
||||
if isinstance(obj, staticmethod):
|
||||
# staticmethods need to be unwrapped.
|
||||
if isinstance(obj, (staticmethod, classmethod)):
|
||||
# staticmethods and classmethods need to be unwrapped.
|
||||
obj = safe_getattr(obj, "__func__", False)
|
||||
return callable(obj) and fixtures.getfixturemarker(obj) is None
|
||||
else:
|
||||
|
||||
@@ -801,7 +801,7 @@ def raises( # noqa: F811
|
||||
r"""Assert that a code block/function call raises an exception.
|
||||
|
||||
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
|
||||
The excpected exception type, or a tuple if one of multiple possible
|
||||
The expected exception type, or a tuple if one of multiple possible
|
||||
exception types are excepted.
|
||||
:kwparam str | typing.Pattern[str] | None match:
|
||||
If specified, a string containing a regular expression,
|
||||
|
||||
@@ -35,6 +35,9 @@ from _pytest.outcomes import OutcomeException
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
@@ -512,22 +515,29 @@ class SetupState:
|
||||
stack is torn down.
|
||||
"""
|
||||
needed_collectors = nextitem and nextitem.listchain() or []
|
||||
exc = None
|
||||
exceptions: List[BaseException] = []
|
||||
while self.stack:
|
||||
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
|
||||
break
|
||||
node, (finalizers, _) = self.stack.popitem()
|
||||
these_exceptions = []
|
||||
while finalizers:
|
||||
fin = finalizers.pop()
|
||||
try:
|
||||
fin()
|
||||
except TEST_OUTCOME as e:
|
||||
# XXX Only first exception will be seen by user,
|
||||
# ideally all should be reported.
|
||||
if exc is None:
|
||||
exc = e
|
||||
if exc:
|
||||
raise exc
|
||||
these_exceptions.append(e)
|
||||
|
||||
if len(these_exceptions) == 1:
|
||||
exceptions.extend(these_exceptions)
|
||||
elif these_exceptions:
|
||||
msg = f"errors while tearing down {node!r}"
|
||||
exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1]))
|
||||
|
||||
if len(exceptions) == 1:
|
||||
raise exceptions[0]
|
||||
elif exceptions:
|
||||
raise BaseExceptionGroup("errors during test teardown", exceptions[::-1])
|
||||
if nextitem is None:
|
||||
assert not self.stack
|
||||
|
||||
|
||||
@@ -4,21 +4,42 @@ import re
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
RetentionType = Literal["all", "failed", "none"]
|
||||
|
||||
|
||||
import attr
|
||||
from _pytest.config.argparsing import Parser
|
||||
|
||||
from .pathlib import LOCK_TIMEOUT
|
||||
from .pathlib import make_numbered_dir
|
||||
from .pathlib import make_numbered_dir_with_cleanup
|
||||
from .pathlib import rm_rf
|
||||
from .pathlib import cleanup_dead_symlink
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
tmppath_result_key = StashKey[Dict[str, bool]]()
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(init=False)
|
||||
@@ -31,10 +52,14 @@ class TempPathFactory:
|
||||
_given_basetemp = attr.ib(type=Optional[Path])
|
||||
_trace = attr.ib()
|
||||
_basetemp = attr.ib(type=Optional[Path])
|
||||
_retention_count = attr.ib(type=int)
|
||||
_retention_policy = attr.ib(type="RetentionType")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
given_basetemp: Optional[Path],
|
||||
retention_count: int,
|
||||
retention_policy: "RetentionType",
|
||||
trace,
|
||||
basetemp: Optional[Path] = None,
|
||||
*,
|
||||
@@ -49,6 +74,8 @@ class TempPathFactory:
|
||||
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
|
||||
self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
|
||||
self._trace = trace
|
||||
self._retention_count = retention_count
|
||||
self._retention_policy = retention_policy
|
||||
self._basetemp = basetemp
|
||||
|
||||
@classmethod
|
||||
@@ -63,9 +90,23 @@ class TempPathFactory:
|
||||
:meta private:
|
||||
"""
|
||||
check_ispytest(_ispytest)
|
||||
count = int(config.getini("tmp_path_retention_count"))
|
||||
if count < 0:
|
||||
raise ValueError(
|
||||
f"tmp_path_retention_count must be >= 0. Current input: {count}."
|
||||
)
|
||||
|
||||
policy = config.getini("tmp_path_retention_policy")
|
||||
if policy not in ("all", "failed", "none"):
|
||||
raise ValueError(
|
||||
f"tmp_path_retention_policy must be either all, failed, none. Current intput: {policy}."
|
||||
)
|
||||
|
||||
return cls(
|
||||
given_basetemp=config.option.basetemp,
|
||||
trace=config.trace.get("tmpdir"),
|
||||
retention_count=count,
|
||||
retention_policy=policy,
|
||||
_ispytest=True,
|
||||
)
|
||||
|
||||
@@ -146,10 +187,13 @@ class TempPathFactory:
|
||||
)
|
||||
if (rootdir_stat.st_mode & 0o077) != 0:
|
||||
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
|
||||
keep = self._retention_count
|
||||
if self._retention_policy == "none":
|
||||
keep = 0
|
||||
basetemp = make_numbered_dir_with_cleanup(
|
||||
prefix="pytest-",
|
||||
root=rootdir,
|
||||
keep=3,
|
||||
keep=keep,
|
||||
lock_timeout=LOCK_TIMEOUT,
|
||||
mode=0o700,
|
||||
)
|
||||
@@ -184,6 +228,21 @@ def pytest_configure(config: Config) -> None:
|
||||
mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
parser.addini(
|
||||
"tmp_path_retention_count",
|
||||
help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.",
|
||||
default=3,
|
||||
)
|
||||
|
||||
parser.addini(
|
||||
"tmp_path_retention_policy",
|
||||
help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. "
|
||||
"(all/failed/none)",
|
||||
default="failed",
|
||||
)
|
||||
|
||||
|
||||
@fixture(scope="session")
|
||||
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
||||
"""Return a :class:`pytest.TempPathFactory` instance for the test session."""
|
||||
@@ -200,17 +259,69 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
|
||||
|
||||
|
||||
@fixture
|
||||
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
|
||||
def tmp_path(
|
||||
request: FixtureRequest, tmp_path_factory: TempPathFactory
|
||||
) -> Generator[Path, None, None]:
|
||||
"""Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
and only the base of failed session is kept. Also it only keeps the last 3 bases
|
||||
at most. This can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
"""
|
||||
|
||||
return _mk_tmp(request, tmp_path_factory)
|
||||
path = _mk_tmp(request, tmp_path_factory)
|
||||
yield path
|
||||
|
||||
# Remove the tmpdir if the policy is "failed" and the test passed.
|
||||
tmp_path_factory: TempPathFactory = request.session.config._tmp_path_factory # type: ignore
|
||||
policy = tmp_path_factory._retention_policy
|
||||
result_dict = request.node.stash[tmppath_result_key]
|
||||
|
||||
if policy == "failed" and result_dict.get("call", True):
|
||||
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||
# permissions, etc, in which case we ignore it.
|
||||
rmtree(path, ignore_errors=True)
|
||||
|
||||
del request.node.stash[tmppath_result_key]
|
||||
|
||||
# remove dead symlink
|
||||
basetemp = tmp_path_factory._basetemp
|
||||
if basetemp is None:
|
||||
return
|
||||
cleanup_dead_symlink(basetemp)
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
||||
"""After each session, remove base directory if all the tests passed,
|
||||
the policy is "failed", and the basetemp is not specified by a user.
|
||||
"""
|
||||
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
||||
if tmp_path_factory._basetemp is None:
|
||||
return
|
||||
policy = tmp_path_factory._retention_policy
|
||||
if (
|
||||
exitstatus == 0
|
||||
and policy == "failed"
|
||||
and tmp_path_factory._given_basetemp is None
|
||||
):
|
||||
passed_dir = tmp_path_factory._basetemp
|
||||
if passed_dir.exists():
|
||||
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||
# permissions, etc, in which case we ignore it.
|
||||
rmtree(passed_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item: Item, call):
|
||||
outcome = yield
|
||||
result: CollectReport = outcome.get_result()
|
||||
|
||||
empty: Dict[str, bool] = {}
|
||||
item.stash.setdefault(tmppath_result_key, empty)[result.when] = result.passed
|
||||
|
||||
@@ -803,7 +803,7 @@ class TestLocalPath(CommonFSTests):
|
||||
# depending on how the paths are used), but > 4096 (which is the
|
||||
# Linux' limitation) - the behaviour of paths with names > 4096 chars
|
||||
# is undetermined
|
||||
newfilename = "/test" * 60
|
||||
newfilename = "/test" * 60 # type:ignore[unreachable]
|
||||
l1 = tmpdir.join(newfilename)
|
||||
l1.ensure(file=True)
|
||||
l1.write("foo")
|
||||
@@ -1344,8 +1344,8 @@ class TestPOSIXLocalPath:
|
||||
assert realpath.basename == "file"
|
||||
|
||||
def test_owner(self, path1, tmpdir):
|
||||
from pwd import getpwuid
|
||||
from grp import getgrgid
|
||||
from pwd import getpwuid # type:ignore[attr-defined]
|
||||
from grp import getgrgid # type:ignore[attr-defined]
|
||||
|
||||
stat = path1.stat()
|
||||
assert stat.path == path1
|
||||
|
||||
@@ -1165,3 +1165,72 @@ def test_log_file_cli_subdirectories_are_successfully_created(
|
||||
result = pytester.runpytest("--log-file=foo/bar/logf.log")
|
||||
assert "logf.log" in os.listdir(expected)
|
||||
assert result.ret == ExitCode.OK
|
||||
|
||||
|
||||
def test_disable_loggers(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
disabled_log = logging.getLogger('disabled')
|
||||
test_log = logging.getLogger('test')
|
||||
def test_logger_propagation(caplog):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
disabled_log.warning("no log; no stderr")
|
||||
test_log.debug("Visible text!")
|
||||
assert caplog.record_tuples == [('test', 10, 'Visible text!')]
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--log-disable=disabled", "-s")
|
||||
assert result.ret == ExitCode.OK
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_disable_loggers_does_not_propagate(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
parent_logger = logging.getLogger("parent")
|
||||
child_logger = parent_logger.getChild("child")
|
||||
|
||||
def test_logger_propagation_to_parent(caplog):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
parent_logger.warning("some parent logger message")
|
||||
child_logger.warning("some child logger message")
|
||||
assert len(caplog.record_tuples) == 1
|
||||
assert caplog.record_tuples[0][0] == "parent"
|
||||
assert caplog.record_tuples[0][2] == "some parent logger message"
|
||||
"""
|
||||
)
|
||||
|
||||
result = testdir.runpytest("--log-disable=parent.child", "-s")
|
||||
assert result.ret == ExitCode.OK
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_log_disabling_works_with_log_cli(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
disabled_log = logging.getLogger('disabled')
|
||||
test_log = logging.getLogger('test')
|
||||
|
||||
def test_log_cli_works(caplog):
|
||||
test_log.info("Visible text!")
|
||||
disabled_log.warning("This string will be suppressed.")
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest(
|
||||
"--log-cli-level=DEBUG",
|
||||
"--log-disable=disabled",
|
||||
)
|
||||
assert result.ret == ExitCode.OK
|
||||
result.stdout.fnmatch_lines(
|
||||
"INFO test:test_log_disabling_works_with_log_cli.py:6 Visible text!"
|
||||
)
|
||||
result.stdout.no_fnmatch_line(
|
||||
"WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed."
|
||||
)
|
||||
assert not result.stderr.lines
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
anyio[curio,trio]==3.6.1
|
||||
django==4.1.2
|
||||
pytest-asyncio==0.19.0
|
||||
pytest-bdd==6.0.1
|
||||
anyio[curio,trio]==3.6.2
|
||||
django==4.1.3
|
||||
pytest-asyncio==0.20.2
|
||||
pytest-bdd==6.1.1
|
||||
pytest-cov==4.0.0
|
||||
pytest-django==4.5.2
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==3.1.1
|
||||
pytest-html==3.2.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-rerunfailures==10.2
|
||||
pytest-rerunfailures==10.3
|
||||
pytest-sugar==0.9.5
|
||||
pytest-trio==0.7.0
|
||||
pytest-twisted==1.14.0
|
||||
|
||||
@@ -416,7 +416,7 @@ def test_function_instance(pytester: Pytester) -> None:
|
||||
def test_static(): pass
|
||||
"""
|
||||
)
|
||||
assert len(items) == 3
|
||||
assert len(items) == 4
|
||||
assert isinstance(items[0], Function)
|
||||
assert items[0].name == "test_func"
|
||||
assert items[0].instance is None
|
||||
@@ -424,6 +424,6 @@ def test_function_instance(pytester: Pytester) -> None:
|
||||
assert items[1].name == "test_method"
|
||||
assert items[1].instance is not None
|
||||
assert items[1].instance.__class__.__name__ == "TestIt"
|
||||
assert isinstance(items[2], Function)
|
||||
assert items[2].name == "test_static"
|
||||
assert items[2].instance is None
|
||||
assert isinstance(items[3], Function)
|
||||
assert items[3].name == "test_static"
|
||||
assert items[3].instance is None
|
||||
|
||||
@@ -807,9 +807,9 @@ class TestAssert_reprcompare_dataclass:
|
||||
"E ['field_b']",
|
||||
"E ",
|
||||
"E Drill down into differing attribute field_b:",
|
||||
"E field_b: 'b' != 'c'...",
|
||||
"E ",
|
||||
"E ...Full output truncated (3 lines hidden), use '-vv' to show",
|
||||
"E field_b: 'b' != 'c'",
|
||||
"E - c",
|
||||
"E + b",
|
||||
],
|
||||
consecutive=True,
|
||||
)
|
||||
@@ -827,7 +827,7 @@ class TestAssert_reprcompare_dataclass:
|
||||
"E Drill down into differing attribute g:",
|
||||
"E g: S(a=10, b='ten') != S(a=20, b='xxx')...",
|
||||
"E ",
|
||||
"E ...Full output truncated (52 lines hidden), use '-vv' to show",
|
||||
"E ...Full output truncated (51 lines hidden), use '-vv' to show",
|
||||
],
|
||||
consecutive=True,
|
||||
)
|
||||
@@ -1188,30 +1188,55 @@ class TestTruncateExplanation:
|
||||
def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None:
|
||||
expl = ["" for x in range(50)]
|
||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100)
|
||||
assert len(result) != len(expl)
|
||||
assert result != expl
|
||||
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
||||
assert "Full output truncated" in result[-1]
|
||||
assert "43 lines hidden" in result[-1]
|
||||
assert "42 lines hidden" in result[-1]
|
||||
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||
assert last_line_before_trunc_msg.endswith("...")
|
||||
|
||||
def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None:
|
||||
expl = ["a" for x in range(100)]
|
||||
total_lines = 100
|
||||
expl = ["a" for x in range(total_lines)]
|
||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
|
||||
assert result != expl
|
||||
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
||||
assert "Full output truncated" in result[-1]
|
||||
assert "93 lines hidden" in result[-1]
|
||||
assert f"{total_lines - 8} lines hidden" in result[-1]
|
||||
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||
assert last_line_before_trunc_msg.endswith("...")
|
||||
|
||||
def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None:
|
||||
"""The number of line in the result is 9, the same number as if we truncated."""
|
||||
expl = ["a" for x in range(9)]
|
||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
|
||||
assert result == expl
|
||||
assert "truncated" not in result[-1]
|
||||
|
||||
def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_chars(
|
||||
self,
|
||||
) -> None:
|
||||
line = "a" * 10
|
||||
expl = [line, line]
|
||||
result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10)
|
||||
assert result == [line, line]
|
||||
|
||||
def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines(
|
||||
self,
|
||||
) -> None:
|
||||
line = "a" * 10
|
||||
expl = [line, line]
|
||||
result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100)
|
||||
assert result == [line, line]
|
||||
|
||||
def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None:
|
||||
expl = ["a" * 80 for x in range(16)]
|
||||
expl = [chr(97 + x) * 80 for x in range(16)]
|
||||
result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80)
|
||||
assert result != expl
|
||||
assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG
|
||||
assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG
|
||||
assert "Full output truncated" in result[-1]
|
||||
assert "9 lines hidden" in result[-1]
|
||||
assert "8 lines hidden" in result[-1]
|
||||
last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1]
|
||||
assert last_line_before_trunc_msg.endswith("...")
|
||||
|
||||
@@ -1240,7 +1265,7 @@ class TestTruncateExplanation:
|
||||
|
||||
line_count = 7
|
||||
line_len = 100
|
||||
expected_truncated_lines = 2
|
||||
expected_truncated_lines = 1
|
||||
pytester.makepyfile(
|
||||
r"""
|
||||
def test_many_lines():
|
||||
@@ -1261,7 +1286,7 @@ class TestTruncateExplanation:
|
||||
"*+ 1*",
|
||||
"*+ 3*",
|
||||
"*+ 5*",
|
||||
"*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines,
|
||||
"*truncated (%d line hidden)*use*-vv*" % expected_truncated_lines,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1664,15 +1689,7 @@ def test_raise_assertion_error_raising_repr(pytester: Pytester) -> None:
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
if sys.version_info >= (3, 11):
|
||||
# python 3.11 has native support for un-str-able exceptions
|
||||
result.stdout.fnmatch_lines(
|
||||
["E AssertionError: <exception str() failed>"]
|
||||
)
|
||||
else:
|
||||
result.stdout.fnmatch_lines(
|
||||
["E AssertionError: <unprintable AssertionError object>"]
|
||||
)
|
||||
result.stdout.fnmatch_lines(["E AssertionError: <exception str() failed>"])
|
||||
|
||||
|
||||
def test_issue_1944(pytester: Pytester) -> None:
|
||||
|
||||
@@ -735,6 +735,20 @@ class Test_genitems:
|
||||
assert s.endswith("test_example_items1.testone")
|
||||
print(s)
|
||||
|
||||
def test_classmethod_is_discovered(self, pytester: Pytester) -> None:
|
||||
"""Test that classmethods are discovered"""
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
class TestCase:
|
||||
@classmethod
|
||||
def test_classmethod(cls) -> None:
|
||||
pass
|
||||
"""
|
||||
)
|
||||
items, reprec = pytester.inline_genitems(p)
|
||||
ids = [x.getmodpath() for x in items] # type: ignore[attr-defined]
|
||||
assert ids == ["TestCase.test_classmethod"]
|
||||
|
||||
def test_class_and_functions_discovery_using_glob(self, pytester: Pytester) -> None:
|
||||
"""Test that Python_classes and Python_functions config options work
|
||||
as prefixes and glob-like patterns (#600)."""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
@@ -5,6 +6,7 @@ import pytest
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.config.findpaths import get_common_ancestor
|
||||
from _pytest.config.findpaths import get_dirs_from_args
|
||||
from _pytest.config.findpaths import is_fs_root
|
||||
from _pytest.config.findpaths import load_config_dict_from_file
|
||||
|
||||
|
||||
@@ -133,3 +135,18 @@ def test_get_dirs_from_args(tmp_path):
|
||||
assert get_dirs_from_args(
|
||||
[str(fn), str(tmp_path / "does_not_exist"), str(d), option, xdist_rsync_option]
|
||||
) == [fn.parent, d]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path, expected",
|
||||
[
|
||||
pytest.param(
|
||||
f"e:{os.sep}", True, marks=pytest.mark.skipif("sys.platform != 'win32'")
|
||||
),
|
||||
(f"{os.sep}", True),
|
||||
(f"e:{os.sep}projects", False),
|
||||
(f"{os.sep}projects", False),
|
||||
],
|
||||
)
|
||||
def test_is_fs_root(path: Path, expected: bool) -> None:
|
||||
assert is_fs_root(Path(path)) is expected
|
||||
|
||||
@@ -92,7 +92,7 @@ class TestSetattrWithImportPath:
|
||||
mp.delattr("os.path.abspath")
|
||||
assert not hasattr(os.path, "abspath")
|
||||
mp.undo()
|
||||
assert os.path.abspath
|
||||
assert os.path.abspath # type:ignore[truthy-function]
|
||||
|
||||
|
||||
def test_delattr() -> None:
|
||||
|
||||
@@ -2,6 +2,7 @@ import inspect
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
@@ -19,6 +20,9 @@ from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.outcomes import OutcomeException
|
||||
from _pytest.pytester import Pytester
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
|
||||
class TestSetupState:
|
||||
def test_setup(self, pytester: Pytester) -> None:
|
||||
@@ -77,8 +81,6 @@ class TestSetupState:
|
||||
assert r == ["fin3", "fin1"]
|
||||
|
||||
def test_teardown_multiple_fail(self, pytester: Pytester) -> None:
|
||||
# Ensure the first exception is the one which is re-raised.
|
||||
# Ideally both would be reported however.
|
||||
def fin1():
|
||||
raise Exception("oops1")
|
||||
|
||||
@@ -90,9 +92,14 @@ class TestSetupState:
|
||||
ss.setup(item)
|
||||
ss.addfinalizer(fin1, item)
|
||||
ss.addfinalizer(fin2, item)
|
||||
with pytest.raises(Exception) as err:
|
||||
with pytest.raises(ExceptionGroup) as err:
|
||||
ss.teardown_exact(None)
|
||||
assert err.value.args == ("oops2",)
|
||||
|
||||
# Note that finalizers are run LIFO, but because FIFO is more intuitive for
|
||||
# users we reverse the order of messages, and see the error from fin1 first.
|
||||
err1, err2 = err.value.exceptions
|
||||
assert err1.args == ("oops1",)
|
||||
assert err2.args == ("oops2",)
|
||||
|
||||
def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None:
|
||||
module_teardown = []
|
||||
@@ -113,6 +120,25 @@ class TestSetupState:
|
||||
ss.teardown_exact(None)
|
||||
assert module_teardown == ["fin_module"]
|
||||
|
||||
def test_teardown_multiple_scopes_several_fail(self, pytester) -> None:
|
||||
def raiser(exc):
|
||||
raise exc
|
||||
|
||||
item = pytester.getitem("def test_func(): pass")
|
||||
mod = item.listchain()[-2]
|
||||
ss = item.session._setupstate
|
||||
ss.setup(item)
|
||||
ss.addfinalizer(partial(raiser, KeyError("from module scope")), mod)
|
||||
ss.addfinalizer(partial(raiser, TypeError("from function scope 1")), item)
|
||||
ss.addfinalizer(partial(raiser, ValueError("from function scope 2")), item)
|
||||
|
||||
with pytest.raises(ExceptionGroup, match="errors during test teardown") as e:
|
||||
ss.teardown_exact(None)
|
||||
mod, func = e.value.exceptions
|
||||
assert isinstance(mod, KeyError)
|
||||
assert isinstance(func.exceptions[0], TypeError) # type: ignore
|
||||
assert isinstance(func.exceptions[1], ValueError) # type: ignore
|
||||
|
||||
|
||||
class BaseFunctionalTests:
|
||||
def test_passfunction(self, pytester: Pytester) -> None:
|
||||
|
||||
@@ -1439,6 +1439,27 @@ def test_relpath_rootdir(pytester: Pytester) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_skip_from_fixture(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
**{
|
||||
"tests/test_1.py": """
|
||||
import pytest
|
||||
def test_pass(arg):
|
||||
pass
|
||||
@pytest.fixture
|
||||
def arg():
|
||||
condition = True
|
||||
if condition:
|
||||
pytest.skip("Fixture conditional skip")
|
||||
""",
|
||||
}
|
||||
)
|
||||
result = pytester.runpytest("-rs", "tests/test_1.py", "--rootdir=tests")
|
||||
result.stdout.fnmatch_lines(
|
||||
["SKIPPED [[]1[]] tests/test_1.py:2: Fixture conditional skip"]
|
||||
)
|
||||
|
||||
|
||||
def test_skip_using_reason_works_ok(pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
|
||||
@@ -42,6 +42,14 @@ class FakeConfig:
|
||||
def get(self, key):
|
||||
return lambda *k: None
|
||||
|
||||
def getini(self, name):
|
||||
if name == "tmp_path_retention_count":
|
||||
return 3
|
||||
elif name == "tmp_path_retention_policy":
|
||||
return "failed"
|
||||
else:
|
||||
assert False
|
||||
|
||||
@property
|
||||
def option(self):
|
||||
return self
|
||||
@@ -84,6 +92,117 @@ class TestConfigTmpPath:
|
||||
assert mytemp.exists()
|
||||
assert not mytemp.joinpath("hello").exists()
|
||||
|
||||
def test_policy_failed_removes_only_passed_dir(self, pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
def test_1(tmp_path):
|
||||
assert 0 == 0
|
||||
def test_2(tmp_path):
|
||||
assert 0 == 1
|
||||
"""
|
||||
)
|
||||
|
||||
pytester.inline_run(p)
|
||||
root = pytester._test_tmproot
|
||||
|
||||
for child in root.iterdir():
|
||||
base_dir = list(
|
||||
filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir())
|
||||
)
|
||||
assert len(base_dir) == 1
|
||||
test_dir = list(
|
||||
filter(
|
||||
lambda x: x.is_dir() and not x.is_symlink(), base_dir[0].iterdir()
|
||||
)
|
||||
)
|
||||
# Check only the failed one remains
|
||||
assert len(test_dir) == 1
|
||||
assert test_dir[0].name == "test_20"
|
||||
|
||||
def test_policy_failed_removes_basedir_when_all_passed(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
def test_1(tmp_path):
|
||||
assert 0 == 0
|
||||
"""
|
||||
)
|
||||
|
||||
pytester.inline_run(p)
|
||||
root = pytester._test_tmproot
|
||||
for child in root.iterdir():
|
||||
# This symlink will be deleted by cleanup_numbered_dir **after**
|
||||
# the test finishes because it's triggered by atexit.
|
||||
# So it has to be ignored here.
|
||||
base_dir = filter(lambda x: not x.is_symlink(), child.iterdir())
|
||||
# Check the base dir itself is gone
|
||||
assert len(list(base_dir)) == 0
|
||||
|
||||
# issue #10502
|
||||
def test_policy_failed_removes_dir_when_skipped_from_fixture(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def fixt(tmp_path):
|
||||
pytest.skip()
|
||||
|
||||
def test_fixt(fixt):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
pytester.inline_run(p)
|
||||
|
||||
# Check if the whole directory is removed
|
||||
root = pytester._test_tmproot
|
||||
for child in root.iterdir():
|
||||
base_dir = list(
|
||||
filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir())
|
||||
)
|
||||
assert len(base_dir) == 0
|
||||
|
||||
# issue #10502
|
||||
def test_policy_all_keeps_dir_when_skipped_from_fixture(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def fixt(tmp_path):
|
||||
pytest.skip()
|
||||
|
||||
def test_fixt(fixt):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
pytester.makepyprojecttoml(
|
||||
"""
|
||||
[tool.pytest.ini_options]
|
||||
tmp_path_retention_policy = "all"
|
||||
"""
|
||||
)
|
||||
pytester.inline_run(p)
|
||||
|
||||
# Check if the whole directory is kept
|
||||
root = pytester._test_tmproot
|
||||
for child in root.iterdir():
|
||||
base_dir = list(
|
||||
filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir())
|
||||
)
|
||||
assert len(base_dir) == 1
|
||||
test_dir = list(
|
||||
filter(
|
||||
lambda x: x.is_dir() and not x.is_symlink(), base_dir[0].iterdir()
|
||||
)
|
||||
)
|
||||
assert len(test_dir) == 1
|
||||
|
||||
|
||||
testdata = [
|
||||
("mypath", True),
|
||||
@@ -275,12 +394,12 @@ class TestNumberedDir:
|
||||
|
||||
assert not lock.exists()
|
||||
|
||||
def _do_cleanup(self, tmp_path: Path) -> None:
|
||||
def _do_cleanup(self, tmp_path: Path, keep: int = 2) -> None:
|
||||
self.test_make(tmp_path)
|
||||
cleanup_numbered_dir(
|
||||
root=tmp_path,
|
||||
prefix=self.PREFIX,
|
||||
keep=2,
|
||||
keep=keep,
|
||||
consider_lock_dead_if_created_before=0,
|
||||
)
|
||||
|
||||
@@ -289,6 +408,11 @@ class TestNumberedDir:
|
||||
a, b = (x for x in tmp_path.iterdir() if not x.is_symlink())
|
||||
print(a, b)
|
||||
|
||||
def test_cleanup_keep_0(self, tmp_path: Path):
|
||||
self._do_cleanup(tmp_path, 0)
|
||||
dir_num = len(list(tmp_path.iterdir()))
|
||||
assert dir_num == 0
|
||||
|
||||
def test_cleanup_locked(self, tmp_path):
|
||||
p = make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
|
||||
|
||||
@@ -446,7 +570,7 @@ def test_tmp_path_factory_create_directory_with_safe_permissions(
|
||||
"""Verify that pytest creates directories under /tmp with private permissions."""
|
||||
# Use the test's tmp_path as the system temproot (/tmp).
|
||||
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
|
||||
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# No world-readable permissions.
|
||||
@@ -466,14 +590,14 @@ def test_tmp_path_factory_fixes_up_world_readable_permissions(
|
||||
"""
|
||||
# Use the test's tmp_path as the system temproot (/tmp).
|
||||
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
|
||||
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# Before - simulate bad perms.
|
||||
os.chmod(basetemp.parent, 0o777)
|
||||
assert (basetemp.parent.stat().st_mode & 0o077) != 0
|
||||
|
||||
tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# After - fixed.
|
||||
|
||||
15
tox.ini
15
tox.ini
@@ -9,6 +9,7 @@ envlist =
|
||||
py39
|
||||
py310
|
||||
py311
|
||||
py312
|
||||
pypy3
|
||||
py37-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib}
|
||||
doctesting
|
||||
@@ -29,7 +30,11 @@ commands =
|
||||
doctesting: {env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest
|
||||
coverage: coverage combine
|
||||
coverage: coverage report -m
|
||||
passenv = USER USERNAME COVERAGE_* PYTEST_ADDOPTS TERM SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST
|
||||
passenv =
|
||||
COVERAGE_*
|
||||
PYTEST_ADDOPTS
|
||||
TERM
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST
|
||||
setenv =
|
||||
_PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_DOCTESTING:} {env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_XDIST:}
|
||||
|
||||
@@ -92,7 +97,8 @@ commands =
|
||||
[testenv:regen]
|
||||
changedir = doc/en
|
||||
basepython = python3
|
||||
passenv = SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST
|
||||
passenv =
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST
|
||||
deps =
|
||||
dataclasses
|
||||
PyYAML
|
||||
@@ -160,7 +166,10 @@ commands = python scripts/prepare-release-pr.py {posargs}
|
||||
description = create GitHub release after deployment
|
||||
basepython = python3
|
||||
usedevelop = True
|
||||
passenv = GH_RELEASE_NOTES_TOKEN GITHUB_REF GITHUB_REPOSITORY
|
||||
passenv =
|
||||
GH_RELEASE_NOTES_TOKEN
|
||||
GITHUB_REF
|
||||
GITHUB_REPOSITORY
|
||||
deps =
|
||||
github3.py
|
||||
pypandoc
|
||||
|
||||
Reference in New Issue
Block a user