Merge branch 'main' into RonnyPfannschmidt-pypi-oidc-deploy-env

This commit is contained in:
Ronny Pfannschmidt 2023-07-02 19:45:11 +02:00 committed by GitHub
commit d7def89b2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 2746 additions and 1314 deletions

View File

@ -58,7 +58,8 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.8" python-version: "3.11"
- name: Install tox - name: Install tox
run: | run: |

View File

@ -10,12 +10,12 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/stale@v5 - uses: actions/stale@v8
with: with:
debug-only: true debug-only: false
days-before-issue-stale: 14 days-before-issue-stale: 14
days-before-issue-close: 7 days-before-issue-close: 7
only-labels: ["status: needs information"] only-labels: "status: needs information"
stale-issue-label: "stale" stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity." stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."

View File

@ -37,26 +37,26 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
name: [ name: [
"windows-py37",
"windows-py37-pluggy",
"windows-py38", "windows-py38",
"windows-py38-pluggy",
"windows-py39", "windows-py39",
"windows-py310", "windows-py310",
"windows-py311", "windows-py311",
"windows-py312",
"ubuntu-py37",
"ubuntu-py37-pluggy",
"ubuntu-py37-freeze",
"ubuntu-py38", "ubuntu-py38",
"ubuntu-py38-pluggy",
"ubuntu-py38-freeze",
"ubuntu-py39", "ubuntu-py39",
"ubuntu-py310", "ubuntu-py310",
"ubuntu-py311", "ubuntu-py311",
"ubuntu-py312",
"ubuntu-pypy3", "ubuntu-pypy3",
"macos-py37",
"macos-py38", "macos-py38",
"macos-py39", "macos-py39",
"macos-py310", "macos-py310",
"macos-py312",
"docs", "docs",
"doctesting", "doctesting",
@ -64,19 +64,15 @@ jobs:
] ]
include: include:
- name: "windows-py37"
python: "3.7"
os: windows-latest
tox_env: "py37-numpy"
- name: "windows-py37-pluggy"
python: "3.7"
os: windows-latest
tox_env: "py37-pluggymain-pylib-xdist"
- name: "windows-py38" - name: "windows-py38"
python: "3.8" python: "3.8"
os: windows-latest os: windows-latest
tox_env: "py38-unittestextras" tox_env: "py38-unittestextras"
use_coverage: true use_coverage: true
- name: "windows-py38-pluggy"
python: "3.8"
os: windows-latest
tox_env: "py38-pluggymain-pylib-xdist"
- name: "windows-py39" - name: "windows-py39"
python: "3.9" python: "3.9"
os: windows-latest os: windows-latest
@ -86,27 +82,27 @@ jobs:
os: windows-latest os: windows-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "windows-py311" - name: "windows-py311"
python: "3.11-dev" python: "3.11"
os: windows-latest os: windows-latest
tox_env: "py311" tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
os: windows-latest
tox_env: "py312"
- name: "ubuntu-py37"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-lsof-numpy-pexpect"
use_coverage: true
- name: "ubuntu-py37-pluggy"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-pluggymain-pylib-xdist"
- name: "ubuntu-py37-freeze"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-freeze"
- name: "ubuntu-py38" - name: "ubuntu-py38"
python: "3.8" python: "3.8"
os: ubuntu-latest os: ubuntu-latest
tox_env: "py38-xdist" tox_env: "py38-lsof-numpy-pexpect"
use_coverage: true
- name: "ubuntu-py38-pluggy"
python: "3.8"
os: ubuntu-latest
tox_env: "py38-pluggymain-pylib-xdist"
- name: "ubuntu-py38-freeze"
python: "3.8"
os: ubuntu-latest
tox_env: "py38-freeze"
- name: "ubuntu-py39" - name: "ubuntu-py39"
python: "3.9" python: "3.9"
os: ubuntu-latest os: ubuntu-latest
@ -116,32 +112,37 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "ubuntu-py311" - name: "ubuntu-py311"
python: "3.11-dev" python: "3.11"
os: ubuntu-latest os: ubuntu-latest
tox_env: "py311" tox_env: "py311"
use_coverage: true use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-pypy3" - name: "ubuntu-pypy3"
python: "pypy-3.7" python: "pypy-3.8"
os: ubuntu-latest os: ubuntu-latest
tox_env: "pypy3-xdist" tox_env: "pypy3-xdist"
- name: "macos-py37"
python: "3.7"
os: macos-latest
tox_env: "py37-xdist"
- name: "macos-py38" - name: "macos-py38"
python: "3.8" python: "3.8"
os: macos-latest os: macos-latest
tox_env: "py38-xdist" tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39" - name: "macos-py39"
python: "3.9" python: "3.9"
os: macos-latest os: macos-latest
tox_env: "py39-xdist" tox_env: "py39-xdist"
use_coverage: true
- name: "macos-py310" - name: "macos-py310"
python: "3.10" python: "3.10"
os: macos-latest os: macos-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
os: macos-latest
tox_env: "py312-xdist"
- name: "plugins" - name: "plugins"
python: "3.9" python: "3.9"
@ -149,11 +150,11 @@ jobs:
tox_env: "plugins" tox_env: "plugins"
- name: "docs" - name: "docs"
python: "3.7" python: "3.8"
os: ubuntu-latest os: ubuntu-latest
tox_env: "docs" tox_env: "docs"
- name: "doctesting" - name: "doctesting"
python: "3.7" python: "3.8"
os: ubuntu-latest os: ubuntu-latest
tox_env: "doctesting" tox_env: "doctesting"
use_coverage: true use_coverage: true
@ -168,6 +169,7 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }}
- name: Install dependencies - name: Install dependencies
run: | run: |

View File

@ -38,7 +38,7 @@ jobs:
run: python scripts/update-plugin-list.py run: python scripts/update-plugin-list.py
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
with: with:
commit-message: '[automated] Update plugin list' commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>' author: 'pytest bot <pytestbot@users.noreply.github.com>'

View File

@ -1,5 +1,3 @@
default_language_version:
python: "3.10"
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.3.0 rev: 23.3.0
@ -7,7 +5,7 @@ repos:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs - repo: https://github.com/asottile/blacken-docs
rev: 1.13.0 rev: 1.14.0
hooks: hooks:
- id: blacken-docs - id: blacken-docs
additional_dependencies: [black==23.1.0] additional_dependencies: [black==23.1.0]
@ -23,7 +21,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py exclude: _pytest/(debugging|hookspec).py
language_version: python3 language_version: python3
- repo: https://github.com/PyCQA/autoflake - repo: https://github.com/PyCQA/autoflake
rev: v2.1.1 rev: v2.2.0
hooks: hooks:
- id: autoflake - id: autoflake
name: autoflake name: autoflake
@ -39,26 +37,26 @@ repos:
- flake8-typing-imports==1.12.0 - flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0 - flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder-python-imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.9.0 rev: v3.10.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus] args: ['--application-directories=.:src', --py38-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.4.0 rev: v3.7.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py38-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.0 rev: v2.3.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
args: ["--max-py-version=3.11", "--include-version-classifiers"] args: ["--max-py-version=3.12", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0 rev: v1.10.0
hooks: hooks:
- id: python-use-type-annotations - id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.2.0 rev: v1.4.1
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)

View File

@ -8,12 +8,14 @@ Abdeali JK
Abdelrahman Elbehery Abdelrahman Elbehery
Abhijeet Kasurde Abhijeet Kasurde
Adam Johnson Adam Johnson
Adam Stewart
Adam Uhlir Adam Uhlir
Ahn Ki-Wook Ahn Ki-Wook
Akiomi Kamakura Akiomi Kamakura
Alan Velasco Alan Velasco
Alessio Izzo Alessio Izzo
Alex Jones Alex Jones
Alex Lambson
Alexander Johnson Alexander Johnson
Alexander King Alexander King
Alexei Kozlenok Alexei Kozlenok
@ -70,6 +72,7 @@ Charles Cloud
Charles Machalow Charles Machalow
Charnjit SiNGH (CCSJ) Charnjit SiNGH (CCSJ)
Cheuk Ting Ho Cheuk Ting Ho
Chris Mahoney
Chris Lamb Chris Lamb
Chris NeJame Chris NeJame
Chris Rose Chris Rose
@ -126,8 +129,10 @@ Eric Hunsberger
Eric Liu Eric Liu
Eric Siegerman Eric Siegerman
Erik Aronesty Erik Aronesty
Erik Hasse
Erik M. Bray Erik M. Bray
Evan Kepner Evan Kepner
Evgeny Seliverstov
Fabien Zarifian Fabien Zarifian
Fabio Zadrozny Fabio Zadrozny
Felix Hofstätter Felix Hofstätter
@ -194,6 +199,7 @@ Justice Ndou
Justyna Janczyszyn Justyna Janczyszyn
Kale Kundert Kale Kundert
Kamran Ahmad Kamran Ahmad
Kenny Y
Karl O. Pinc Karl O. Pinc
Karthikeyan Singaravelan Karthikeyan Singaravelan
Katarzyna Jachim Katarzyna Jachim
@ -305,7 +311,9 @@ Raphael Pierzina
Rafal Semik Rafal Semik
Raquel Alegre Raquel Alegre
Ravi Chandra Ravi Chandra
Reagan Lee
Robert Holt Robert Holt
Roberto Aldera
Roberto Polli Roberto Polli
Roland Puntaier Roland Puntaier
Romain Dorgueil Romain Dorgueil
@ -371,6 +379,7 @@ Victor Maryama
Victor Rodriguez Victor Rodriguez
Victor Uriarte Victor Uriarte
Vidar T. Fauske Vidar T. Fauske
Vijay Arora
Virgil Dupras Virgil Dupras
Vitaly Lashmanov Vitaly Lashmanov
Vivaan Verma Vivaan Verma

View File

@ -201,7 +201,7 @@ Short version
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting. #. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
#. Tests are run using ``tox``:: #. Tests are run using ``tox``::
tox -e linting,py37 tox -e linting,py39
The test environments above are usually enough to cover most cases locally. The test environments above are usually enough to cover most cases locally.
@ -272,24 +272,24 @@ Here is a simple overview, with pytest-specific bits:
#. Run all the tests #. Run all the tests
You need to have Python 3.7 available in your system. Now You need to have Python 3.8 or later available in your system. Now
running tests is as simple as issuing this command:: running tests is as simple as issuing this command::
$ tox -e linting,py37 $ tox -e linting,py39
This command will run tests via the "tox" tool against Python 3.7 This command will run tests via the "tox" tool against Python 3.9
and also perform "lint" coding-style checks. and also perform "lint" coding-style checks.
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming. #. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming.
You can pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
(e.g. enter pdb on failure) to pytest you can do:: (e.g. enter pdb on failure) to pytest you can do::
$ tox -e py37 -- --pdb $ tox -e py39 -- --pdb
Or to only run tests in a particular test module on Python 3.7:: Or to only run tests in a particular test module on Python 3.9::
$ tox -e py37 -- testing/test_config.py $ tox -e py39 -- testing/test_config.py
When committing, ``pre-commit`` will re-format the files if necessary. When committing, ``pre-commit`` will re-format the files if necessary.

View File

@ -100,7 +100,7 @@ Features
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial), - Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box `nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
- Python 3.7+ or PyPy3 - Python 3.8+ or PyPy3
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community - Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community

View File

@ -1 +0,0 @@
Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.

View File

@ -0,0 +1,2 @@
Fixed but that fake intermediate modules generated by ``--import-mode=importlib`` would not include the
child modules as attributes of the parent modules.

View File

@ -0,0 +1,2 @@
Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
to be imported more than once, causing problems with modules that have import side effects.

View File

@ -1 +0,0 @@
Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.

View File

@ -1,2 +0,0 @@
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.

View File

@ -1,5 +0,0 @@
When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
Previously, the last frame of the traceback was shown, even though it was hidden.

View File

@ -1,3 +0,0 @@
Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.

View File

@ -1 +0,0 @@
:confval:`testpaths` is now honored to load root ``conftests``.

1
changelog/11011.doc.rst Normal file
View File

@ -0,0 +1 @@
Added a warning about modifying the root logger during tests when using ``caplog``.

View File

@ -0,0 +1,2 @@
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27
<https://devguide.python.org/versions/>`__.

View File

@ -0,0 +1,3 @@
Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
This will become an error in the future.

View File

@ -0,0 +1,22 @@
**PytestRemovedIn8Warning deprecation warnings are now errors by default.**
Following our plan to remove deprecated features with as little disruption as
possible, all warnings of type ``PytestRemovedIn8Warning`` now generate errors
instead of warning messages by default.
**The affected features will be effectively removed in pytest 8.1**, so please consult the
:ref:`deprecations` section in the docs for directions on how to update existing code.
In the pytest ``8.0.X`` series, it is possible to change the errors back into warnings as a
stopgap measure by adding this to your ``pytest.ini`` file:
.. code-block:: ini
[pytest]
filterwarnings =
ignore::pytest.PytestRemovedIn8Warning
But this will stop working when pytest ``8.1`` is released.
**If you have concerns** about the removal of a specific feature, please add a
comment to :issue:`7363`.

View File

@ -0,0 +1,5 @@
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
(unless :confval:`python_files` was changed to allow `__init__.py` file).
To collect the entire package, specify just the directory: `pytest pkg`.

View File

@ -0,0 +1 @@
``pytest.warns`` and similar functions now capture warnings when an exception is raised inside a ``with`` block.

View File

@ -0,0 +1,7 @@
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
closes -- previously it would consume all warnings, hiding those that were not
matched by the function.
While this is a new feature, we decided to announce this as a breaking change
because many test suites are configured to error-out on warnings, and will
therefore fail on the newly-re-emitted warnings.

View File

@ -6,6 +6,8 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-7.4.0
release-7.3.2
release-7.3.1 release-7.3.1
release-7.3.0 release-7.3.0
release-7.2.2 release-7.2.2

View File

@ -0,0 +1,21 @@
pytest-7.3.2
=======================================
pytest 7.3.2 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Adam J. Stewart
* Alessio Izzo
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,49 @@
pytest-7.4.0
=======================================
The pytest team is proud to announce the 7.4.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:
* Adam J. Stewart
* Alessio Izzo
* Alex
* Alex Lambson
* Brian Larsen
* Bruno Oliveira
* Bryan Ricker
* Chris Mahoney
* Facundo Batista
* Florian Bruhin
* Jarrett Keifer
* Kenny Y
* Miro Hrončok
* Ran Benita
* Roberto Aldera
* Ronny Pfannschmidt
* Sergey Kim
* Stefanie Molin
* Vijay Arora
* Ville Skyttä
* Zac Hatfield-Dodds
* bzoracler
* leeyueh
* nondescryptid
* theirix
Happy testing,
The pytest Development Team

View File

@ -87,8 +87,11 @@ Released pytest versions support all Python versions that are actively maintaine
============== =================== ============== ===================
pytest version min. Python version pytest version min. Python version
============== =================== ============== ===================
8.0+ 3.8+
7.1+ 3.7+ 7.1+ 3.7+
6.2 - 7.0 3.6+ 6.2 - 7.0 3.6+
5.0 - 6.1 3.5+ 5.0 - 6.1 3.5+
3.3 - 4.6 2.7, 3.4+ 3.3 - 4.6 2.7, 3.4+
============== =================== ============== ===================
`Status of Python Versions <https://devguide.python.org/versions/>`__.

View File

@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache cachedir: .pytest_cache
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 0 items collected 0 items
cache -- .../_pytest/cacheprovider.py:510 cache -- .../_pytest/cacheprovider.py:528
Return a cache object that can persist state between testing sessions. Return a cache object that can persist state between testing sessions.
cache.get(key, default) cache.get(key, default)
@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`. For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360 pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
Session-scoped fixture that returns the session's :class:`pytest.Config` Session-scoped fixture that returns the session's :class:`pytest.Config`
object. object.
@ -196,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 .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:498 caplog -- .../_pytest/logging.py:570
Access and control log capturing. Access and control log capturing.
Captured logs are available through the following properties/methods:: Captured logs are available through the following properties/methods::
@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
* caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string * caplog.clear() -> clear captured records and formatted log output string
monkeypatch -- .../_pytest/monkeypatch.py:29 monkeypatch -- .../_pytest/monkeypatch.py:30
A convenient fixture for monkey-patching. A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or The fixture provides these methods to modify objects, dictionaries, or

View File

@ -28,6 +28,122 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 7.4.0 (2023-06-23)
=========================
Features
--------
- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
Improvements
------------
- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
Previously, the last frame of the traceback was shown, even though it was hidden.
- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
Added ``TerminalReporter.wrap_write()`` as a helper for that.
- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`.
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
If after updating to this version you see that your `norecursedirs` setting is not being respected,
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
Most likely, your hook returns `False` for paths it does not want to ignore,
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
``logging.disable(LEVEL)``.
Bug Fixes
---------
- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
even when it was not utilized (e.g. when explicit paths were given on the command line).
Now the ``testpaths`` are only considered when they are in use.
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
Improved Documentation
----------------------
- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
Trivial/Internal Changes
------------------------
- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
pytest 7.3.2 (2023-06-10)
=========================
Bug Fixes
---------
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
pytest 7.3.1 (2023-04-14) pytest 7.3.1 (2023-04-14)
========================= =========================
@ -567,7 +683,7 @@ Breaking Changes
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`. - `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation. Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted. Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`. Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead. Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
@ -4067,7 +4183,7 @@ Removals
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code. See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check. - :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
Use ``Node.get_closest_marker(name)`` as a replacement. Use ``Node.get_closest_marker(name)`` as a replacement.

View File

@ -15,12 +15,10 @@
# #
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
# The short X.Y version. # The short X.Y version.
import ast
import os import os
import shutil import shutil
import sys import sys
from textwrap import dedent from textwrap import dedent
from typing import List
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from _pytest import __version__ as version from _pytest import __version__ as version
@ -341,7 +339,7 @@ epub_copyright = "2013, holger krekel et alii"
# The scheme of the identifier. Typical schemes are ISBN or URL. # The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = '' # epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number # The unique identifier of the text. This can be an ISBN number
# or the project homepage. # or the project homepage.
# epub_identifier = '' # epub_identifier = ''
@ -451,25 +449,6 @@ def setup(app: "sphinx.application.Sphinx") -> None:
configure_logging(app) configure_logging(app)
# Make Sphinx mark classes with "final" when decorated with @final.
# We need this because we import final from pytest._compat, not from
# typing (for Python < 3.8 compat), so Sphinx doesn't detect it.
# To keep things simple we accept any `@final` decorator.
# Ref: https://github.com/pytest-dev/pytest/pull/7780
import sphinx.pycode.ast
import sphinx.pycode.parser
original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final
def patched_is_final(self, decorators: List[ast.expr]) -> bool:
if original_is_final(self, decorators):
return True
return any(
sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators
)
sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final
# legacypath.py monkey-patches pytest.Testdir in. Import the file so # legacypath.py monkey-patches pytest.Testdir in. Import the file so
# that autodoc can discover references to it. # that autodoc can discover references to it.
import _pytest.legacypath # noqa: F401 import _pytest.legacypath # noqa: F401

View File

@ -380,6 +380,25 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a ``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
deprecation warning is now raised. deprecation warning is now raised.
Applying a mark to a fixture function
-------------------------------------
.. deprecated:: 7.4
Applying a mark to a fixture function never had any effect, but it is a common user error.
.. code-block:: python
@pytest.mark.usefixtures("clean_database")
@pytest.fixture
def user() -> User:
...
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
Backward compatibilities in ``Parser.addoption`` Backward compatibilities in ``Parser.addoption``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -467,12 +486,26 @@ The ``yield_fixture`` function/decorator
It has been so for a very long time, so can be search/replaced safely. It has been so for a very long time, so can be search/replaced safely.
Removed Features Removed Features and Breaking Changes
---------------- -------------------------------------
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
an appropriate period of deprecation has passed. an appropriate period of deprecation has passed.
Some breaking changes which could not be deprecated are also listed.
Collecting ``__init__.py`` files no longer collects package
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 8.0
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
(unless :confval:`python_files` was changed to allow `__init__.py` file).
To collect the entire package, specify just the directory: `pytest pkg`.
The ``pytest.collect`` module The ``pytest.collect`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
" no further details known at this point.", " no further details known at this point.",
] ]
) )
return super().repr_failure(excinfo)
def reportinfo(self): def reportinfo(self):
return self.path, 0, f"usecase: {self.name}" return self.path, 0, f"usecase: {self.name}"

View File

@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
pass pass
@pytest.fixture(scope="session") @pytest.fixture(scope="package")
def db(): def db():
return DB() return DB()

View File

@ -9,7 +9,7 @@ Get Started
Install ``pytest`` Install ``pytest``
---------------------------------------- ----------------------------------------
``pytest`` requires: Python 3.7+ or PyPy3. ``pytest`` requires: Python 3.8+ or PyPy3.
1. Run the following command in your command line: 1. Run the following command in your command line:
@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash .. code-block:: bash
$ pytest --version $ pytest --version
pytest 7.3.1 pytest 7.4.0
.. _`simpletest`: .. _`simpletest`:

View File

@ -1752,8 +1752,7 @@ into an ini-file:
def my_fixture_that_sadly_wont_use_my_other_fixture(): def my_fixture_that_sadly_wont_use_my_other_fixture():
... ...
Currently this will not generate any error or warning, but this is intended This generates a deprecation warning, and will become an error in Pytest 8.
to be handled by :issue:`3664`.
.. _`override fixtures`: .. _`override fixtures`:

View File

@ -172,6 +172,13 @@ the records for the ``setup`` and ``call`` stages during teardown like so:
The full API is available at :class:`pytest.LogCaptureFixture`. The full API is available at :class:`pytest.LogCaptureFixture`.
.. warning::
The ``caplog`` fixture adds a handler to the root logger to capture logs. If the root logger is
modified during a test, for example with ``logging.config.dictConfig``, this handler may be
removed and cause no logs to be captured. To avoid this, ensure that any root logger configuration
only adds to the existing handlers.
.. _live_logs: .. _live_logs:

View File

@ -2,6 +2,7 @@
.. sidebar:: Next Open Trainings .. sidebar:: Next Open Trainings
- `pytest tips and tricks for a better testsuite <https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2023 <https://ep2023.europython.eu/>`_, July 18th (3h), Prague/Remote
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote - `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
Also see :doc:`previous talks and blogposts <talks>`. Also see :doc:`previous talks and blogposts <talks>`.
@ -76,7 +77,7 @@ Features
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box - Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
- Python 3.7+ or PyPy 3 - Python 3.8+ or PyPy 3
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community - Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community

File diff suppressed because it is too large Load Diff

View File

@ -956,6 +956,12 @@ TestReport
:show-inheritance: :show-inheritance:
:inherited-members: :inherited-members:
TestShortLogReport
~~~~~~~~~~~~~~~~~~
.. autoclass:: pytest.TestShortLogReport()
:members:
_Result _Result
~~~~~~~ ~~~~~~~
@ -1049,11 +1055,11 @@ Environment variables that can be used to change pytest's behavior.
.. envvar:: CI .. envvar:: CI
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable. When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
.. envvar:: BUILD_NUMBER .. envvar:: BUILD_NUMBER
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable. When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
.. envvar:: PYTEST_ADDOPTS .. envvar:: PYTEST_ADDOPTS
@ -1147,6 +1153,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
.. autoclass:: pytest.PytestRemovedIn8Warning .. autoclass:: pytest.PytestRemovedIn8Warning
:show-inheritance: :show-inheritance:
.. autoclass:: pytest.PytestRemovedIn9Warning
:show-inheritance:
.. autoclass:: pytest.PytestUnhandledCoroutineWarning .. autoclass:: pytest.PytestUnhandledCoroutineWarning
:show-inheritance: :show-inheritance:
@ -1697,6 +1706,11 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest] [pytest]
pythonpath = src1 src2 pythonpath = src1 src2
.. note::
``pythonpath`` does not affect some imports that happen very early,
most notably plugins loaded using the ``-p`` command line option.
.. confval:: required_plugins .. confval:: required_plugins
@ -1912,8 +1926,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
--strict-markers Markers not registered in the `markers` section of --strict-markers Markers not registered in the `markers` section of
the configuration file raise errors the configuration file raise errors
--strict (Deprecated) alias to --strict-markers --strict (Deprecated) alias to --strict-markers
-c file Load configuration from `file` instead of trying to -c FILE, --config-file=FILE
locate one of the implicit configuration files Load configuration from `FILE` instead of trying to
locate one of the implicit configuration files.
--continue-on-collection-errors --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 --rootdir=ROOTDIR Define root directory for tests. Can be relative

View File

@ -7,7 +7,9 @@ def main():
Platform agnostic wrapper script for towncrier. Platform agnostic wrapper script for towncrier.
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs. Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
""" """
with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file: with open(
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"
) as draft_file:
return call(("towncrier", "--draft"), stdout=draft_file) return call(("towncrier", "--draft"), stdout=draft_file)

View File

@ -17,7 +17,9 @@ Plugin List
=========== ===========
PyPI projects that match "pytest-\*" are considered plugins and are listed PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded. automatically together with a manually-maintained list in `the source
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Packages classified as inactive are excluded.
.. The following conditional uses a different format for this list when .. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the creating a PDF, because otherwise the table gets far too wide for the
@ -33,6 +35,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
"Development Status :: 7 - Inactive", "Development Status :: 7 - Inactive",
) )
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
}
def escape_rst(text: str) -> str: def escape_rst(text: str) -> str:
@ -52,18 +57,18 @@ def iter_plugins():
regex = r">([\d\w-]*)</a>" regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple") response = requests.get("https://pypi.org/simple")
matches = list( match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
match plugin_names = [
for match in re.finditer(regex, response.text) name
if match.groups()[0].startswith("pytest-") for name in match_names
) if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
]
for match in tqdm(matches, smoothing=0): for name in tqdm(plugin_names, smoothing=0):
name = match.groups()[0]
response = requests.get(f"https://pypi.org/pypi/{name}/json") response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404: if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
# return 404 on the JSON API. Skip. # but return 404 on the JSON API. Skip.
continue continue
response.raise_for_status() response.raise_for_status()
info = response.json()["info"] info = response.json()["info"]

View File

@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
url = https://docs.pytest.org/en/latest/ url = https://docs.pytest.org/en/latest/
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
license = MIT license = MIT
license_file = LICENSE license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32 platforms = unix, linux, osx, cygwin, win32
classifiers = classifiers =
Development Status :: 6 - Mature Development Status :: 6 - Mature
@ -17,11 +17,11 @@ classifiers =
Operating System :: POSIX Operating System :: POSIX
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing Topic :: Software Development :: Testing
Topic :: Utilities Topic :: Utilities
@ -49,9 +49,8 @@ install_requires =
pluggy>=0.12,<2.0 pluggy>=0.12,<2.0
colorama;sys_platform=="win32" colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11" exceptiongroup>=1.0.0rc8;python_version<"3.11"
importlib-metadata>=0.12;python_version<"3.8"
tomli>=1.0.0;python_version<"3.11" tomli>=1.0.0;python_version<"3.11"
python_requires = >=3.7 python_requires = >=3.8
package_dir = package_dir =
=src =src
setup_requires = setup_requires =
@ -73,6 +72,7 @@ testing =
nose nose
pygments>=2.7.2 pygments>=2.7.2
requests requests
setuptools
xmlschema xmlschema
[options.package_data] [options.package_data]

View File

@ -17,21 +17,23 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import ClassVar from typing import ClassVar
from typing import Dict from typing import Dict
from typing import Final
from typing import final
from typing import Generic from typing import Generic
from typing import Iterable from typing import Iterable
from typing import List from typing import List
from typing import Literal
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import overload from typing import overload
from typing import Pattern from typing import Pattern
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import SupportsIndex
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
from weakref import ref
import pluggy import pluggy
@ -43,22 +45,16 @@ from _pytest._code.source import Source
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import final
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
if TYPE_CHECKING:
from typing_extensions import Literal
from typing_extensions import SupportsIndex
from weakref import ReferenceType
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
if sys.version_info[:2] < (3, 11): if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup from exceptiongroup import BaseExceptionGroup
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
class Code: class Code:
"""Wrapper around Python code objects.""" """Wrapper around Python code objects."""
@ -194,25 +190,25 @@ class Frame:
class TracebackEntry: class TracebackEntry:
"""A single entry in a Traceback.""" """A single entry in a Traceback."""
__slots__ = ("_rawentry", "_excinfo", "_repr_style") __slots__ = ("_rawentry", "_repr_style")
def __init__( def __init__(
self, self,
rawentry: TracebackType, rawentry: TracebackType,
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, repr_style: Optional['Literal["short", "long"]'] = None,
) -> None: ) -> None:
self._rawentry = rawentry self._rawentry: "Final" = rawentry
self._excinfo = excinfo self._repr_style: "Final" = repr_style
self._repr_style: Optional['Literal["short", "long"]'] = None
def with_repr_style(
self, repr_style: Optional['Literal["short", "long"]']
) -> "TracebackEntry":
return TracebackEntry(self._rawentry, repr_style)
@property @property
def lineno(self) -> int: def lineno(self) -> int:
return self._rawentry.tb_lineno - 1 return self._rawentry.tb_lineno - 1
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
assert mode in ("short", "long")
self._repr_style = mode
@property @property
def frame(self) -> Frame: def frame(self) -> Frame:
return Frame(self._rawentry.tb_frame) return Frame(self._rawentry.tb_frame)
@ -272,7 +268,7 @@ class TracebackEntry:
source = property(getsource) source = property(getsource)
def ishidden(self) -> bool: def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
"""Return True if the current frame has a var __tracebackhide__ """Return True if the current frame has a var __tracebackhide__
resolving to True. resolving to True.
@ -296,7 +292,7 @@ class TracebackEntry:
else: else:
break break
if tbh and callable(tbh): if tbh and callable(tbh):
return tbh(None if self._excinfo is None else self._excinfo()) return tbh(excinfo)
return tbh return tbh
def __str__(self) -> str: def __str__(self) -> str:
@ -329,16 +325,14 @@ class Traceback(List[TracebackEntry]):
def __init__( def __init__(
self, self,
tb: Union[TracebackType, Iterable[TracebackEntry]], tb: Union[TracebackType, Iterable[TracebackEntry]],
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
) -> None: ) -> None:
"""Initialize from given python traceback object and ExceptionInfo.""" """Initialize from given python traceback object and ExceptionInfo."""
self._excinfo = excinfo
if isinstance(tb, TracebackType): if isinstance(tb, TracebackType):
def f(cur: TracebackType) -> Iterable[TracebackEntry]: def f(cur: TracebackType) -> Iterable[TracebackEntry]:
cur_: Optional[TracebackType] = cur cur_: Optional[TracebackType] = cur
while cur_ is not None: while cur_ is not None:
yield TracebackEntry(cur_, excinfo=excinfo) yield TracebackEntry(cur_)
cur_ = cur_.tb_next cur_ = cur_.tb_next
super().__init__(f(tb)) super().__init__(f(tb))
@ -378,7 +372,7 @@ class Traceback(List[TracebackEntry]):
continue continue
if firstlineno is not None and x.frame.code.firstlineno != firstlineno: if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
continue continue
return Traceback(x._rawentry, self._excinfo) return Traceback(x._rawentry)
return self return self
@overload @overload
@ -398,27 +392,27 @@ class Traceback(List[TracebackEntry]):
return super().__getitem__(key) return super().__getitem__(key)
def filter( def filter(
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() self,
# TODO(py38): change to positional only.
_excinfo_or_fn: Union[
"ExceptionInfo[BaseException]",
Callable[[TracebackEntry], bool],
],
) -> "Traceback": ) -> "Traceback":
"""Return a Traceback instance with certain items removed """Return a Traceback instance with certain items removed.
fn is a function that gets a single argument, a TracebackEntry If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
instance, and should return True when the item should be added which are hidden (see ishidden() above).
to the Traceback, False when not.
By default this removes all the TracebackEntries which are hidden Otherwise, the filter is a function that gets a single argument, a
(see ishidden() above). ``TracebackEntry`` instance, and should return True when the item should
be added to the ``Traceback``, False when not.
""" """
return Traceback(filter(fn, self), self._excinfo) if isinstance(_excinfo_or_fn, ExceptionInfo):
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
def getcrashentry(self) -> Optional[TracebackEntry]: else:
"""Return last non-hidden traceback entry that lead to the exception of fn = _excinfo_or_fn
a traceback, or None if all hidden.""" return Traceback(filter(fn, self))
for i in range(-1, -len(self) - 1, -1):
entry = self[i]
if not entry.ishidden():
return entry
return None
def recursionindex(self) -> Optional[int]: def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if """Return the index of the frame/TracebackEntry where recursion originates if
@ -583,7 +577,7 @@ class ExceptionInfo(Generic[E]):
def traceback(self) -> Traceback: def traceback(self) -> Traceback:
"""The traceback.""" """The traceback."""
if self._traceback is None: if self._traceback is None:
self._traceback = Traceback(self.tb, excinfo=ref(self)) self._traceback = Traceback(self.tb)
return self._traceback return self._traceback
@traceback.setter @traceback.setter
@ -623,19 +617,24 @@ class ExceptionInfo(Generic[E]):
return isinstance(self.value, exc) return isinstance(self.value, exc)
def _getreprcrash(self) -> Optional["ReprFileLocation"]: def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True) # Find last non-hidden traceback entry that led to the exception of the
entry = self.traceback.getcrashentry() # traceback, or None if all hidden.
if entry is None: for i in range(-1, -len(self.traceback) - 1, -1):
return None entry = self.traceback[i]
path, lineno = entry.frame.code.raw.co_filename, entry.lineno if not entry.ishidden(self):
return ReprFileLocation(path, lineno + 1, exconly) path, lineno = entry.frame.code.raw.co_filename, entry.lineno
exconly = self.exconly(tryshort=True)
return ReprFileLocation(path, lineno + 1, exconly)
return None
def getrepr( def getrepr(
self, self,
showlocals: bool = False, showlocals: bool = False,
style: "_TracebackStyle" = "long", style: _TracebackStyle = "long",
abspath: bool = False, abspath: bool = False,
tbfilter: bool = True, tbfilter: Union[
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
] = True,
funcargs: bool = False, funcargs: bool = False,
truncate_locals: bool = True, truncate_locals: bool = True,
chain: bool = True, chain: bool = True,
@ -652,9 +651,15 @@ class ExceptionInfo(Generic[E]):
:param bool abspath: :param bool abspath:
If paths should be changed to absolute or left unchanged. If paths should be changed to absolute or left unchanged.
:param bool tbfilter: :param tbfilter:
Hide entries that contain a local variable ``__tracebackhide__==True``. A filter for traceback entries.
Ignored if ``style=="native"``.
* If false, don't hide any entries.
* If true, hide internal entries and entries that contain a local
variable ``__tracebackhide__ = True``.
* If a callable, delegates the filtering to the callable.
Ignored if ``style`` is ``"native"``.
:param bool funcargs: :param bool funcargs:
Show fixtures ("funcargs" for legacy purposes) per traceback entry. Show fixtures ("funcargs" for legacy purposes) per traceback entry.
@ -717,9 +722,9 @@ class FormattedExcinfo:
fail_marker: ClassVar = "E" fail_marker: ClassVar = "E"
showlocals: bool = False showlocals: bool = False
style: "_TracebackStyle" = "long" style: _TracebackStyle = "long"
abspath: bool = True abspath: bool = True
tbfilter: bool = True tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
funcargs: bool = False funcargs: bool = False
truncate_locals: bool = True truncate_locals: bool = True
chain: bool = True chain: bool = True
@ -881,8 +886,10 @@ class FormattedExcinfo:
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
traceback = excinfo.traceback traceback = excinfo.traceback
if self.tbfilter: if callable(self.tbfilter):
traceback = traceback.filter() traceback = self.tbfilter(excinfo)
elif self.tbfilter:
traceback = traceback.filter(excinfo)
if isinstance(excinfo.value, RecursionError): if isinstance(excinfo.value, RecursionError):
traceback, extraline = self._truncate_recursive_traceback(traceback) traceback, extraline = self._truncate_recursive_traceback(traceback)
@ -1080,7 +1087,7 @@ class ReprExceptionInfo(ExceptionRepr):
class ReprTraceback(TerminalRepr): class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str] extraline: Optional[str]
style: "_TracebackStyle" style: _TracebackStyle
entrysep: ClassVar = "_ " entrysep: ClassVar = "_ "
@ -1114,7 +1121,7 @@ class ReprTracebackNative(ReprTraceback):
class ReprEntryNative(TerminalRepr): class ReprEntryNative(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
style: ClassVar["_TracebackStyle"] = "native" style: ClassVar[_TracebackStyle] = "native"
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
tw.write("".join(self.lines)) tw.write("".join(self.lines))
@ -1126,7 +1133,7 @@ class ReprEntry(TerminalRepr):
reprfuncargs: Optional["ReprFuncArgs"] reprfuncargs: Optional["ReprFuncArgs"]
reprlocals: Optional["ReprLocals"] reprlocals: Optional["ReprLocals"]
reprfileloc: Optional["ReprFileLocation"] reprfileloc: Optional["ReprFileLocation"]
style: "_TracebackStyle" style: _TracebackStyle
def _write_entry_lines(self, tw: TerminalWriter) -> None: def _write_entry_lines(self, tw: TerminalWriter) -> None:
"""Write the source code portions of a list of traceback entries with syntax highlighting. """Write the source code portions of a list of traceback entries with syntax highlighting.

View File

@ -149,8 +149,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i
values: List[int] = [] values: List[int] = []
for x in ast.walk(node): for x in ast.walk(node):
if isinstance(x, (ast.stmt, ast.ExceptHandler)): if isinstance(x, (ast.stmt, ast.ExceptHandler)):
# Before Python 3.8, the lineno of a decorated class or function pointed at the decorator. # The lineno points to the class/def, so need to include the decorators.
# Since Python 3.8, the lineno points to the class/def, so need to include the decorators.
if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
for d in x.decorator_list: for d in x.decorator_list:
values.append(d.lineno - 1) values.append(d.lineno - 1)

View File

@ -2,12 +2,12 @@
import os import os
import shutil import shutil
import sys import sys
from typing import final
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
from typing import TextIO from typing import TextIO
from .wcwidth import wcswidth from .wcwidth import wcswidth
from _pytest.compat import final
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. # This code was initially copied from py 1.8.1, file _io/terminalwriter.py.

View File

@ -44,11 +44,6 @@ from _pytest.stash import StashKey
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.assertion import AssertionState from _pytest.assertion import AssertionState
if sys.version_info >= (3, 8):
namedExpr = ast.NamedExpr
else:
namedExpr = ast.Expr
assertstate_key = StashKey["AssertionState"]() assertstate_key = StashKey["AssertionState"]()
@ -680,9 +675,9 @@ class AssertionRewriter(ast.NodeVisitor):
if ( if (
expect_docstring expect_docstring
and isinstance(item, ast.Expr) and isinstance(item, ast.Expr)
and isinstance(item.value, ast.Str) and isinstance(item.value, ast.Constant)
): ):
doc = item.value.s doc = item.value.value
if self.is_rewrite_disabled(doc): if self.is_rewrite_disabled(doc):
return return
expect_docstring = False expect_docstring = False
@ -814,7 +809,7 @@ class AssertionRewriter(ast.NodeVisitor):
current = self.stack.pop() current = self.stack.pop()
if self.stack: if self.stack:
self.explanation_specifiers = self.stack[-1] self.explanation_specifiers = self.stack[-1]
keys = [ast.Str(key) for key in current.keys()] keys = [ast.Constant(key) for key in current.keys()]
format_dict = ast.Dict(keys, list(current.values())) format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict) form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter)) name = "@py_format" + str(next(self.variable_counter))
@ -868,16 +863,16 @@ class AssertionRewriter(ast.NodeVisitor):
negation = ast.UnaryOp(ast.Not(), top_condition) negation = ast.UnaryOp(ast.Not(), top_condition)
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
msg = self.pop_format_context(ast.Str(explanation)) msg = self.pop_format_context(ast.Constant(explanation))
# Failed # Failed
if assert_.msg: if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg) assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert " gluestr = "\n>assert "
else: else:
assertmsg = ast.Str("") assertmsg = ast.Constant("")
gluestr = "assert " gluestr = "assert "
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg) err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
err_name = ast.Name("AssertionError", ast.Load()) err_name = ast.Name("AssertionError", ast.Load())
fmt = self.helper("_format_explanation", err_msg) fmt = self.helper("_format_explanation", err_msg)
@ -893,8 +888,8 @@ class AssertionRewriter(ast.NodeVisitor):
hook_call_pass = ast.Expr( hook_call_pass = ast.Expr(
self.helper( self.helper(
"_call_assertion_pass", "_call_assertion_pass",
ast.Num(assert_.lineno), ast.Constant(assert_.lineno),
ast.Str(orig), ast.Constant(orig),
fmt_pass, fmt_pass,
) )
) )
@ -913,7 +908,7 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [ variables = [
ast.Name(name, ast.Store()) for name in self.format_variables ast.Name(name, ast.Store()) for name in self.format_variables
] ]
clear_format = ast.Assign(variables, ast.NameConstant(None)) clear_format = ast.Assign(variables, ast.Constant(None))
self.statements.append(clear_format) self.statements.append(clear_format)
else: # Original assertion rewriting else: # Original assertion rewriting
@ -924,9 +919,9 @@ class AssertionRewriter(ast.NodeVisitor):
assertmsg = self.helper("_format_assertmsg", assert_.msg) assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation explanation = "\n>assert " + explanation
else: else:
assertmsg = ast.Str("") assertmsg = ast.Constant("")
explanation = "assert " + explanation explanation = "assert " + explanation
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
msg = self.pop_format_context(template) msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg) fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load()) err_name = ast.Name("AssertionError", ast.Load())
@ -938,7 +933,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Clear temporary variables by setting them to None. # Clear temporary variables by setting them to None.
if self.variables: if self.variables:
variables = [ast.Name(name, ast.Store()) for name in self.variables] variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, ast.NameConstant(None)) clear = ast.Assign(variables, ast.Constant(None))
self.statements.append(clear) self.statements.append(clear)
# Fix locations (line numbers/column offsets). # Fix locations (line numbers/column offsets).
for stmt in self.statements: for stmt in self.statements:
@ -946,26 +941,26 @@ class AssertionRewriter(ast.NodeVisitor):
ast.copy_location(node, assert_) ast.copy_location(node, assert_)
return self.statements return self.statements
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]: def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]:
# This method handles the 'walrus operator' repr of the target # This method handles the 'walrus operator' repr of the target
# name if it's a local variable or _should_repr_global_name() # name if it's a local variable or _should_repr_global_name()
# thinks it's acceptable. # thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], []) locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined] target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs]) inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name) dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(target_id)) expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
return name, self.explanation_param(expr) return name, self.explanation_param(expr)
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or # Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable. # _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], []) locs = ast.Call(self.builtin("locals"), [], [])
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name) dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
return name, self.explanation_param(expr) return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@ -984,10 +979,10 @@ class AssertionRewriter(ast.NodeVisitor):
# cond is set in a prior loop iteration below # cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts = fail_inner self.expl_stmts = fail_inner
# Check if the left operand is a namedExpr and the value has already been visited # Check if the left operand is a ast.NamedExpr and the value has already been visited
if ( if (
isinstance(v, ast.Compare) isinstance(v, ast.Compare)
and isinstance(v.left, namedExpr) and isinstance(v.left, ast.NamedExpr)
and v.left.target.id and v.left.target.id
in [ in [
ast_expr.id ast_expr.id
@ -996,12 +991,14 @@ class AssertionRewriter(ast.NodeVisitor):
] ]
): ):
pytest_temp = self.variable() pytest_temp = self.variable()
self.variables_overwrite[v.left.target.id] = pytest_temp self.variables_overwrite[
v.left.target.id
] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp v.left.target.id = pytest_temp
self.push_format_context() self.push_format_context()
res, expl = self.visit(v) res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
expl_format = self.pop_format_context(ast.Str(expl)) expl_format = self.pop_format_context(ast.Constant(expl))
call = ast.Call(app, [expl_format], []) call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call)) self.expl_stmts.append(ast.Expr(call))
if i < levels: if i < levels:
@ -1013,7 +1010,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.statements = body = inner self.statements = body = inner
self.statements = save self.statements = save
self.expl_stmts = fail_save self.expl_stmts = fail_save
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or)) expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
expl = self.pop_format_context(expl_template) expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl) return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
@ -1037,10 +1034,19 @@ class AssertionRewriter(ast.NodeVisitor):
new_args = [] new_args = []
new_kwargs = [] new_kwargs = []
for arg in call.args: for arg in call.args:
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
res, expl = self.visit(arg) res, expl = self.visit(arg)
arg_expls.append(expl) arg_expls.append(expl)
new_args.append(res) new_args.append(res)
for keyword in call.keywords: for keyword in call.keywords:
if (
isinstance(keyword.value, ast.Name)
and keyword.value.id in self.variables_overwrite
):
keyword.value = self.variables_overwrite[
keyword.value.id
] # type:ignore[assignment]
res, expl = self.visit(keyword.value) res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res)) new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg: if keyword.arg:
@ -1075,7 +1081,13 @@ class AssertionRewriter(ast.NodeVisitor):
self.push_format_context() self.push_format_context()
# We first check if we have overwritten a variable in the previous assert # We first check if we have overwritten a variable in the previous assert
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite: if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
comp.left.id = self.variables_overwrite[comp.left.id] comp.left = self.variables_overwrite[
comp.left.id
] # type:ignore[assignment]
if isinstance(comp.left, ast.NamedExpr):
self.variables_overwrite[
comp.left.target.id
] = comp.left # type:ignore[assignment]
left_res, left_expl = self.visit(comp.left) left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)): if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})" left_expl = f"({left_expl})"
@ -1088,20 +1100,22 @@ class AssertionRewriter(ast.NodeVisitor):
results = [left_res] results = [left_res]
for i, op, next_operand in it: for i, op, next_operand in it:
if ( if (
isinstance(next_operand, namedExpr) isinstance(next_operand, ast.NamedExpr)
and isinstance(left_res, ast.Name) and isinstance(left_res, ast.Name)
and next_operand.target.id == left_res.id and next_operand.target.id == left_res.id
): ):
next_operand.target.id = self.variable() next_operand.target.id = self.variable()
self.variables_overwrite[left_res.id] = next_operand.target.id self.variables_overwrite[
left_res.id
] = next_operand # type:ignore[assignment]
next_res, next_expl = self.visit(next_operand) next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)): if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})" next_expl = f"({next_expl})"
results.append(next_res) results.append(next_res)
sym = BINOP_MAP[op.__class__] sym = BINOP_MAP[op.__class__]
syms.append(ast.Str(sym)) syms.append(ast.Constant(sym))
expl = f"{left_expl} {sym} {next_expl}" expl = f"{left_expl} {sym} {next_expl}"
expls.append(ast.Str(expl)) expls.append(ast.Constant(expl))
res_expr = ast.Compare(left_res, [op], [next_res]) res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr)) self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl left_res, left_expl = next_res, next_expl
@ -1145,7 +1159,7 @@ def try_makedirs(cache_dir: Path) -> bool:
def get_cache_dir(file_path: Path) -> Path: def get_cache_dir(file_path: Path) -> Path:
"""Return the cache directory to write .pyc files for the given .py file path.""" """Return the cache directory to write .pyc files for the given .py file path."""
if sys.version_info >= (3, 8) and sys.pycache_prefix: if sys.pycache_prefix:
# given: # given:
# prefix = '/tmp/pycs' # prefix = '/tmp/pycs'
# path = '/home/user/proj/test_app.py' # path = '/home/user/proj/test_app.py'

View File

@ -6,6 +6,7 @@ import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import Iterable from typing import Iterable
from typing import List from typing import List
@ -18,7 +19,6 @@ from .pathlib import rm_rf
from .reports import CollectReport from .reports import CollectReport
from _pytest import nodes from _pytest import nodes
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
@ -27,7 +27,7 @@ from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.main import Session from _pytest.main import Session
from _pytest.python import Module from _pytest.nodes import File
from _pytest.python import Package from _pytest.python import Package
from _pytest.reports import TestReport from _pytest.reports import TestReport
@ -179,16 +179,22 @@ class Cache:
else: else:
cache_dir_exists_already = self._cachedir.exists() cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True) path.parent.mkdir(exist_ok=True, parents=True)
except OSError: except OSError as exc:
self.warn("could not create cache path {path}", path=path, _ispytest=True) self.warn(
f"could not create cache path {path}: {exc}",
_ispytest=True,
)
return return
if not cache_dir_exists_already: if not cache_dir_exists_already:
self._ensure_supporting_files() self._ensure_supporting_files()
data = json.dumps(value, ensure_ascii=False, indent=2) data = json.dumps(value, ensure_ascii=False, indent=2)
try: try:
f = path.open("w", encoding="UTF-8") f = path.open("w", encoding="UTF-8")
except OSError: except OSError as exc:
self.warn("cache could not write path {path}", path=path, _ispytest=True) self.warn(
f"cache could not write path {path}: {exc}",
_ispytest=True,
)
else: else:
with f: with f:
f.write(data) f.write(data)
@ -213,22 +219,30 @@ class LFPluginCollWrapper:
@hookimpl(hookwrapper=True) @hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector): def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, Session): if isinstance(collector, (Session, Package)):
out = yield out = yield
res: CollectReport = out.get_result() res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning. # Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths lf_paths = self.lfplugin._last_failed_paths
# Use stable sort to priorize last failed.
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
# Package.path is the __init__.py file, we need the directory.
if isinstance(node, Package):
path = node.path.parent
else:
path = node.path
return path in lf_paths
res.result = sorted( res.result = sorted(
res.result, res.result,
# use stable sort to priorize last failed key=sort_key,
key=lambda x: x.path in lf_paths,
reverse=True, reverse=True,
) )
return return
elif isinstance(collector, Module): elif isinstance(collector, File):
if collector.path in self.lfplugin._last_failed_paths: if collector.path in self.lfplugin._last_failed_paths:
out = yield out = yield
res = out.get_result() res = out.get_result()
@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
def pytest_make_collect_report( def pytest_make_collect_report(
self, collector: nodes.Collector self, collector: nodes.Collector
) -> Optional[CollectReport]: ) -> Optional[CollectReport]:
# Packages are Modules, but _last_failed_paths only contains # Packages are Files, but we only want to skip test-bearing Files,
# test-bearing paths and doesn't try to include the paths of their # so don't filter Packages.
# packages, so don't filter them. if isinstance(collector, File) and not isinstance(collector, Package):
if isinstance(collector, Module) and not isinstance(collector, Package):
if collector.path not in self.lfplugin._last_failed_paths: if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1 self.lfplugin._skipped_files += 1
@ -299,9 +312,14 @@ class LFPlugin:
) )
def get_last_failed_paths(self) -> Set[Path]: def get_last_failed_paths(self) -> Set[Path]:
"""Return a set with all Paths()s of the previously failed nodeids.""" """Return a set with all Paths of the previously failed nodeids and
their parents."""
rootpath = self.config.rootpath rootpath = self.config.rootpath
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed} result = set()
for nodeid in self.lastfailed:
path = rootpath / nodeid.split("::")[0]
result.add(path)
result.update(path.parents)
return {x for x in result if x.exists()} return {x for x in result if x.exists()}
def pytest_report_collectionfinish(self) -> Optional[str]: def pytest_report_collectionfinish(self) -> Optional[str]:

View File

@ -11,11 +11,14 @@ from types import TracebackType
from typing import Any from typing import Any
from typing import AnyStr from typing import AnyStr
from typing import BinaryIO from typing import BinaryIO
from typing import Final
from typing import final
from typing import Generator from typing import Generator
from typing import Generic from typing import Generic
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
from typing import Literal
from typing import NamedTuple from typing import NamedTuple
from typing import Optional from typing import Optional
from typing import TextIO from typing import TextIO
@ -24,7 +27,6 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
from _pytest.compat import final
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
@ -35,11 +37,7 @@ from _pytest.nodes import Collector
from _pytest.nodes import File from _pytest.nodes import File
from _pytest.nodes import Item from _pytest.nodes import Item
if TYPE_CHECKING: _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
from typing_extensions import Final
from typing_extensions import Literal
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
def pytest_addoption(parser: Parser) -> None: def pytest_addoption(parser: Parser) -> None:
@ -241,7 +239,7 @@ class DontReadFromInput(TextIO):
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()") raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: Optional[int] = None) -> int: def truncate(self, size: Optional[int] = None) -> int:
raise UnsupportedOperation("cannont truncate stdin") raise UnsupportedOperation("cannot truncate stdin")
def write(self, data: str) -> int: def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin") raise UnsupportedOperation("cannot write to stdin")
@ -687,7 +685,7 @@ class MultiCapture(Generic[AnyStr]):
return CaptureResult(out, err) # type: ignore[arg-type] return CaptureResult(out, err) # type: ignore[arg-type]
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
if method == "fd": if method == "fd":
return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
elif method == "sys": elif method == "sys":
@ -723,7 +721,7 @@ class CaptureManager:
needed to ensure the fixtures take precedence over the global capture. needed to ensure the fixtures take precedence over the global capture.
""" """
def __init__(self, method: "_CaptureMethod") -> None: def __init__(self, method: _CaptureMethod) -> None:
self._method: Final = method self._method: Final = method
self._global_capturing: Optional[MultiCapture[str]] = None self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None self._capture_fixture: Optional[CaptureFixture[Any]] = None

View File

@ -12,26 +12,12 @@ from inspect import signature
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Generic from typing import Final
from typing import NoReturn from typing import NoReturn
from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
import py import py
# fmt: off
# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
# If `overload` is imported from `compat` instead of from `typing`,
# Sphinx doesn't recognize it as `overload` and the API docs for
# overloaded functions look good again. But type checkers handle
# it fine.
# fmt: on
if True:
from typing import overload as overload
if TYPE_CHECKING:
from typing_extensions import Final
_T = TypeVar("_T") _T = TypeVar("_T")
_S = TypeVar("_S") _S = TypeVar("_S")
@ -58,17 +44,6 @@ class NotSetType(enum.Enum):
NOTSET: Final = NotSetType.token # noqa: E305 NOTSET: Final = NotSetType.token # noqa: E305
# fmt: on # fmt: on
if sys.version_info >= (3, 8):
import importlib.metadata
importlib_metadata = importlib.metadata
else:
import importlib_metadata as importlib_metadata # noqa: F401
def _format_args(func: Callable[..., Any]) -> str:
return str(signature(func))
def is_generator(func: object) -> bool: def is_generator(func: object) -> bool:
genfunc = inspect.isgeneratorfunction(func) genfunc = inspect.isgeneratorfunction(func)
@ -338,47 +313,6 @@ def safe_isclass(obj: object) -> bool:
return False return False
if TYPE_CHECKING:
if sys.version_info >= (3, 8):
from typing import final as final
else:
from typing_extensions import final as final
elif sys.version_info >= (3, 8):
from typing import final as final
else:
def final(f):
return f
if sys.version_info >= (3, 8):
from functools import cached_property as cached_property
else:
class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__")
def __init__(self, func: Callable[[_S], _T]) -> None:
self.func = func
self.__doc__ = func.__doc__
@overload
def __get__(
self, instance: None, owner: type[_S] | None = ...
) -> cached_property[_S, _T]:
...
@overload
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
...
def __get__(self, instance, owner=None):
if instance is None:
return self
value = instance.__dict__[self.func.__name__] = self.func(instance)
return value
def get_user_id() -> int | None: def get_user_id() -> int | None:
"""Return the current user id, or None if we cannot get it reliably on the current platform.""" """Return the current user id, or None if we cannot get it reliably on the current platform."""
# win32 does not have a getuid() function. # win32 does not have a getuid() function.

View File

@ -5,6 +5,7 @@ import copy
import dataclasses import dataclasses
import enum import enum
import glob import glob
import importlib.metadata
import inspect import inspect
import os import os
import re import re
@ -21,6 +22,7 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import IO from typing import IO
from typing import Iterable from typing import Iterable
@ -48,8 +50,6 @@ from .findpaths import determine_setup
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import importlib_metadata
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@ -257,7 +257,8 @@ default_plugins = essential_plugins + (
"logging", "logging",
"reports", "reports",
"python_path", "python_path",
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), "unraisableexception",
"threadexception",
"faulthandler", "faulthandler",
) )
@ -527,9 +528,12 @@ class PytestPluginManager(PluginManager):
# #
def _set_initial_conftests( def _set_initial_conftests(
self, self,
namespace: argparse.Namespace, args: Sequence[Union[str, Path]],
pyargs: bool,
noconftest: bool,
rootpath: Path, rootpath: Path,
testpaths_ini: Sequence[str], confcutdir: Optional[Path],
importmode: Union[ImportMode, str],
) -> None: ) -> None:
"""Load initial conftest files given a preparsed "namespace". """Load initial conftest files given a preparsed "namespace".
@ -539,17 +543,12 @@ class PytestPluginManager(PluginManager):
common options will not confuse our logic here. common options will not confuse our logic here.
""" """
current = Path.cwd() current = Path.cwd()
self._confcutdir = ( self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
absolutepath(current / namespace.confcutdir) self._noconftest = noconftest
if namespace.confcutdir self._using_pyargs = pyargs
else None
)
self._noconftest = namespace.noconftest
self._using_pyargs = namespace.pyargs
testpaths = namespace.file_or_dir + testpaths_ini
foundanchor = False foundanchor = False
for testpath in testpaths: for intitial_path in args:
path = str(testpath) path = str(intitial_path)
# remove node-id syntax # remove node-id syntax
i = path.find("::") i = path.find("::")
if i != -1: if i != -1:
@ -563,10 +562,10 @@ class PytestPluginManager(PluginManager):
except OSError: # pragma: no cover except OSError: # pragma: no cover
anchor_exists = False anchor_exists = False
if anchor_exists: if anchor_exists:
self._try_load_conftest(anchor, namespace.importmode, rootpath) self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True foundanchor = True
if not foundanchor: if not foundanchor:
self._try_load_conftest(current, namespace.importmode, rootpath) self._try_load_conftest(current, importmode, rootpath)
def _is_in_confcutdir(self, path: Path) -> bool: def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether a path is within the confcutdir. """Whether a path is within the confcutdir.
@ -1140,10 +1139,25 @@ class Config:
@hookimpl(trylast=True) @hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None: def pytest_load_initial_conftests(self, early_config: "Config") -> None:
self.pluginmanager._set_initial_conftests( # We haven't fully parsed the command line arguments yet, so
early_config.known_args_namespace, # early_config.args it not set yet. But we need it for
# discovering the initial conftests. So "pre-run" the logic here.
# It will be done for real in `parse()`.
args, args_source = early_config._decide_args(
args=early_config.known_args_namespace.file_or_dir,
pyargs=early_config.known_args_namespace.pyargs,
testpaths=early_config.getini("testpaths"),
invocation_dir=early_config.invocation_params.dir,
rootpath=early_config.rootpath, rootpath=early_config.rootpath,
testpaths_ini=self.getini("testpaths"), warn=False,
)
self.pluginmanager._set_initial_conftests(
args=args,
pyargs=early_config.known_args_namespace.pyargs,
noconftest=early_config.known_args_namespace.noconftest,
rootpath=early_config.rootpath,
confcutdir=early_config.known_args_namespace.confcutdir,
importmode=early_config.known_args_namespace.importmode,
) )
def _initini(self, args: Sequence[str]) -> None: def _initini(self, args: Sequence[str]) -> None:
@ -1203,7 +1217,7 @@ class Config:
package_files = ( package_files = (
str(file) str(file)
for dist in importlib_metadata.distributions() for dist in importlib.metadata.distributions()
if any(ep.group == "pytest11" for ep in dist.entry_points) if any(ep.group == "pytest11" for ep in dist.entry_points)
for file in dist.files or [] for file in dist.files or []
) )
@ -1223,6 +1237,49 @@ class Config:
return args return args
def _decide_args(
self,
*,
args: List[str],
pyargs: List[str],
testpaths: List[str],
invocation_dir: Path,
rootpath: Path,
warn: bool,
) -> Tuple[List[str], ArgsSource]:
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
:param warn: Whether can issue warnings.
"""
if args:
source = Config.ArgsSource.ARGS
result = args
else:
if invocation_dir == rootpath:
source = Config.ArgsSource.TESTPATHS
if pyargs:
result = testpaths
else:
result = []
for path in testpaths:
result.extend(sorted(glob.iglob(path, recursive=True)))
if testpaths and not result:
if warn:
warning_text = (
"No files were found in testpaths; "
"consider removing or adjusting your testpaths configuration. "
"Searching recursively from the current directory instead."
)
self.issue_config_time_warning(
PytestConfigWarning(warning_text), stacklevel=3
)
else:
result = []
if not result:
source = Config.ArgsSource.INCOVATION_DIR
result = [str(invocation_dir)]
return result, source
def _preparse(self, args: List[str], addopts: bool = True) -> None: def _preparse(self, args: List[str], addopts: bool = True) -> None:
if addopts: if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "") env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
@ -1261,8 +1318,11 @@ class Config:
_pytest.deprecated.STRICT_OPTION, stacklevel=2 _pytest.deprecated.STRICT_OPTION, stacklevel=2
) )
if self.known_args_namespace.confcutdir is None and self.inipath is not None: if self.known_args_namespace.confcutdir is None:
confcutdir = str(self.inipath.parent) if self.inipath is not None:
confcutdir = str(self.inipath.parent)
else:
confcutdir = str(self.rootpath)
self.known_args_namespace.confcutdir = confcutdir self.known_args_namespace.confcutdir = confcutdir
try: try:
self.hook.pytest_load_initial_conftests( self.hook.pytest_load_initial_conftests(
@ -1368,25 +1428,17 @@ class Config:
self.hook.pytest_cmdline_preparse(config=self, args=args) self.hook.pytest_cmdline_preparse(config=self, args=args)
self._parser.after_preparse = True # type: ignore self._parser.after_preparse = True # type: ignore
try: try:
source = Config.ArgsSource.ARGS
args = self._parser.parse_setoption( args = self._parser.parse_setoption(
args, self.option, namespace=self.option args, self.option, namespace=self.option
) )
if not args: self.args, self.args_source = self._decide_args(
if self.invocation_params.dir == self.rootpath: args=args,
source = Config.ArgsSource.TESTPATHS pyargs=self.known_args_namespace.pyargs,
testpaths: List[str] = self.getini("testpaths") testpaths=self.getini("testpaths"),
if self.known_args_namespace.pyargs: invocation_dir=self.invocation_params.dir,
args = testpaths rootpath=self.rootpath,
else: warn=True,
args = [] )
for path in testpaths:
args.extend(sorted(glob.iglob(path, recursive=True)))
if not args:
source = Config.ArgsSource.INCOVATION_DIR
args = [str(self.invocation_params.dir)]
self.args = args
self.args_source = source
except PrintHelp: except PrintHelp:
pass pass

View File

@ -7,6 +7,7 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import final
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import NoReturn from typing import NoReturn
@ -17,7 +18,6 @@ from typing import TYPE_CHECKING
from typing import Union from typing import Union
import _pytest._io import _pytest._io
from _pytest.compat import final
from _pytest.config.exceptions import UsageError from _pytest.config.exceptions import UsageError
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
from _pytest.deprecated import ARGUMENT_TYPE_STR from _pytest.deprecated import ARGUMENT_TYPE_STR

View File

@ -1,4 +1,4 @@
from _pytest.compat import final from typing import final
@final @final

View File

@ -122,6 +122,11 @@ HOOK_LEGACY_MARKING = UnformattedWarning(
"#configuring-hook-specs-impls-using-markers", "#configuring-hook-specs-impls-using-markers",
) )
MARKED_FIXTURE = PytestRemovedIn8Warning(
"Marks applied to fixtures have no effect\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
)
# You want to make some `__init__` or function "private". # You want to make some `__init__` or function "private".
# #
# def my_private_function(some, args): # def my_private_function(some, args):

View File

@ -13,6 +13,7 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import Generic from typing import Generic
from typing import Iterable from typing import Iterable
@ -21,6 +22,7 @@ from typing import List
from typing import MutableMapping from typing import MutableMapping
from typing import NoReturn from typing import NoReturn
from typing import Optional from typing import Optional
from typing import overload
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
@ -35,10 +37,8 @@ from _pytest._code import getfslineno
from _pytest._code.code import FormattedExcinfo from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never from _pytest.compat import assert_never
from _pytest.compat import final
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import get_real_method from _pytest.compat import get_real_method
from _pytest.compat import getfuncargnames from _pytest.compat import getfuncargnames
@ -46,12 +46,13 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import overload from _pytest.compat import NotSetType
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE
from _pytest.deprecated import YIELD_FIXTURE from _pytest.deprecated import YIELD_FIXTURE
from _pytest.mark import Mark from _pytest.mark import Mark
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
def get_scope_package(node, fixturedef: "FixtureDef[object]"): def get_scope_package(
import pytest node: nodes.Item,
fixturedef: "FixtureDef[object]",
) -> Optional[Union[nodes.Item, nodes.Collector]]:
from _pytest.python import Package
cls = pytest.Package current: Optional[Union[nodes.Item, nodes.Collector]] = node
current = node
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py") fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
while current and ( while current and (
type(current) is not cls or fixture_package_name != current.nodeid not isinstance(current, Package) or fixture_package_name != current.nodeid
): ):
current = current.parent current = current.parent # type: ignore[assignment]
if current is None: if current is None:
return node.session return node.session
return current return current
@ -434,7 +437,23 @@ class FixtureRequest:
@property @property
def node(self): def node(self):
"""Underlying collection node (depends on current request scope).""" """Underlying collection node (depends on current request scope)."""
return self._getscopeitem(self._scope) scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]": def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None) fixturedefs = self._arg2fixturedefs.get(argname, None)
@ -518,11 +537,7 @@ class FixtureRequest:
"""Add finalizer/teardown function to be called without arguments after """Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution.""" the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones. # XXX usually this method is shadowed by fixturedef specific ones.
self._addfinalizer(finalizer, scope=self.scope) self.node.addfinalizer(finalizer)
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
node = self._getscopeitem(scope)
node.addfinalizer(finalizer)
def applymarker(self, marker: Union[str, MarkDecorator]) -> None: def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation. """Apply a marker to a single test function invocation.
@ -713,32 +728,12 @@ class FixtureRequest:
p = bestrelpath(session.path, fs) p = bestrelpath(session.path, fs)
else: else:
p = fs p = fs
args = _format_args(factory) lines.append(
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) "%s:%d: def %s%s"
% (p, lineno + 1, factory.__name__, inspect.signature(factory))
)
return lines return lines
def _getscopeitem(
self, scope: Union[Scope, "_ScopeName"]
) -> Union[nodes.Item, nodes.Collector]:
if isinstance(scope, str):
scope = Scope(scope)
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def __repr__(self) -> str: def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node) return "<FixtureRequest for %r>" % (self.node)
@ -1206,6 +1201,9 @@ class FixtureFunctionMarker:
"fixture is being applied more than once to the same function" "fixture is being applied more than once to the same function"
) )
if hasattr(function, "pytestmark"):
warnings.warn(MARKED_FIXTURE, stacklevel=2)
function = wrap_function_to_error_out_if_called_directly(function, self) function = wrap_function_to_error_out_if_called_directly(function, self)
name = self.name or function.__name__ name = self.name or function.__name__
@ -1593,13 +1591,52 @@ class FixtureManager:
# Separate parametrized setups. # Separate parametrized setups.
items[:] = reorder_items(items) items[:] = reorder_items(items)
@overload
def parsefactories( def parsefactories(
self, node_or_obj, nodeid=NOTSET, unittest: bool = False self,
node_or_obj: nodes.Node,
*,
unittest: bool = ...,
) -> None: ) -> None:
raise NotImplementedError()
@overload
def parsefactories( # noqa: F811
self,
node_or_obj: object,
nodeid: Optional[str],
*,
unittest: bool = ...,
) -> None:
raise NotImplementedError()
def parsefactories( # noqa: F811
self,
node_or_obj: Union[nodes.Node, object],
nodeid: Union[str, NotSetType, None] = NOTSET,
*,
unittest: bool = False,
) -> None:
"""Collect fixtures from a collection node or object.
Found fixtures are parsed into `FixtureDef`s and saved.
If `node_or_object` is a collection node (with an underlying Python
object), the node's object is traversed and the node's nodeid is used to
determine the fixtures' visibilty. `nodeid` must not be specified in
this case.
If `node_or_object` is an object (e.g. a plugin), the object is
traversed and the given `nodeid` is used to determine the fixtures'
visibility. `nodeid` must be specified in this case; None and "" mean
total visibility.
"""
if nodeid is not NOTSET: if nodeid is not NOTSET:
holderobj = node_or_obj holderobj = node_or_obj
else: else:
holderobj = node_or_obj.obj assert isinstance(node_or_obj, nodes.Node)
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
assert isinstance(node_or_obj.nodeid, str)
nodeid = node_or_obj.nodeid nodeid = node_or_obj.nodeid
if holderobj in self._holderobjseen: if holderobj in self._holderobjseen:
return return

View File

@ -105,7 +105,7 @@ def pytest_cmdline_parse():
if config.option.debug: if config.option.debug:
# --debug | --debug <file.log> was provided. # --debug | --debug <file.log> was provided.
path = config.option.debug path = config.option.debug
debugfile = open(path, "w") debugfile = open(path, "w", encoding="utf-8")
debugfile.write( debugfile.write(
"versions pytest-%s, " "versions pytest-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n" "python-%s\ncwd=%s\nargs=%s\n\n"

View File

@ -41,6 +41,7 @@ if TYPE_CHECKING:
from _pytest.reports import TestReport from _pytest.reports import TestReport
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from _pytest.terminal import TestShortLogReport
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
@ -806,7 +807,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_report_teststatus( # type:ignore[empty-body] def pytest_report_teststatus( # type:ignore[empty-body]
report: Union["CollectReport", "TestReport"], config: "Config" report: Union["CollectReport", "TestReport"], config: "Config"
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]: ) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
"""Return result-category, shortletter and verbose word for status """Return result-category, shortletter and verbose word for status
reporting. reporting.

View File

@ -3,6 +3,8 @@ import dataclasses
import shlex import shlex
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import Final
from typing import final
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -11,7 +13,6 @@ from typing import Union
from iniconfig import SectionWrapper from iniconfig import SectionWrapper
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache
from _pytest.compat import final
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
@ -32,8 +33,6 @@ from _pytest.terminal import TerminalReporter
from _pytest.tmpdir import TempPathFactory from _pytest.tmpdir import TempPathFactory
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Final
import pexpect import pexpect

View File

@ -5,10 +5,15 @@ import os
import re import re
from contextlib import contextmanager from contextlib import contextmanager
from contextlib import nullcontext from contextlib import nullcontext
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from io import StringIO from io import StringIO
from logging import LogRecord
from pathlib import Path from pathlib import Path
from typing import AbstractSet from typing import AbstractSet
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Mapping from typing import Mapping
@ -21,7 +26,6 @@ from typing import Union
from _pytest import nodes from _pytest import nodes
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.capture import CaptureManager from _pytest.capture import CaptureManager
from _pytest.compat import final
from _pytest.config import _strtobool from _pytest.config import _strtobool
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import create_terminal_writer from _pytest.config import create_terminal_writer
@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str:
return _ANSI_ESCAPE_SEQ.sub("", text) return _ANSI_ESCAPE_SEQ.sub("", text)
class ColoredLevelFormatter(logging.Formatter): class DatetimeFormatter(logging.Formatter):
"""A logging formatter which formats record with
:func:`datetime.datetime.strftime` formatter instead of
:func:`time.strftime` in case of microseconds in format string.
"""
def formatTime(self, record: LogRecord, datefmt=None) -> str:
if datefmt and "%f" in datefmt:
ct = self.converter(record.created)
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
# Construct `datetime.datetime` object from `struct_time`
# and msecs information from `record`
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
return dt.strftime(datefmt)
# Use `logging.Formatter` for non-microsecond formats
return super().formatTime(record, datefmt)
class ColoredLevelFormatter(DatetimeFormatter):
"""A logging formatter which colorizes the %(levelname)..s part of the """A logging formatter which colorizes the %(levelname)..s part of the
log format passed to __init__.""" log format passed to __init__."""
@ -376,11 +398,12 @@ class LogCaptureFixture:
self._initial_handler_level: Optional[int] = None self._initial_handler_level: Optional[int] = None
# Dict of log name -> log level. # Dict of log name -> log level.
self._initial_logger_levels: Dict[Optional[str], int] = {} self._initial_logger_levels: Dict[Optional[str], int] = {}
self._initial_disabled_logging_level: Optional[int] = None
def _finalize(self) -> None: def _finalize(self) -> None:
"""Finalize the fixture. """Finalize the fixture.
This restores the log levels changed by :meth:`set_level`. This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
""" """
# Restore log levels. # Restore log levels.
if self._initial_handler_level is not None: if self._initial_handler_level is not None:
@ -388,6 +411,10 @@ class LogCaptureFixture:
for logger_name, level in self._initial_logger_levels.items(): for logger_name, level in self._initial_logger_levels.items():
logger = logging.getLogger(logger_name) logger = logging.getLogger(logger_name)
logger.setLevel(level) logger.setLevel(level)
# Disable logging at the original disabled logging level.
if self._initial_disabled_logging_level is not None:
logging.disable(self._initial_disabled_logging_level)
self._initial_disabled_logging_level = None
@property @property
def handler(self) -> LogCaptureHandler: def handler(self) -> LogCaptureHandler:
@ -453,13 +480,51 @@ class LogCaptureFixture:
"""Reset the list of log records and the captured log text.""" """Reset the list of log records and the captured log text."""
self.handler.clear() self.handler.clear()
def _force_enable_logging(
self, level: Union[int, str], logger_obj: logging.Logger
) -> int:
"""Enable the desired logging level if the global level was disabled via ``logging.disabled``.
Only enables logging levels greater than or equal to the requested ``level``.
Does nothing if the desired ``level`` wasn't disabled.
:param level:
The logger level caplog should capture.
All logging is enabled if a non-standard logging level string is supplied.
Valid level strings are in :data:`logging._nameToLevel`.
:param logger_obj: The logger object to check.
:return: The original disabled logging level.
"""
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
if isinstance(level, str):
# Try to translate the level string to an int for `logging.disable()`
level = logging.getLevelName(level)
if not isinstance(level, int):
# The level provided was not valid, so just un-disable all logging.
logging.disable(logging.NOTSET)
elif not logger_obj.isEnabledFor(level):
# Each level is `10` away from other levels.
# https://docs.python.org/3/library/logging.html#logging-levels
disable_level = max(level - 10, logging.NOTSET)
logging.disable(disable_level)
return original_disable_level
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None: def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
"""Set the level of a logger for the duration of a test. """Set the threshold level of a logger for the duration of a test.
Logging messages which are less severe than this level will not be captured.
.. versionchanged:: 3.4 .. versionchanged:: 3.4
The levels of the loggers changed by this function will be The levels of the loggers changed by this function will be
restored to their initial values at the end of the test. restored to their initial values at the end of the test.
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
:param level: The level. :param level: The level.
:param logger: The logger to update. If not given, the root logger. :param logger: The logger to update. If not given, the root logger.
""" """
@ -470,6 +535,9 @@ class LogCaptureFixture:
if self._initial_handler_level is None: if self._initial_handler_level is None:
self._initial_handler_level = self.handler.level self._initial_handler_level = self.handler.level
self.handler.setLevel(level) self.handler.setLevel(level)
initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
if self._initial_disabled_logging_level is None:
self._initial_disabled_logging_level = initial_disabled_logging_level
@contextmanager @contextmanager
def at_level( def at_level(
@ -479,6 +547,8 @@ class LogCaptureFixture:
the end of the 'with' statement the level is restored to its original the end of the 'with' statement the level is restored to its original
value. value.
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
:param level: The level. :param level: The level.
:param logger: The logger to update. If not given, the root logger. :param logger: The logger to update. If not given, the root logger.
""" """
@ -487,11 +557,13 @@ class LogCaptureFixture:
logger_obj.setLevel(level) logger_obj.setLevel(level)
handler_orig_level = self.handler.level handler_orig_level = self.handler.level
self.handler.setLevel(level) self.handler.setLevel(level)
original_disable_level = self._force_enable_logging(level, logger_obj)
try: try:
yield yield
finally: finally:
logger_obj.setLevel(orig_level) logger_obj.setLevel(orig_level)
self.handler.setLevel(handler_orig_level) self.handler.setLevel(handler_orig_level)
logging.disable(original_disable_level)
@fixture @fixture
@ -577,7 +649,7 @@ class LoggingPlugin:
config, "log_file_date_format", "log_date_format" config, "log_file_date_format", "log_date_format"
) )
log_file_formatter = logging.Formatter( log_file_formatter = DatetimeFormatter(
log_file_format, datefmt=log_file_date_format log_file_format, datefmt=log_file_date_format
) )
self.log_file_handler.setFormatter(log_file_formatter) self.log_file_handler.setFormatter(log_file_formatter)
@ -621,7 +693,7 @@ class LoggingPlugin:
create_terminal_writer(self._config), log_format, log_date_format create_terminal_writer(self._config), log_format, log_date_format
) )
else: else:
formatter = logging.Formatter(log_format, log_date_format) formatter = DatetimeFormatter(log_format, log_date_format)
formatter._style = PercentStyleMultiline( formatter._style = PercentStyleMultiline(
formatter._style._fmt, auto_indent=auto_indent formatter._style._fmt, auto_indent=auto_indent

View File

@ -9,10 +9,12 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import final
from typing import FrozenSet from typing import FrozenSet
from typing import Iterator from typing import Iterator
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import overload
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
@ -22,8 +24,6 @@ from typing import Union
import _pytest._code import _pytest._code
from _pytest import nodes from _pytest import nodes
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import directory_arg from _pytest.config import directory_arg
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -122,11 +122,12 @@ def pytest_addoption(parser: Parser) -> None:
) )
group._addoption( group._addoption(
"-c", "-c",
metavar="file", "--config-file",
metavar="FILE",
type=str, type=str,
dest="inifilename", dest="inifilename",
help="Load configuration from `file` instead of trying to locate one of the " help="Load configuration from `FILE` instead of trying to locate one of the "
"implicit configuration files", "implicit configuration files.",
) )
group._addoption( group._addoption(
"--continue-on-collection-errors", "--continue-on-collection-errors",
@ -399,6 +400,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
allow_in_venv = config.getoption("collect_in_virtualenv") allow_in_venv = config.getoption("collect_in_virtualenv")
if not allow_in_venv and _in_venv(collection_path): if not allow_in_venv and _in_venv(collection_path):
return True return True
if collection_path.is_dir():
norecursepatterns = config.getini("norecursedirs")
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
return True
return None return None
@ -562,9 +569,6 @@ class Session(nodes.FSCollector):
ihook = self.gethookproxy(fspath.parent) ihook = self.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True return True
def _collectfile( def _collectfile(
@ -685,8 +689,8 @@ class Session(nodes.FSCollector):
# are not collected more than once. # are not collected more than once.
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {} matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
# Dirnames of pkgs with dunder-init files. # Directories of pkgs with dunder-init files.
pkg_roots: Dict[str, Package] = {} pkg_roots: Dict[Path, Package] = {}
for argpath, names in self._initial_parts: for argpath, names in self._initial_parts:
self.trace("processing argument", (argpath, names)) self.trace("processing argument", (argpath, names))
@ -707,7 +711,7 @@ class Session(nodes.FSCollector):
col = self._collectfile(pkginit, handle_dupes=False) col = self._collectfile(pkginit, handle_dupes=False)
if col: if col:
if isinstance(col[0], Package): if isinstance(col[0], Package):
pkg_roots[str(parent)] = col[0] pkg_roots[parent] = col[0]
node_cache1[col[0].path] = [col[0]] node_cache1[col[0].path] = [col[0]]
# If it's a directory argument, recurse and look for any Subpackages. # If it's a directory argument, recurse and look for any Subpackages.
@ -716,7 +720,7 @@ class Session(nodes.FSCollector):
assert not names, f"invalid arg {(argpath, names)!r}" assert not names, f"invalid arg {(argpath, names)!r}"
seen_dirs: Set[Path] = set() seen_dirs: Set[Path] = set()
for direntry in visit(str(argpath), self._recurse): for direntry in visit(argpath, self._recurse):
if not direntry.is_file(): if not direntry.is_file():
continue continue
@ -731,8 +735,8 @@ class Session(nodes.FSCollector):
for x in self._collectfile(pkginit): for x in self._collectfile(pkginit):
yield x yield x
if isinstance(x, Package): if isinstance(x, Package):
pkg_roots[str(dirpath)] = x pkg_roots[dirpath] = x
if str(dirpath) in pkg_roots: if dirpath in pkg_roots:
# Do not collect packages here. # Do not collect packages here.
continue continue
@ -749,7 +753,7 @@ class Session(nodes.FSCollector):
if argpath in node_cache1: if argpath in node_cache1:
col = node_cache1[argpath] col = node_cache1[argpath]
else: else:
collect_root = pkg_roots.get(str(argpath.parent), self) collect_root = pkg_roots.get(argpath.parent, self)
col = collect_root._collectfile(argpath, handle_dupes=False) col = collect_root._collectfile(argpath, handle_dupes=False)
if col: if col:
node_cache1[argpath] = col node_cache1[argpath] = col

View File

@ -26,7 +26,6 @@ from typing import NoReturn
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
__all__ = [ __all__ = [
"Expression", "Expression",
"ParseError", "ParseError",
@ -132,7 +131,7 @@ IDENT_PREFIX = "$"
def expression(s: Scanner) -> ast.Expression: def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF): if s.accept(TokenType.EOF):
ret: ast.expr = ast.NameConstant(False) ret: ast.expr = ast.Constant(False)
else: else:
ret = expr(s) ret = expr(s)
s.accept(TokenType.EOF, reject=True) s.accept(TokenType.EOF, reject=True)

View File

@ -5,6 +5,7 @@ import warnings
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Collection from typing import Collection
from typing import final
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
@ -23,11 +24,11 @@ from typing import Union
from .._code import getfslineno from .._code import getfslineno
from ..compat import ascii_escaped from ..compat import ascii_escaped
from ..compat import final
from ..compat import NOTSET from ..compat import NOTSET
from ..compat import NotSetType from ..compat import NotSetType
from _pytest.config import Config from _pytest.config import Config
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestUnknownMarkWarning
@ -412,6 +413,12 @@ def store_mark(obj, mark: Mark) -> None:
This is used to implement the Mark declarations/decorators correctly. This is used to implement the Mark declarations/decorators correctly.
""" """
assert isinstance(mark, Mark), mark assert isinstance(mark, Mark), mark
from ..fixtures import getfixturemarker
if getfixturemarker(obj) is not None:
warnings.warn(MARKED_FIXTURE, stacklevel=2)
# Always reassign name to avoid updating pytestmark in a reference that # Always reassign name to avoid updating pytestmark in a reference that
# was only borrowed. # was only borrowed.
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark] obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]

View File

@ -5,8 +5,10 @@ import sys
import warnings import warnings
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any from typing import Any
from typing import final
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Mapping
from typing import MutableMapping from typing import MutableMapping
from typing import Optional from typing import Optional
from typing import overload from typing import overload
@ -14,7 +16,6 @@ from typing import Tuple
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
from _pytest.compat import final
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
@ -129,7 +130,7 @@ class MonkeyPatch:
def __init__(self) -> None: def __init__(self) -> None:
self._setattr: List[Tuple[object, str, object]] = [] self._setattr: List[Tuple[object, str, object]] = []
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = [] self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
self._cwd: Optional[str] = None self._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None self._savesyspath: Optional[List[str]] = None
@ -290,12 +291,13 @@ class MonkeyPatch:
self._setattr.append((target, name, oldval)) self._setattr.append((target, name, oldval))
delattr(target, name) delattr(target, name)
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None: def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
"""Set dictionary entry ``name`` to value.""" """Set dictionary entry ``name`` to value."""
self._setitem.append((dic, name, dic.get(name, notset))) self._setitem.append((dic, name, dic.get(name, notset)))
dic[name] = value # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dic[name] = value # type: ignore[index]
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None: def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
"""Delete ``name`` from dict. """Delete ``name`` from dict.
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
@ -306,7 +308,8 @@ class MonkeyPatch:
raise KeyError(name) raise KeyError(name)
else: else:
self._setitem.append((dic, name, dic.get(name, notset))) self._setitem.append((dic, name, dic.get(name, notset)))
del dic[name] # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dic[name] # type: ignore[attr-defined]
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
"""Set environment variable ``name`` to ``value``. """Set environment variable ``name`` to ``value``.
@ -401,11 +404,13 @@ class MonkeyPatch:
for dictionary, key, value in reversed(self._setitem): for dictionary, key, value in reversed(self._setitem):
if value is notset: if value is notset:
try: try:
del dictionary[key] # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dictionary[key] # type: ignore[attr-defined]
except KeyError: except KeyError:
pass # Was already deleted, so we have the desired state. pass # Was already deleted, so we have the desired state.
else: else:
dictionary[key] = value # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dictionary[key] = value # type: ignore[index]
self._setitem[:] = [] self._setitem[:] = []
if self._savesyspath is not None: if self._savesyspath is not None:
sys.path[:] = self._savesyspath sys.path[:] = self._savesyspath

View File

@ -1,5 +1,6 @@
import os import os
import warnings import warnings
from functools import cached_property
from inspect import signature from inspect import signature
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -22,7 +23,7 @@ import _pytest._code
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest.compat import cached_property from _pytest._code.code import Traceback
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
@ -432,8 +433,8 @@ class Node(metaclass=NodeMeta):
assert current is None or isinstance(current, cls) assert current is None or isinstance(current, cls)
return current return current
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
pass return excinfo.traceback
def _repr_failure_py( def _repr_failure_py(
self, self,
@ -449,10 +450,13 @@ class Node(metaclass=NodeMeta):
style = "value" style = "value"
if isinstance(excinfo.value, FixtureLookupError): if isinstance(excinfo.value, FixtureLookupError):
return excinfo.value.formatrepr() return excinfo.value.formatrepr()
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
if self.config.getoption("fulltrace", False): if self.config.getoption("fulltrace", False):
style = "long" style = "long"
tbfilter = False
else: else:
self._prunetraceback(excinfo) tbfilter = self._traceback_filter
if style == "auto": if style == "auto":
style = "long" style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it? # XXX should excinfo.getrepr record all data and toterminal() process it?
@ -483,7 +487,7 @@ class Node(metaclass=NodeMeta):
abspath=abspath, abspath=abspath,
showlocals=self.config.getoption("showlocals", False), showlocals=self.config.getoption("showlocals", False),
style=style, style=style,
tbfilter=False, # pruned already, or in --fulltrace mode. tbfilter=tbfilter,
truncate_locals=truncate_locals, truncate_locals=truncate_locals,
) )
@ -554,13 +558,14 @@ class Collector(Node):
return self._repr_failure_py(excinfo, style=tbstyle) return self._repr_failure_py(excinfo, style=tbstyle)
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
if hasattr(self, "path"): if hasattr(self, "path"):
traceback = excinfo.traceback traceback = excinfo.traceback
ntraceback = traceback.cut(path=self.path) ntraceback = traceback.cut(path=self.path)
if ntraceback == traceback: if ntraceback == traceback:
ntraceback = ntraceback.cut(excludepath=tracebackcutdir) ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
excinfo.traceback = ntraceback.filter() return excinfo.traceback.filter(excinfo)
return excinfo.traceback
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]: def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:

View File

@ -7,23 +7,12 @@ from typing import Callable
from typing import cast from typing import cast
from typing import NoReturn from typing import NoReturn
from typing import Optional from typing import Optional
from typing import Protocol
from typing import Type from typing import Type
from typing import TypeVar from typing import TypeVar
from _pytest.deprecated import KEYWORD_MSG_ARG from _pytest.deprecated import KEYWORD_MSG_ARG
TYPE_CHECKING = False # Avoid circular import through compat.
if TYPE_CHECKING:
from typing_extensions import Protocol
else:
# typing.Protocol is only available starting from Python 3.8. It is also
# available from typing_extensions, but we don't want a runtime dependency
# on that. So use a dummy runtime implementation.
from typing import Generic
Protocol = Generic
class OutcomeException(BaseException): class OutcomeException(BaseException):
"""OutcomeException and its subclass instances indicate and contain info """OutcomeException and its subclass instances indicate and contain info

View File

@ -27,6 +27,7 @@ from typing import Callable
from typing import Dict from typing import Dict
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List
from typing import Optional from typing import Optional
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
@ -522,6 +523,8 @@ def import_path(
if mode is ImportMode.importlib: if mode is ImportMode.importlib:
module_name = module_name_from_path(path, root) module_name = module_name_from_path(path, root)
with contextlib.suppress(KeyError):
return sys.modules[module_name]
for meta_importer in sys.meta_path: for meta_importer in sys.meta_path:
spec = meta_importer.find_spec(module_name, [str(path.parent)]) spec = meta_importer.find_spec(module_name, [str(path.parent)])
@ -632,6 +635,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
otherwise "src.tests.test_foo" is not importable by ``__import__``. otherwise "src.tests.test_foo" is not importable by ``__import__``.
""" """
module_parts = module_name.split(".") module_parts = module_name.split(".")
child_module: Union[ModuleType, None] = None
module: Union[ModuleType, None] = None
child_name: str = ""
while module_name: while module_name:
if module_name not in modules: if module_name not in modules:
try: try:
@ -641,13 +647,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
# ourselves to fall back to creating a dummy module. # ourselves to fall back to creating a dummy module.
if not sys.meta_path: if not sys.meta_path:
raise ModuleNotFoundError raise ModuleNotFoundError
importlib.import_module(module_name) module = importlib.import_module(module_name)
except ModuleNotFoundError: except ModuleNotFoundError:
module = ModuleType( module = ModuleType(
module_name, module_name,
doc="Empty module created by pytest's importmode=importlib.", doc="Empty module created by pytest's importmode=importlib.",
) )
else:
module = modules[module_name]
if child_module:
# Add child attribute to the parent that can reference the child
# modules.
if not hasattr(module, child_name):
setattr(module, child_name, child_module)
modules[module_name] = module modules[module_name] = module
# Keep track of the child module while moving up the tree.
child_module, child_name = module, module_name.rpartition(".")[-1]
module_parts.pop(-1) module_parts.pop(-1)
module_name = ".".join(module_parts) module_name = ".".join(module_parts)
@ -669,30 +684,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
return result return result
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
"""Scan a directory recursively, in breadth-first order.
The returned entries are sorted.
"""
entries = []
with os.scandir(path) as s:
# Skip entries with symlink loops and other brokenness, so the caller
# doesn't have to deal with it.
for entry in s:
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
return entries
def visit( def visit(
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool] path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
) -> Iterator["os.DirEntry[str]"]: ) -> Iterator["os.DirEntry[str]"]:
"""Walk a directory recursively, in breadth-first order. """Walk a directory recursively, in breadth-first order.
The `recurse` predicate determines whether a directory is recursed.
Entries at each directory level are sorted. Entries at each directory level are sorted.
""" """
entries = scandir(path)
# Skip entries with symlink loops and other brokenness, so the caller doesn't
# have to deal with it.
entries = []
for entry in os.scandir(path):
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
yield from entries yield from entries
for entry in entries: for entry in entries:
if entry.is_dir() and recurse(entry): if entry.is_dir() and recurse(entry):
yield from visit(entry.path, recurse) yield from visit(entry.path, recurse)

View File

@ -6,6 +6,7 @@ import collections.abc
import contextlib import contextlib
import gc import gc
import importlib import importlib
import locale
import os import os
import platform import platform
import re import re
@ -19,10 +20,13 @@ from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import Final
from typing import final
from typing import Generator from typing import Generator
from typing import IO from typing import IO
from typing import Iterable from typing import Iterable
from typing import List from typing import List
from typing import Literal
from typing import Optional from typing import Optional
from typing import overload from typing import overload
from typing import Sequence from typing import Sequence
@ -39,7 +43,6 @@ from iniconfig import SectionWrapper
from _pytest import timing from _pytest import timing
from _pytest._code import Source from _pytest._code import Source
from _pytest.capture import _get_multicapture from _pytest.capture import _get_multicapture
from _pytest.compat import final
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import NotSetType from _pytest.compat import NotSetType
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -67,11 +70,7 @@ from _pytest.reports import TestReport
from _pytest.tmpdir import TempPathFactory from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal
import pexpect import pexpect
@ -129,6 +128,7 @@ class LsofFdLeakChecker:
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True, check=True,
text=True, text=True,
encoding=locale.getpreferredencoding(False),
).stdout ).stdout
def isopen(line: str) -> bool: def isopen(line: str) -> bool:

View File

@ -15,6 +15,7 @@ from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
@ -35,11 +36,11 @@ from _pytest._code import filter_traceback
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped from _pytest.compat import ascii_escaped
from _pytest.compat import assert_never from _pytest.compat import assert_never
from _pytest.compat import final
from _pytest.compat import get_default_arg_names from _pytest.compat import get_default_arg_names
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import getimfunc from _pytest.compat import getimfunc
@ -56,7 +57,6 @@ from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import INSTANCE_COLLECTOR from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FuncFixtureInfo from _pytest.fixtures import FuncFixtureInfo
@ -667,7 +667,7 @@ class Package(Module):
config=None, config=None,
session=None, session=None,
nodeid=None, nodeid=None,
path=Optional[Path], path: Optional[Path] = None,
) -> None: ) -> None:
# NOTE: Could be just the following, but kept as-is for compat. # NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent) # nodes.FSCollector.__init__(self, fspath, parent=parent)
@ -699,14 +699,6 @@ class Package(Module):
func = partial(_call_with_optional_argument, teardown_module, self.obj) func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func) self.addfinalizer(func)
def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.gethookproxy(fspath)
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.isinitpath(path)
def _recurse(self, direntry: "os.DirEntry[str]") -> bool: def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
if direntry.name == "__pycache__": if direntry.name == "__pycache__":
return False return False
@ -714,9 +706,6 @@ class Package(Module):
ihook = self.session.gethookproxy(fspath.parent) ihook = self.session.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True return True
def _collectfile( def _collectfile(
@ -745,11 +734,13 @@ class Package(Module):
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent this_path = self.path.parent
init_module = this_path / "__init__.py"
if init_module.is_file() and path_matches_patterns( # Always collect the __init__ first.
init_module, self.config.getini("python_files") if self.session.isinitpath(self.path) or path_matches_patterns(
self.path, self.config.getini("python_files")
): ):
yield Module.from_parent(self, path=init_module) yield Module.from_parent(self, path=self.path)
pkg_prefixes: Set[Path] = set() pkg_prefixes: Set[Path] = set()
for direntry in visit(str(this_path), recurse=self._recurse): for direntry in visit(str(this_path), recurse=self._recurse):
path = Path(direntry.path) path = Path(direntry.path)
@ -1801,7 +1792,7 @@ class Function(PyobjMixin, nodes.Item):
def setup(self) -> None: def setup(self) -> None:
self._request._fillfixtures() self._request._fillfixtures()
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
code = _pytest._code.Code.from_function(get_real_func(self.obj)) code = _pytest._code.Code.from_function(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno path, firstlineno = code.path, code.firstlineno
@ -1813,14 +1804,21 @@ class Function(PyobjMixin, nodes.Item):
ntraceback = ntraceback.filter(filter_traceback) ntraceback = ntraceback.filter(filter_traceback)
if not ntraceback: if not ntraceback:
ntraceback = traceback ntraceback = traceback
ntraceback = ntraceback.filter(excinfo)
excinfo.traceback = ntraceback.filter()
# issue364: mark all but first and last frames to # issue364: mark all but first and last frames to
# only show a single-line message for each frame. # only show a single-line message for each frame.
if self.config.getoption("tbstyle", "auto") == "auto": if self.config.getoption("tbstyle", "auto") == "auto":
if len(excinfo.traceback) > 2: if len(ntraceback) > 2:
for entry in excinfo.traceback[1:-1]: ntraceback = Traceback(
entry.set_repr_style("short") entry
if i == 0 or i == len(ntraceback) - 1
else entry.with_repr_style("short")
for i, entry in enumerate(ntraceback)
)
return ntraceback
return excinfo.traceback
# TODO: Type ignored -- breaks Liskov Substitution. # TODO: Type ignored -- breaks Liskov Substitution.
def repr_failure( # type: ignore[override] def repr_failure( # type: ignore[override]

View File

@ -9,9 +9,11 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import ContextManager from typing import ContextManager
from typing import final
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import overload
from typing import Pattern from typing import Pattern
from typing import Sequence from typing import Sequence
from typing import Tuple from typing import Tuple
@ -20,17 +22,14 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import _pytest._code
from _pytest.compat import STRING_TYPES
from _pytest.outcomes import fail
if TYPE_CHECKING: if TYPE_CHECKING:
from numpy import ndarray from numpy import ndarray
import _pytest._code
from _pytest.compat import final
from _pytest.compat import STRING_TYPES
from _pytest.compat import overload
from _pytest.outcomes import fail
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
at_str = f" at {at}" if at else "" at_str = f" at {at}" if at else ""
return TypeError( return TypeError(

View File

@ -5,18 +5,18 @@ from pprint import pformat
from types import TracebackType from types import TracebackType
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import final
from typing import Generator from typing import Generator
from typing import Iterator from typing import Iterator
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import overload
from typing import Pattern from typing import Pattern
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import WARNS_NONE_ARG from _pytest.deprecated import WARNS_NONE_ARG
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
@ -117,10 +117,10 @@ def warns( # noqa: F811
warning of that class or classes. warning of that class or classes.
This helper produces a list of :class:`warnings.WarningMessage` objects, one for This helper produces a list of :class:`warnings.WarningMessage` objects, one for
each warning raised (regardless of whether it is an ``expected_warning`` or not). each warning emitted (regardless of whether it is an ``expected_warning`` or not).
Since pytest 8.0, unmatched warnings are also re-emitted when the context closes.
This function can be used as a context manager, which will capture all the raised This function can be used as a context manager::
warnings inside it::
>>> import pytest >>> import pytest
>>> with pytest.warns(RuntimeWarning): >>> with pytest.warns(RuntimeWarning):
@ -135,8 +135,9 @@ def warns( # noqa: F811
>>> with pytest.warns(UserWarning, match=r'must be \d+$'): >>> with pytest.warns(UserWarning, match=r'must be \d+$'):
... warnings.warn("value must be 42", UserWarning) ... warnings.warn("value must be 42", UserWarning)
>>> with pytest.warns(UserWarning, match=r'must be \d+$'): >>> with pytest.warns(UserWarning): # catch re-emitted warning
... warnings.warn("this is not here", UserWarning) ... with pytest.warns(UserWarning, match=r'must be \d+$'):
... warnings.warn("this is not here", UserWarning)
Traceback (most recent call last): Traceback (most recent call last):
... ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
@ -277,6 +278,12 @@ class WarningsChecker(WarningsRecorder):
self.expected_warning = expected_warning_tup self.expected_warning = expected_warning_tup
self.match_expr = match_expr self.match_expr = match_expr
def matches(self, warning: warnings.WarningMessage) -> bool:
assert self.expected_warning is not None
return issubclass(warning.category, self.expected_warning) and bool(
self.match_expr is None or re.search(self.match_expr, str(warning.message))
)
def __exit__( def __exit__(
self, self,
exc_type: Optional[Type[BaseException]], exc_type: Optional[Type[BaseException]],
@ -287,27 +294,34 @@ class WarningsChecker(WarningsRecorder):
__tracebackhide__ = True __tracebackhide__ = True
if self.expected_warning is None:
# nothing to do in this deprecated case, see WARNS_NONE_ARG above
return
def found_str(): def found_str():
return pformat([record.message for record in self], indent=2) return pformat([record.message for record in self], indent=2)
# only check if we're not currently handling an exception try:
if exc_type is None and exc_val is None and exc_tb is None: if not any(issubclass(w.category, self.expected_warning) for w in self):
if self.expected_warning is not None: fail(
if not any(issubclass(r.category, self.expected_warning) for r in self): f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
__tracebackhide__ = True f" Emitted warnings: {found_str()}."
fail( )
f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" elif not any(self.matches(w) for w in self):
f"The list of emitted warnings is: {found_str()}." fail(
f"DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.\n"
f" Regex: {self.match_expr}\n"
f" Emitted warnings: {found_str()}."
)
finally:
# Whether or not any warnings matched, we want to re-emit all unmatched warnings.
for w in self:
if not self.matches(w):
warnings.warn_explicit(
str(w.message),
w.message.__class__, # type: ignore[arg-type]
w.filename,
w.lineno,
module=w.__module__,
source=w.source,
) )
elif self.match_expr is not None:
for r in self:
if issubclass(r.category, self.expected_warning):
if re.compile(self.match_expr).search(str(r.message)):
break
else:
fail(
f"""\
DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.
Regex: {self.match_expr}
Emitted warnings: {found_str()}"""
)

View File

@ -5,6 +5,7 @@ from pprint import pprint
from typing import Any from typing import Any
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import final
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
@ -29,7 +30,6 @@ from _pytest._code.code import ReprLocals
from _pytest._code.code import ReprTraceback from _pytest._code.code import ReprTraceback
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.config import Config from _pytest.config import Config
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Item from _pytest.nodes import Item

View File

@ -6,6 +6,7 @@ import sys
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import final
from typing import Generic from typing import Generic
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -23,7 +24,6 @@ from _pytest import timing
from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest.compat import final
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.nodes import Collector from _pytest.nodes import Collector

View File

@ -18,9 +18,11 @@ from typing import Callable
from typing import cast from typing import cast
from typing import ClassVar from typing import ClassVar
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import NamedTuple
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
@ -39,7 +41,6 @@ from _pytest._code.code import ExceptionRepr
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest._io.wcwidth import wcswidth from _pytest._io.wcwidth import wcswidth
from _pytest.assertion.util import running_on_ci from _pytest.assertion.util import running_on_ci
from _pytest.compat import final
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -112,6 +113,26 @@ class MoreQuietAction(argparse.Action):
namespace.quiet = getattr(namespace, "quiet", 0) + 1 namespace.quiet = getattr(namespace, "quiet", 0) + 1
class TestShortLogReport(NamedTuple):
"""Used to store the test status result category, shortletter and verbose word.
For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
:ivar category:
The class of result, for example ``passed``, ``skipped``, ``error``, or the empty string.
:ivar letter:
The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
:ivar word:
Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
``"ERROR"``, or the empty string.
"""
category: str
letter: str
word: Union[str, Tuple[str, Mapping[str, bool]]]
def pytest_addoption(parser: Parser) -> None: def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("terminal reporting", "Reporting", after="general") group = parser.getgroup("terminal reporting", "Reporting", after="general")
group._addoption( group._addoption(
@ -548,10 +569,11 @@ class TerminalReporter:
def pytest_runtest_logreport(self, report: TestReport) -> None: def pytest_runtest_logreport(self, report: TestReport) -> None:
self._tests_ran = True self._tests_ran = True
rep = report rep = report
res: Tuple[
str, str, Union[str, Tuple[str, Mapping[str, bool]]] res = TestShortLogReport(
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) *self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
category, letter, word = res )
category, letter, word = res.category, res.letter, res.word
if not isinstance(word, tuple): if not isinstance(word, tuple):
markup = None markup = None
else: else:

View File

@ -7,38 +7,32 @@ from pathlib import Path
from shutil import rmtree from shutil import rmtree
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import Literal
from typing import Optional from typing import Optional
from typing import TYPE_CHECKING
from typing import Union from typing import Union
from _pytest.nodes import Item from .pathlib import cleanup_dead_symlinks
from _pytest.reports import CollectReport
from _pytest.stash import StashKey
if TYPE_CHECKING:
from typing_extensions import Literal
RetentionType = Literal["all", "failed", "none"]
from _pytest.config.argparsing import Parser
from .pathlib import LOCK_TIMEOUT from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf from .pathlib import rm_rf
from .pathlib import cleanup_dead_symlinks from _pytest.compat import get_user_id
from _pytest.compat import final, get_user_id
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.nodes import Item
from _pytest.reports import CollectReport
from _pytest.stash import StashKey
tmppath_result_key = StashKey[Dict[str, bool]]() tmppath_result_key = StashKey[Dict[str, bool]]()
RetentionType = Literal["all", "failed", "none"]
@final @final
@ -100,7 +94,7 @@ class TempPathFactory:
policy = config.getini("tmp_path_retention_policy") policy = config.getini("tmp_path_retention_policy")
if policy not in ("all", "failed", "none"): if policy not in ("all", "failed", "none"):
raise ValueError( raise ValueError(
f"tmp_path_retention_policy must be either all, failed, none. Current intput: {policy}." f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
) )
return cls( return cls(

View File

@ -334,15 +334,16 @@ class TestCaseFunction(Function):
finally: finally:
delattr(self._testcase, self.name) delattr(self._testcase, self.name)
def _prunetraceback( def _traceback_filter(
self, excinfo: _pytest._code.ExceptionInfo[BaseException] self, excinfo: _pytest._code.ExceptionInfo[BaseException]
) -> None: ) -> _pytest._code.Traceback:
super()._prunetraceback(excinfo) traceback = super()._traceback_filter(excinfo)
traceback = excinfo.traceback.filter( ntraceback = traceback.filter(
lambda x: not x.frame.f_globals.get("__unittest") lambda x: not x.frame.f_globals.get("__unittest"),
) )
if traceback: if not ntraceback:
excinfo.traceback = traceback ntraceback = traceback
return ntraceback
@hookimpl(tryfirst=True) @hookimpl(tryfirst=True)

View File

@ -3,12 +3,11 @@ import inspect
import warnings import warnings
from types import FunctionType from types import FunctionType
from typing import Any from typing import Any
from typing import final
from typing import Generic from typing import Generic
from typing import Type from typing import Type
from typing import TypeVar from typing import TypeVar
from _pytest.compat import final
class PytestWarning(UserWarning): class PytestWarning(UserWarning):
"""Base class for all warnings emitted by pytest.""" """Base class for all warnings emitted by pytest."""
@ -56,6 +55,12 @@ class PytestRemovedIn8Warning(PytestDeprecationWarning):
__module__ = "pytest" __module__ = "pytest"
class PytestRemovedIn9Warning(PytestDeprecationWarning):
"""Warning class for features that will be removed in pytest 9."""
__module__ = "pytest"
class PytestReturnNotNoneWarning(PytestRemovedIn8Warning): class PytestReturnNotNoneWarning(PytestRemovedIn8Warning):
"""Warning emitted when a test function is returning value other than None.""" """Warning emitted when a test function is returning value other than None."""
@ -149,7 +154,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
""" """
Issue the warning :param:`message` for the definition of the given :param:`method` Issue the warning :param:`message` for the definition of the given :param:`method`
this helps to log warnigns for functions defined prior to finding an issue with them this helps to log warnings for functions defined prior to finding an issue with them
(like hook wrappers being marked in a legacy mechanism) (like hook wrappers being marked in a legacy mechanism)
""" """
lineno = method.__code__.co_firstlineno lineno = method.__code__.co_firstlineno

View File

@ -49,6 +49,8 @@ def catch_warnings_for_item(
warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=DeprecationWarning)
warnings.filterwarnings("always", category=PendingDeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning)
warnings.filterwarnings("error", category=pytest.PytestRemovedIn8Warning)
apply_warning_filters(config_filters, cmdline_filters) apply_warning_filters(config_filters, cmdline_filters)
# apply filters from "filterwarnings" marks # apply filters from "filterwarnings" marks

View File

@ -62,6 +62,7 @@ from _pytest.reports import TestReport
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.stash import Stash from _pytest.stash import Stash
from _pytest.stash import StashKey from _pytest.stash import StashKey
from _pytest.terminal import TestShortLogReport
from _pytest.tmpdir import TempPathFactory from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warning_types import PytestAssertRewriteWarning
from _pytest.warning_types import PytestCacheWarning from _pytest.warning_types import PytestCacheWarning
@ -70,6 +71,7 @@ from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestExperimentalApiWarning from _pytest.warning_types import PytestExperimentalApiWarning
from _pytest.warning_types import PytestRemovedIn8Warning from _pytest.warning_types import PytestRemovedIn8Warning
from _pytest.warning_types import PytestRemovedIn9Warning
from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestReturnNotNoneWarning
from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning
from _pytest.warning_types import PytestUnhandledThreadExceptionWarning from _pytest.warning_types import PytestUnhandledThreadExceptionWarning
@ -130,6 +132,7 @@ __all__ = [
"PytestDeprecationWarning", "PytestDeprecationWarning",
"PytestExperimentalApiWarning", "PytestExperimentalApiWarning",
"PytestRemovedIn8Warning", "PytestRemovedIn8Warning",
"PytestRemovedIn9Warning",
"PytestReturnNotNoneWarning", "PytestReturnNotNoneWarning",
"Pytester", "Pytester",
"PytestPluginManager", "PytestPluginManager",
@ -152,6 +155,7 @@ __all__ = [
"TempPathFactory", "TempPathFactory",
"Testdir", "Testdir",
"TestReport", "TestReport",
"TestShortLogReport",
"UsageError", "UsageError",
"WarningsRecorder", "WarningsRecorder",
"warns", "warns",

View File

@ -1,7 +1,9 @@
import contextlib
import multiprocessing import multiprocessing
import os import os
import sys import sys
import time import time
import warnings
from unittest import mock from unittest import mock
import pytest import pytest
@ -9,6 +11,14 @@ from py import error
from py.path import local from py.path import local
@contextlib.contextmanager
def ignore_encoding_warning():
with warnings.catch_warnings():
with contextlib.suppress(NameError): # new in 3.10
warnings.simplefilter("ignore", EncodingWarning)
yield
class CommonFSTests: class CommonFSTests:
def test_constructor_equality(self, path1): def test_constructor_equality(self, path1):
p = path1.__class__(path1) p = path1.__class__(path1)
@ -223,7 +233,8 @@ class CommonFSTests:
assert not (path1 < path1) assert not (path1 < path1)
def test_simple_read(self, path1): def test_simple_read(self, path1):
x = path1.join("samplefile").read("r") with ignore_encoding_warning():
x = path1.join("samplefile").read("r")
assert x == "samplefile\n" assert x == "samplefile\n"
def test_join_div_operator(self, path1): def test_join_div_operator(self, path1):
@ -265,12 +276,14 @@ class CommonFSTests:
def test_readlines(self, path1): def test_readlines(self, path1):
fn = path1.join("samplefile") fn = path1.join("samplefile")
contents = fn.readlines() with ignore_encoding_warning():
contents = fn.readlines()
assert contents == ["samplefile\n"] assert contents == ["samplefile\n"]
def test_readlines_nocr(self, path1): def test_readlines_nocr(self, path1):
fn = path1.join("samplefile") fn = path1.join("samplefile")
contents = fn.readlines(cr=0) with ignore_encoding_warning():
contents = fn.readlines(cr=0)
assert contents == ["samplefile", ""] assert contents == ["samplefile", ""]
def test_file(self, path1): def test_file(self, path1):
@ -362,8 +375,8 @@ class CommonFSTests:
initpy.copy(copied) initpy.copy(copied)
try: try:
assert copied.check() assert copied.check()
s1 = initpy.read() s1 = initpy.read_text(encoding="utf-8")
s2 = copied.read() s2 = copied.read_text(encoding="utf-8")
assert s1 == s2 assert s1 == s2
finally: finally:
if copied.check(): if copied.check():
@ -376,8 +389,8 @@ class CommonFSTests:
otherdir.copy(copied) otherdir.copy(copied)
assert copied.check(dir=1) assert copied.check(dir=1)
assert copied.join("__init__.py").check(file=1) assert copied.join("__init__.py").check(file=1)
s1 = otherdir.join("__init__.py").read() s1 = otherdir.join("__init__.py").read_text(encoding="utf-8")
s2 = copied.join("__init__.py").read() s2 = copied.join("__init__.py").read_text(encoding="utf-8")
assert s1 == s2 assert s1 == s2
finally: finally:
if copied.check(dir=1): if copied.check(dir=1):
@ -463,13 +476,13 @@ def setuptestfs(path):
return return
# print "setting up test fs for", repr(path) # print "setting up test fs for", repr(path)
samplefile = path.ensure("samplefile") samplefile = path.ensure("samplefile")
samplefile.write("samplefile\n") samplefile.write_text("samplefile\n", encoding="utf-8")
execfile = path.ensure("execfile") execfile = path.ensure("execfile")
execfile.write("x=42") execfile.write_text("x=42", encoding="utf-8")
execfilepy = path.ensure("execfile.py") execfilepy = path.ensure("execfile.py")
execfilepy.write("x=42") execfilepy.write_text("x=42", encoding="utf-8")
d = {1: 2, "hello": "world", "answer": 42} d = {1: 2, "hello": "world", "answer": 42}
path.ensure("samplepickle").dump(d) path.ensure("samplepickle").dump(d)
@ -481,22 +494,24 @@ def setuptestfs(path):
otherdir.ensure("__init__.py") otherdir.ensure("__init__.py")
module_a = otherdir.ensure("a.py") module_a = otherdir.ensure("a.py")
module_a.write("from .b import stuff as result\n") module_a.write_text("from .b import stuff as result\n", encoding="utf-8")
module_b = otherdir.ensure("b.py") module_b = otherdir.ensure("b.py")
module_b.write('stuff="got it"\n') module_b.write_text('stuff="got it"\n', encoding="utf-8")
module_c = otherdir.ensure("c.py") module_c = otherdir.ensure("c.py")
module_c.write( module_c.write_text(
"""import py; """import py;
import otherdir.a import otherdir.a
value = otherdir.a.result value = otherdir.a.result
""" """,
encoding="utf-8",
) )
module_d = otherdir.ensure("d.py") module_d = otherdir.ensure("d.py")
module_d.write( module_d.write_text(
"""import py; """import py;
from otherdir import a from otherdir import a
value2 = a.result value2 = a.result
""" """,
encoding="utf-8",
) )
@ -534,9 +549,11 @@ def batch_make_numbered_dirs(rootdir, repeats):
for i in range(repeats): for i in range(repeats):
dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir) dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir)
file_ = dir_.join("foo") file_ = dir_.join("foo")
file_.write("%s" % i) file_.write_text("%s" % i, encoding="utf-8")
actual = int(file_.read()) actual = int(file_.read_text(encoding="utf-8"))
assert actual == i, f"int(file_.read()) is {actual} instead of {i}" assert (
actual == i
), f"int(file_.read_text(encoding='utf-8')) is {actual} instead of {i}"
dir_.join(".lock").remove(ignore_errors=True) dir_.join(".lock").remove(ignore_errors=True)
return True return True
@ -692,14 +709,14 @@ class TestLocalPath(CommonFSTests):
def test_open_and_ensure(self, path1): def test_open_and_ensure(self, path1):
p = path1.join("sub1", "sub2", "file") p = path1.join("sub1", "sub2", "file")
with p.open("w", ensure=1) as f: with p.open("w", ensure=1, encoding="utf-8") as f:
f.write("hello") f.write("hello")
assert p.read() == "hello" assert p.read_text(encoding="utf-8") == "hello"
def test_write_and_ensure(self, path1): def test_write_and_ensure(self, path1):
p = path1.join("sub1", "sub2", "file") p = path1.join("sub1", "sub2", "file")
p.write("hello", ensure=1) p.write_text("hello", ensure=1, encoding="utf-8")
assert p.read() == "hello" assert p.read_text(encoding="utf-8") == "hello"
@pytest.mark.parametrize("bin", (False, True)) @pytest.mark.parametrize("bin", (False, True))
def test_dump(self, tmpdir, bin): def test_dump(self, tmpdir, bin):
@ -770,9 +787,9 @@ class TestLocalPath(CommonFSTests):
newfile = tmpdir.join("test1", "test") newfile = tmpdir.join("test1", "test")
newfile.ensure() newfile.ensure()
assert newfile.check(file=1) assert newfile.check(file=1)
newfile.write("42") newfile.write_text("42", encoding="utf-8")
newfile.ensure() newfile.ensure()
s = newfile.read() s = newfile.read_text(encoding="utf-8")
assert s == "42" assert s == "42"
def test_ensure_filepath_withoutdir(self, tmpdir): def test_ensure_filepath_withoutdir(self, tmpdir):
@ -806,9 +823,9 @@ class TestLocalPath(CommonFSTests):
newfilename = "/test" * 60 # type:ignore[unreachable] newfilename = "/test" * 60 # type:ignore[unreachable]
l1 = tmpdir.join(newfilename) l1 = tmpdir.join(newfilename)
l1.ensure(file=True) l1.ensure(file=True)
l1.write("foo") l1.write_text("foo", encoding="utf-8")
l2 = tmpdir.join(newfilename) l2 = tmpdir.join(newfilename)
assert l2.read() == "foo" assert l2.read_text(encoding="utf-8") == "foo"
def test_visit_depth_first(self, tmpdir): def test_visit_depth_first(self, tmpdir):
tmpdir.ensure("a", "1") tmpdir.ensure("a", "1")
@ -1278,14 +1295,14 @@ class TestPOSIXLocalPath:
def test_hardlink(self, tmpdir): def test_hardlink(self, tmpdir):
linkpath = tmpdir.join("test") linkpath = tmpdir.join("test")
filepath = tmpdir.join("file") filepath = tmpdir.join("file")
filepath.write("Hello") filepath.write_text("Hello", encoding="utf-8")
nlink = filepath.stat().nlink nlink = filepath.stat().nlink
linkpath.mklinkto(filepath) linkpath.mklinkto(filepath)
assert filepath.stat().nlink == nlink + 1 assert filepath.stat().nlink == nlink + 1
def test_symlink_are_identical(self, tmpdir): def test_symlink_are_identical(self, tmpdir):
filepath = tmpdir.join("file") filepath = tmpdir.join("file")
filepath.write("Hello") filepath.write_text("Hello", encoding="utf-8")
linkpath = tmpdir.join("test") linkpath = tmpdir.join("test")
linkpath.mksymlinkto(filepath) linkpath.mksymlinkto(filepath)
assert linkpath.readlink() == str(filepath) assert linkpath.readlink() == str(filepath)
@ -1293,7 +1310,7 @@ class TestPOSIXLocalPath:
def test_symlink_isfile(self, tmpdir): def test_symlink_isfile(self, tmpdir):
linkpath = tmpdir.join("test") linkpath = tmpdir.join("test")
filepath = tmpdir.join("file") filepath = tmpdir.join("file")
filepath.write("") filepath.write_text("", encoding="utf-8")
linkpath.mksymlinkto(filepath) linkpath.mksymlinkto(filepath)
assert linkpath.check(file=1) assert linkpath.check(file=1)
assert not linkpath.check(link=0, file=1) assert not linkpath.check(link=0, file=1)
@ -1302,10 +1319,12 @@ class TestPOSIXLocalPath:
def test_symlink_relative(self, tmpdir): def test_symlink_relative(self, tmpdir):
linkpath = tmpdir.join("test") linkpath = tmpdir.join("test")
filepath = tmpdir.join("file") filepath = tmpdir.join("file")
filepath.write("Hello") filepath.write_text("Hello", encoding="utf-8")
linkpath.mksymlinkto(filepath, absolute=False) linkpath.mksymlinkto(filepath, absolute=False)
assert linkpath.readlink() == "file" assert linkpath.readlink() == "file"
assert filepath.read() == linkpath.read() assert filepath.read_text(encoding="utf-8") == linkpath.read_text(
encoding="utf-8"
)
def test_symlink_not_existing(self, tmpdir): def test_symlink_not_existing(self, tmpdir):
linkpath = tmpdir.join("testnotexisting") linkpath = tmpdir.join("testnotexisting")
@ -1338,7 +1357,7 @@ class TestPOSIXLocalPath:
def test_realpath_file(self, tmpdir): def test_realpath_file(self, tmpdir):
linkpath = tmpdir.join("test") linkpath = tmpdir.join("test")
filepath = tmpdir.join("file") filepath = tmpdir.join("file")
filepath.write("") filepath.write_text("", encoding="utf-8")
linkpath.mksymlinkto(filepath) linkpath.mksymlinkto(filepath)
realpath = linkpath.realpath() realpath = linkpath.realpath()
assert realpath.basename == "file" assert realpath.basename == "file"
@ -1383,7 +1402,7 @@ class TestPOSIXLocalPath:
atime1 = path.atime() atime1 = path.atime()
# we could wait here but timer resolution is very # we could wait here but timer resolution is very
# system dependent # system dependent
path.read() path.read_binary()
time.sleep(ATIME_RESOLUTION) time.sleep(ATIME_RESOLUTION)
atime2 = path.atime() atime2 = path.atime()
time.sleep(ATIME_RESOLUTION) time.sleep(ATIME_RESOLUTION)
@ -1467,7 +1486,7 @@ class TestPOSIXLocalPath:
test_files = ["a", "b", "c"] test_files = ["a", "b", "c"]
src = tmpdir.join("src") src = tmpdir.join("src")
for f in test_files: for f in test_files:
src.join(f).write(f, ensure=True) src.join(f).write_text(f, ensure=True, encoding="utf-8")
dst = tmpdir.join("dst") dst = tmpdir.join("dst")
# a small delay before the copy # a small delay before the copy
time.sleep(ATIME_RESOLUTION) time.sleep(ATIME_RESOLUTION)
@ -1521,10 +1540,11 @@ class TestUnicodePy2Py3:
def test_read_write(self, tmpdir): def test_read_write(self, tmpdir):
x = tmpdir.join("hello") x = tmpdir.join("hello")
part = "hällo" part = "hällo"
x.write(part) with ignore_encoding_warning():
assert x.read() == part x.write(part)
x.write(part.encode(sys.getdefaultencoding())) assert x.read() == part
assert x.read() == part.encode(sys.getdefaultencoding()) x.write(part.encode(sys.getdefaultencoding()))
assert x.read() == part.encode(sys.getdefaultencoding())
class TestBinaryAndTextMethods: class TestBinaryAndTextMethods:

View File

@ -1,10 +1,10 @@
import dataclasses import dataclasses
import importlib.metadata
import os import os
import sys import sys
import types import types
import pytest import pytest
from _pytest.compat import importlib_metadata
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.pathlib import symlink_or_skip from _pytest.pathlib import symlink_or_skip
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
@ -139,7 +139,7 @@ class TestGeneralUsage:
def my_dists(): def my_dists():
return (DummyDist(entry_points),) return (DummyDist(entry_points),)
monkeypatch.setattr(importlib_metadata, "distributions", my_dists) monkeypatch.setattr(importlib.metadata, "distributions", my_dists)
params = ("-p", "mycov") if load_cov_early else () params = ("-p", "mycov") if load_cov_early else ()
pytester.runpytest_inprocess(*params) pytester.runpytest_inprocess(*params)
if load_cov_early: if load_cov_early:
@ -267,7 +267,7 @@ class TestGeneralUsage:
def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None: def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None:
sub1 = pytester.mkdir("sub1") sub1 = pytester.mkdir("sub1")
sub2 = pytester.mkdir("sub2") sub2 = pytester.mkdir("sub2")
sub1.joinpath("conftest.py").write_text("assert 0") sub1.joinpath("conftest.py").write_text("assert 0", encoding="utf-8")
result = pytester.runpytest(sub2) result = pytester.runpytest(sub2)
assert result.ret == ExitCode.NO_TESTS_COLLECTED assert result.ret == ExitCode.NO_TESTS_COLLECTED
sub2.joinpath("__init__.py").touch() sub2.joinpath("__init__.py").touch()
@ -467,7 +467,7 @@ class TestGeneralUsage:
assert "invalid" in str(excinfo.value) assert "invalid" in str(excinfo.value)
p = pytester.path.joinpath("test_test_plugins_given_as_strings.py") p = pytester.path.joinpath("test_test_plugins_given_as_strings.py")
p.write_text("def test_foo(): pass") p.write_text("def test_foo(): pass", encoding="utf-8")
mod = types.ModuleType("myplugin") mod = types.ModuleType("myplugin")
monkeypatch.setitem(sys.modules, "myplugin", mod) monkeypatch.setitem(sys.modules, "myplugin", mod)
assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0 assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0
@ -587,7 +587,7 @@ class TestInvocationVariants:
def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None: def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None:
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False) monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
path = pytester.mkpydir("tpkg") path = pytester.mkpydir("tpkg")
path.joinpath("test_hello.py").write_text("raise ImportError") path.joinpath("test_hello.py").write_text("raise ImportError", encoding="utf-8")
result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
assert result.ret != 0 assert result.ret != 0
@ -597,10 +597,10 @@ class TestInvocationVariants:
def test_pyargs_only_imported_once(self, pytester: Pytester) -> None: def test_pyargs_only_imported_once(self, pytester: Pytester) -> None:
pkg = pytester.mkpydir("foo") pkg = pytester.mkpydir("foo")
pkg.joinpath("test_foo.py").write_text( pkg.joinpath("test_foo.py").write_text(
"print('hello from test_foo')\ndef test(): pass" "print('hello from test_foo')\ndef test(): pass", encoding="utf-8"
) )
pkg.joinpath("conftest.py").write_text( pkg.joinpath("conftest.py").write_text(
"def pytest_configure(config): print('configuring')" "def pytest_configure(config): print('configuring')", encoding="utf-8"
) )
result = pytester.runpytest( result = pytester.runpytest(
@ -613,7 +613,7 @@ class TestInvocationVariants:
def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None: def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None:
pytester.path.joinpath("conftest.py").touch() pytester.path.joinpath("conftest.py").touch()
pytester.path.joinpath("t.py").write_text("def test(): pass") pytester.path.joinpath("t.py").write_text("def test(): pass", encoding="utf-8")
result = pytester.runpytest("--pyargs", "t.py") result = pytester.runpytest("--pyargs", "t.py")
assert result.ret == ExitCode.OK assert result.ret == ExitCode.OK
@ -622,8 +622,12 @@ class TestInvocationVariants:
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False) monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
path = pytester.mkpydir("tpkg") path = pytester.mkpydir("tpkg")
path.joinpath("test_hello.py").write_text("def test_hello(): pass") path.joinpath("test_hello.py").write_text(
path.joinpath("test_world.py").write_text("def test_world(): pass") "def test_hello(): pass", encoding="utf-8"
)
path.joinpath("test_world.py").write_text(
"def test_world(): pass", encoding="utf-8"
)
result = pytester.runpytest("--pyargs", "tpkg") result = pytester.runpytest("--pyargs", "tpkg")
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines(["*2 passed*"]) result.stdout.fnmatch_lines(["*2 passed*"])
@ -662,13 +666,15 @@ class TestInvocationVariants:
ns = d.joinpath("ns_pkg") ns = d.joinpath("ns_pkg")
ns.mkdir() ns.mkdir()
ns.joinpath("__init__.py").write_text( ns.joinpath("__init__.py").write_text(
"__import__('pkg_resources').declare_namespace(__name__)" "__import__('pkg_resources').declare_namespace(__name__)",
encoding="utf-8",
) )
lib = ns.joinpath(dirname) lib = ns.joinpath(dirname)
lib.mkdir() lib.mkdir()
lib.joinpath("__init__.py").touch() lib.joinpath("__init__.py").touch()
lib.joinpath(f"test_{dirname}.py").write_text( lib.joinpath(f"test_{dirname}.py").write_text(
f"def test_{dirname}(): pass\ndef test_other():pass" f"def test_{dirname}(): pass\ndef test_other():pass",
encoding="utf-8",
) )
# The structure of the test directory is now: # The structure of the test directory is now:
@ -754,10 +760,10 @@ class TestInvocationVariants:
lib.mkdir() lib.mkdir()
lib.joinpath("__init__.py").touch() lib.joinpath("__init__.py").touch()
lib.joinpath("test_bar.py").write_text( lib.joinpath("test_bar.py").write_text(
"def test_bar(): pass\ndef test_other(a_fixture):pass" "def test_bar(): pass\ndef test_other(a_fixture):pass", encoding="utf-8"
) )
lib.joinpath("conftest.py").write_text( lib.joinpath("conftest.py").write_text(
"import pytest\n@pytest.fixture\ndef a_fixture():pass" "import pytest\n@pytest.fixture\ndef a_fixture():pass", encoding="utf-8"
) )
d_local = pytester.mkdir("symlink_root") d_local = pytester.mkdir("symlink_root")
@ -1158,7 +1164,6 @@ def test_usage_error_code(pytester: Pytester) -> None:
assert result.ret == ExitCode.USAGE_ERROR assert result.ret == ExitCode.USAGE_ERROR
@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning")
def test_warn_on_async_function(pytester: Pytester) -> None: def test_warn_on_async_function(pytester: Pytester) -> None:
# In the below we .close() the coroutine only to avoid # In the below we .close() the coroutine only to avoid
# "RuntimeWarning: coroutine 'test_2' was never awaited" # "RuntimeWarning: coroutine 'test_2' was never awaited"
@ -1175,7 +1180,7 @@ def test_warn_on_async_function(pytester: Pytester) -> None:
return coro return coro
""" """
) )
result = pytester.runpytest() result = pytester.runpytest("-Wdefault")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"test_async.py::test_1", "test_async.py::test_1",
@ -1191,7 +1196,6 @@ def test_warn_on_async_function(pytester: Pytester) -> None:
) )
@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning")
def test_warn_on_async_gen_function(pytester: Pytester) -> None: def test_warn_on_async_gen_function(pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(
test_async=""" test_async="""
@ -1203,7 +1207,7 @@ def test_warn_on_async_gen_function(pytester: Pytester) -> None:
return test_2() return test_2()
""" """
) )
result = pytester.runpytest() result = pytester.runpytest("-Wdefault")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"test_async.py::test_1", "test_async.py::test_1",
@ -1276,8 +1280,7 @@ def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None:
result.stderr.fnmatch_lines(["*@this is stderr@*"]) result.stderr.fnmatch_lines(["*@this is stderr@*"])
# now ensure the output is in the junitxml # now ensure the output is in the junitxml
with open(pytester.path.joinpath("output.xml")) as f: fullXml = pytester.path.joinpath("output.xml").read_text(encoding="utf-8")
fullXml = f.read()
assert "@this is stdout@\n" in fullXml assert "@this is stdout@\n" in fullXml
assert "@this is stderr@\n" in fullXml assert "@this is stderr@\n" in fullXml
@ -1312,3 +1315,38 @@ def test_function_return_non_none_warning(pytester: Pytester) -> None:
) )
res = pytester.runpytest() res = pytester.runpytest()
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"]) res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
"""
Regression test for #10811: previously import_path with ImportMode.importlib would
not return a module if already in sys.modules, resulting in modules being imported
multiple times, which causes problems with modules that have import side effects.
"""
# Uses the exact reproducer form #10811, given it is very minimal
# and illustrates the problem well.
pytester.makepyfile(
**{
"pmxbot/commands.py": "from . import logging",
"pmxbot/logging.py": "",
"tests/__init__.py": "",
"tests/test_commands.py": """
import importlib
from pmxbot import logging
class TestCommands:
def test_boo(self):
assert importlib.import_module('pmxbot.logging') is logging
""",
}
)
pytester.makeini(
"""
[pytest]
addopts=
--doctest-modules
--import-mode importlib
"""
)
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines("*1 passed*")

View File

@ -11,7 +11,7 @@ from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import _pytest import _pytest._code
import pytest import pytest
from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
@ -186,7 +186,7 @@ class TestTraceback_f_g_h:
def test_traceback_filter(self): def test_traceback_filter(self):
traceback = self.excinfo.traceback traceback = self.excinfo.traceback
ntraceback = traceback.filter() ntraceback = traceback.filter(self.excinfo)
assert len(ntraceback) == len(traceback) - 1 assert len(ntraceback) == len(traceback) - 1
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -217,7 +217,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, h) excinfo = pytest.raises(ValueError, h)
traceback = excinfo.traceback traceback = excinfo.traceback
ntraceback = traceback.filter() ntraceback = traceback.filter(excinfo)
print(f"old: {traceback!r}") print(f"old: {traceback!r}")
print(f"new: {ntraceback!r}") print(f"new: {ntraceback!r}")
@ -290,7 +290,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, fail) excinfo = pytest.raises(ValueError, fail)
assert excinfo.traceback.recursionindex() is None assert excinfo.traceback.recursionindex() is None
def test_traceback_getcrashentry(self): def test_getreprcrash(self):
def i(): def i():
__tracebackhide__ = True __tracebackhide__ = True
raise ValueError raise ValueError
@ -306,15 +306,13 @@ class TestTraceback_f_g_h:
g() g()
excinfo = pytest.raises(ValueError, f) excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback reprcrash = excinfo._getreprcrash()
entry = tb.getcrashentry() assert reprcrash is not None
assert entry is not None
co = _pytest._code.Code.from_function(h) co = _pytest._code.Code.from_function(h)
assert entry.frame.code.path == co.path assert reprcrash.path == str(co.path)
assert entry.lineno == co.firstlineno + 1 assert reprcrash.lineno == co.firstlineno + 1 + 1
assert entry.frame.code.name == "h"
def test_traceback_getcrashentry_empty(self): def test_getreprcrash_empty(self):
def g(): def g():
__tracebackhide__ = True __tracebackhide__ = True
raise ValueError raise ValueError
@ -324,7 +322,7 @@ class TestTraceback_f_g_h:
g() g()
excinfo = pytest.raises(ValueError, f) excinfo = pytest.raises(ValueError, f)
assert excinfo.traceback.getcrashentry() is None assert excinfo._getreprcrash() is None
def test_excinfo_exconly(): def test_excinfo_exconly():
@ -376,7 +374,7 @@ def test_excinfo_no_sourcecode():
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None: def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
# XXX: simplified locally testable version # XXX: simplified locally testable version
tmp_path.joinpath("test.txt").write_text("{{ h()}}:") tmp_path.joinpath("test.txt").write_text("{{ h()}}:", encoding="utf-8")
jinja2 = pytest.importorskip("jinja2") jinja2 = pytest.importorskip("jinja2")
loader = jinja2.FileSystemLoader(str(tmp_path)) loader = jinja2.FileSystemLoader(str(tmp_path))
@ -453,7 +451,7 @@ class TestFormattedExcinfo:
source = textwrap.dedent(source) source = textwrap.dedent(source)
modpath = tmp_path.joinpath("mod.py") modpath = tmp_path.joinpath("mod.py")
tmp_path.joinpath("__init__.py").touch() tmp_path.joinpath("__init__.py").touch()
modpath.write_text(source) modpath.write_text(source, encoding="utf-8")
importlib.invalidate_caches() importlib.invalidate_caches()
return import_path(modpath, root=tmp_path) return import_path(modpath, root=tmp_path)
@ -626,7 +624,7 @@ raise ValueError()
""" """
) )
excinfo = pytest.raises(ValueError, mod.func1) excinfo = pytest.raises(ValueError, mod.func1)
excinfo.traceback = excinfo.traceback.filter() excinfo.traceback = excinfo.traceback.filter(excinfo)
p = FormattedExcinfo() p = FormattedExcinfo()
reprtb = p.repr_traceback_entry(excinfo.traceback[-1]) reprtb = p.repr_traceback_entry(excinfo.traceback[-1])
@ -659,7 +657,7 @@ raise ValueError()
""" """
) )
excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120) excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120)
excinfo.traceback = excinfo.traceback.filter() excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1] entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True) p = FormattedExcinfo(funcargs=True)
reprfuncargs = p.repr_args(entry) reprfuncargs = p.repr_args(entry)
@ -686,7 +684,7 @@ raise ValueError()
""" """
) )
excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d") excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d")
excinfo.traceback = excinfo.traceback.filter() excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1] entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True) p = FormattedExcinfo(funcargs=True)
reprfuncargs = p.repr_args(entry) reprfuncargs = p.repr_args(entry)
@ -960,7 +958,7 @@ raise ValueError()
""" """
) )
excinfo = pytest.raises(ValueError, mod.f) excinfo = pytest.raises(ValueError, mod.f)
excinfo.traceback = excinfo.traceback.filter() excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr() repr = excinfo.getrepr()
repr.toterminal(tw_mock) repr.toterminal(tw_mock)
assert tw_mock.lines[0] == "" assert tw_mock.lines[0] == ""
@ -994,7 +992,7 @@ raise ValueError()
) )
excinfo = pytest.raises(ValueError, mod.f) excinfo = pytest.raises(ValueError, mod.f)
tmp_path.joinpath("mod.py").unlink() tmp_path.joinpath("mod.py").unlink()
excinfo.traceback = excinfo.traceback.filter() excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr() repr = excinfo.getrepr()
repr.toterminal(tw_mock) repr.toterminal(tw_mock)
assert tw_mock.lines[0] == "" assert tw_mock.lines[0] == ""
@ -1025,8 +1023,8 @@ raise ValueError()
""" """
) )
excinfo = pytest.raises(ValueError, mod.f) excinfo = pytest.raises(ValueError, mod.f)
tmp_path.joinpath("mod.py").write_text("asdf") tmp_path.joinpath("mod.py").write_text("asdf", encoding="utf-8")
excinfo.traceback = excinfo.traceback.filter() excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr() repr = excinfo.getrepr()
repr.toterminal(tw_mock) repr.toterminal(tw_mock)
assert tw_mock.lines[0] == "" assert tw_mock.lines[0] == ""
@ -1123,9 +1121,11 @@ raise ValueError()
""" """
) )
excinfo = pytest.raises(ValueError, mod.f) excinfo = pytest.raises(ValueError, mod.f)
excinfo.traceback = excinfo.traceback.filter() excinfo.traceback = excinfo.traceback.filter(excinfo)
excinfo.traceback[1].set_repr_style("short") excinfo.traceback = _pytest._code.Traceback(
excinfo.traceback[2].set_repr_style("short") entry if i not in (1, 2) else entry.with_repr_style("short")
for i, entry in enumerate(excinfo.traceback)
)
r = excinfo.getrepr(style="long") r = excinfo.getrepr(style="long")
r.toterminal(tw_mock) r.toterminal(tw_mock)
for line in tw_mock.lines: for line in tw_mock.lines:
@ -1391,7 +1391,7 @@ raise ValueError()
with pytest.raises(TypeError) as excinfo: with pytest.raises(TypeError) as excinfo:
mod.f() mod.f()
# previously crashed with `AttributeError: list has no attribute get` # previously crashed with `AttributeError: list has no attribute get`
excinfo.traceback.filter() excinfo.traceback.filter(excinfo)
@pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("style", ["short", "long"])
@ -1603,3 +1603,48 @@ def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"]) result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
if tbstyle not in ("line", "native"): if tbstyle not in ("line", "native"):
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"]) result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])
def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None:
"""Hidden entries of chained exceptions are not shown (#1904)."""
p = pytester.makepyfile(
"""
def g1():
__tracebackhide__ = True
str.does_not_exist
def f3():
__tracebackhide__ = True
1 / 0
def f2():
try:
f3()
except Exception:
g1()
def f1():
__tracebackhide__ = True
f2()
def test():
f1()
"""
)
result = pytester.runpytest(str(p), "--tb=short")
assert result.ret == 1
result.stdout.fnmatch_lines(
[
"*.py:11: in f2",
" f3()",
"E ZeroDivisionError: division by zero",
"",
"During handling of the above exception, another exception occurred:",
"*.py:20: in test",
" f1()",
"*.py:13: in f2",
" g1()",
"E AttributeError:*'does_not_exist'",
],
consecutive=True,
)

View File

@ -294,7 +294,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) -
""" """
) )
path = tmp_path.joinpath("a.py") path = tmp_path.joinpath("a.py")
path.write_text(str(source)) path.write_text(str(source), encoding="utf-8")
mod: Any = import_path(path, root=tmp_path) mod: Any = import_path(path, root=tmp_path)
s2 = Source(mod.A) s2 = Source(mod.A)
assert str(source).strip() == str(s2).strip() assert str(source).strip() == str(s2).strip()
@ -439,14 +439,9 @@ comment 4
''' '''
for line in range(2, 6): for line in range(2, 6):
assert str(getstatement(line, source)) == " x = 1" assert str(getstatement(line, source)) == " x = 1"
if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): for line in range(6, 8):
tqs_start = 8
else:
tqs_start = 10
assert str(getstatement(10, source)) == '"""'
for line in range(6, tqs_start):
assert str(getstatement(line, source)) == " assert False" assert str(getstatement(line, source)) == " assert False"
for line in range(tqs_start, 10): for line in range(8, 10):
assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' assert str(getstatement(line, source)) == '"""\ncomment 4\n"""'

View File

@ -105,7 +105,7 @@ def tw_mock():
@pytest.fixture @pytest.fixture
def dummy_yaml_custom_test(pytester: Pytester): def dummy_yaml_custom_test(pytester: Pytester) -> None:
"""Writes a conftest file that collects and executes a dummy yaml test. """Writes a conftest file that collects and executes a dummy yaml test.
Taken from the docs, but stripped down to the bare minimum, useful for Taken from the docs, but stripped down to the bare minimum, useful for

View File

@ -103,7 +103,7 @@ def test_strict_option_is_deprecated(pytester: Pytester) -> None:
def test_foo(): pass def test_foo(): pass
""" """
) )
result = pytester.runpytest("--strict") result = pytester.runpytest("--strict", "-Wdefault::pytest.PytestRemovedIn8Warning")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"'unknown' not found in `markers` configuration option", "'unknown' not found in `markers` configuration option",
@ -189,7 +189,7 @@ class TestSkipMsgArgumentDeprecated:
pytest.skip(msg="skippedmsg") pytest.skip(msg="skippedmsg")
""" """
) )
result = pytester.runpytest(p) result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*PytestRemovedIn8Warning: pytest.skip(msg=...) is now deprecated, " "*PytestRemovedIn8Warning: pytest.skip(msg=...) is now deprecated, "
@ -208,7 +208,7 @@ class TestSkipMsgArgumentDeprecated:
pytest.fail(msg="failedmsg") pytest.fail(msg="failedmsg")
""" """
) )
result = pytester.runpytest(p) result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*PytestRemovedIn8Warning: pytest.fail(msg=...) is now deprecated, " "*PytestRemovedIn8Warning: pytest.fail(msg=...) is now deprecated, "
@ -227,7 +227,7 @@ class TestSkipMsgArgumentDeprecated:
pytest.exit(msg="exitmsg") pytest.exit(msg="exitmsg")
""" """
) )
result = pytester.runpytest(p) result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*PytestRemovedIn8Warning: pytest.exit(msg=...) is now deprecated, " "*PytestRemovedIn8Warning: pytest.exit(msg=...) is now deprecated, "
@ -245,7 +245,7 @@ def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
""" """
) )
result = pytester.runpytest() result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*PytestRemovedIn8Warning: The pytest_cmdline_preparse hook is deprecated*", "*PytestRemovedIn8Warning: The pytest_cmdline_preparse hook is deprecated*",
@ -281,6 +281,57 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
from _pytest.python import Instance # noqa: F401 from _pytest.python import Instance # noqa: F401
def test_fixture_disallow_on_marked_functions():
"""Test that applying @pytest.fixture to a marked function warns (#3364)."""
with pytest.warns(
pytest.PytestRemovedIn8Warning,
match=r"Marks applied to fixtures have no effect",
) as record:
@pytest.fixture
@pytest.mark.parametrize("example", ["hello"])
@pytest.mark.usefixtures("tmp_path")
def foo():
raise NotImplementedError()
# it's only possible to get one warning here because you're already prevented
# from applying @fixture twice
# ValueError("fixture is being applied more than once to the same function")
assert len(record) == 1
def test_fixture_disallow_marks_on_fixtures():
"""Test that applying a mark to a fixture warns (#3364)."""
with pytest.warns(
pytest.PytestRemovedIn8Warning,
match=r"Marks applied to fixtures have no effect",
) as record:
@pytest.mark.parametrize("example", ["hello"])
@pytest.mark.usefixtures("tmp_path")
@pytest.fixture
def foo():
raise NotImplementedError()
assert len(record) == 2 # one for each mark decorator
def test_fixture_disallowed_between_marks():
"""Test that applying a mark to a fixture warns (#3364)."""
with pytest.warns(
pytest.PytestRemovedIn8Warning,
match=r"Marks applied to fixtures have no effect",
) as record:
@pytest.mark.parametrize("example", ["hello"])
@pytest.fixture
@pytest.mark.usefixtures("tmp_path")
def foo():
raise NotImplementedError()
assert len(record) == 2 # one for each mark decorator
@pytest.mark.filterwarnings("default") @pytest.mark.filterwarnings("default")
def test_nose_deprecated_with_setup(pytester: Pytester) -> None: def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
pytest.importorskip("nose") pytest.importorskip("nose")
@ -299,7 +350,7 @@ def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
... ...
""" """
) )
output = pytester.runpytest() output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
message = [ message = [
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.", "*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)", "*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)",
@ -327,7 +378,7 @@ def test_nose_deprecated_setup_teardown(pytester: Pytester) -> None:
... ...
""" """
) )
output = pytester.runpytest() output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
message = [ message = [
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.", "*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`", "*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`",

View File

@ -0,0 +1,2 @@
def test_init():
pass

View File

@ -1,2 +1,2 @@
def test(): def test_foo():
pass pass

View File

@ -1,3 +1,4 @@
# mypy: disable-error-code="attr-defined"
import logging import logging
import pytest import pytest
@ -8,6 +9,19 @@ logger = logging.getLogger(__name__)
sublogger = logging.getLogger(__name__ + ".baz") sublogger = logging.getLogger(__name__ + ".baz")
@pytest.fixture
def cleanup_disabled_logging():
"""Simple fixture that ensures that a test doesn't disable logging.
This is necessary because ``logging.disable()`` is global, so a test disabling logging
and not cleaning up after will break every test that runs after it.
This behavior was moved to a fixture so that logging will be un-disabled even if the test fails an assertion.
"""
yield
logging.disable(logging.NOTSET)
def test_fixture_help(pytester: Pytester) -> None: def test_fixture_help(pytester: Pytester) -> None:
result = pytester.runpytest("--fixtures") result = pytester.runpytest("--fixtures")
result.stdout.fnmatch_lines(["*caplog*"]) result.stdout.fnmatch_lines(["*caplog*"])
@ -28,10 +42,27 @@ def test_change_level(caplog):
assert "CRITICAL" in caplog.text assert "CRITICAL" in caplog.text
def test_change_level_logging_disabled(caplog, cleanup_disabled_logging):
logging.disable(logging.CRITICAL)
assert logging.root.manager.disable == logging.CRITICAL
caplog.set_level(logging.WARNING)
logger.info("handler INFO level")
logger.warning("handler WARNING level")
caplog.set_level(logging.CRITICAL, logger=sublogger.name)
sublogger.warning("logger SUB_WARNING level")
sublogger.critical("logger SUB_CRITICAL level")
assert "INFO" not in caplog.text
assert "WARNING" in caplog.text
assert "SUB_WARNING" not in caplog.text
assert "SUB_CRITICAL" in caplog.text
def test_change_level_undo(pytester: Pytester) -> None: def test_change_level_undo(pytester: Pytester) -> None:
"""Ensure that 'set_level' is undone after the end of the test. """Ensure that 'set_level' is undone after the end of the test.
Tests the logging output themselves (affacted both by logger and handler levels). Tests the logging output themselves (affected both by logger and handler levels).
""" """
pytester.makepyfile( pytester.makepyfile(
""" """
@ -54,6 +85,37 @@ def test_change_level_undo(pytester: Pytester) -> None:
result.stdout.no_fnmatch_line("*log from test2*") result.stdout.no_fnmatch_line("*log from test2*")
def test_change_disabled_level_undo(
pytester: Pytester, cleanup_disabled_logging
) -> None:
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
Tests the logging output themselves (affected by disabled logging level).
"""
pytester.makepyfile(
"""
import logging
def test1(caplog):
logging.disable(logging.CRITICAL)
caplog.set_level(logging.INFO)
# using + operator here so fnmatch_lines doesn't match the code in the traceback
logging.info('log from ' + 'test1')
assert 0
def test2(caplog):
# using + operator here so fnmatch_lines doesn't match the code in the traceback
# use logging.warning because we need a level that will show up if logging.disabled
# isn't reset to ``CRITICAL`` after test1.
logging.warning('log from ' + 'test2')
assert 0
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"])
result.stdout.no_fnmatch_line("*log from test2*")
def test_change_level_undos_handler_level(pytester: Pytester) -> None: def test_change_level_undos_handler_level(pytester: Pytester) -> None:
"""Ensure that 'set_level' is undone after the end of the test (handler). """Ensure that 'set_level' is undone after the end of the test (handler).
@ -97,6 +159,65 @@ def test_with_statement(caplog):
assert "CRITICAL" in caplog.text assert "CRITICAL" in caplog.text
def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
logging.disable(logging.CRITICAL)
assert logging.root.manager.disable == logging.CRITICAL
with caplog.at_level(logging.WARNING):
logger.debug("handler DEBUG level")
logger.info("handler INFO level")
logger.warning("handler WARNING level")
logger.error("handler ERROR level")
logger.critical("handler CRITICAL level")
assert logging.root.manager.disable == logging.INFO
with caplog.at_level(logging.CRITICAL, logger=sublogger.name):
sublogger.warning("logger SUB_WARNING level")
sublogger.critical("logger SUB_CRITICAL level")
assert "DEBUG" not in caplog.text
assert "INFO" not in caplog.text
assert "WARNING" in caplog.text
assert "ERROR" in caplog.text
assert " CRITICAL" in caplog.text
assert "SUB_WARNING" not in caplog.text
assert "SUB_CRITICAL" in caplog.text
assert logging.root.manager.disable == logging.CRITICAL
@pytest.mark.parametrize(
"level_str,expected_disable_level",
[
("CRITICAL", logging.ERROR),
("ERROR", logging.WARNING),
("WARNING", logging.INFO),
("INFO", logging.DEBUG),
("DEBUG", logging.NOTSET),
("NOTSET", logging.NOTSET),
("NOTVALIDLEVEL", logging.NOTSET),
],
)
def test_force_enable_logging_level_string(
caplog, cleanup_disabled_logging, level_str, expected_disable_level
):
"""Test _force_enable_logging using a level string.
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
always needs to be *at least* one level lower than the level that caplog is trying to capture.
"""
test_logger = logging.getLogger("test_str_level_force_enable")
# Emulate a testing environment where all logging is disabled.
logging.disable(logging.CRITICAL)
# Make sure all logging is disabled.
assert not test_logger.isEnabledFor(logging.CRITICAL)
# Un-disable logging for `level_str`.
caplog._force_enable_logging(level_str, test_logger)
# Make sure that the disabled level is now one below the requested logging level.
# We don't use `isEnabledFor` here because that also checks the level set by
# `logging.setLevel()` which is irrelevant to `logging.disable()`.
assert test_logger.manager.disable == expected_disable_level
def test_log_access(caplog): def test_log_access(caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
logger.info("boo %s", "arg") logger.info("boo %s", "arg")

View File

@ -81,7 +81,7 @@ def test_root_logger_affected(pytester: Pytester) -> None:
# not the info one, because the default level of the root logger is # not the info one, because the default level of the root logger is
# WARNING. # WARNING.
assert os.path.isfile(log_file) assert os.path.isfile(log_file)
with open(log_file) as rfh: with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read() contents = rfh.read()
assert "info text going to logger" not in contents assert "info text going to logger" not in contents
assert "warning text going to logger" in contents assert "warning text going to logger" in contents
@ -656,7 +656,7 @@ def test_log_file_cli(pytester: Pytester) -> None:
# make sure that we get a '0' exit code for the testsuite # make sure that we get a '0' exit code for the testsuite
assert result.ret == 0 assert result.ret == 0
assert os.path.isfile(log_file) assert os.path.isfile(log_file)
with open(log_file) as rfh: with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read() contents = rfh.read()
assert "This log message will be shown" in contents assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents assert "This log message won't be shown" not in contents
@ -687,7 +687,7 @@ def test_log_file_cli_level(pytester: Pytester) -> None:
# make sure that we get a '0' exit code for the testsuite # make sure that we get a '0' exit code for the testsuite
assert result.ret == 0 assert result.ret == 0
assert os.path.isfile(log_file) assert os.path.isfile(log_file)
with open(log_file) as rfh: with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read() contents = rfh.read()
assert "This log message will be shown" in contents assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents assert "This log message won't be shown" not in contents
@ -738,7 +738,7 @@ def test_log_file_ini(pytester: Pytester) -> None:
# make sure that we get a '0' exit code for the testsuite # make sure that we get a '0' exit code for the testsuite
assert result.ret == 0 assert result.ret == 0
assert os.path.isfile(log_file) assert os.path.isfile(log_file)
with open(log_file) as rfh: with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read() contents = rfh.read()
assert "This log message will be shown" in contents assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents assert "This log message won't be shown" not in contents
@ -777,7 +777,7 @@ def test_log_file_ini_level(pytester: Pytester) -> None:
# make sure that we get a '0' exit code for the testsuite # make sure that we get a '0' exit code for the testsuite
assert result.ret == 0 assert result.ret == 0
assert os.path.isfile(log_file) assert os.path.isfile(log_file)
with open(log_file) as rfh: with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read() contents = rfh.read()
assert "This log message will be shown" in contents assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents assert "This log message won't be shown" not in contents
@ -985,7 +985,7 @@ def test_log_in_hooks(pytester: Pytester) -> None:
) )
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"]) result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"])
with open(log_file) as rfh: with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read() contents = rfh.read()
assert "sessionstart" in contents assert "sessionstart" in contents
assert "runtestloop" in contents assert "runtestloop" in contents
@ -1021,7 +1021,7 @@ def test_log_in_runtest_logreport(pytester: Pytester) -> None:
""" """
) )
pytester.runpytest() pytester.runpytest()
with open(log_file) as rfh: with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read() contents = rfh.read()
assert contents.count("logreport") == 3 assert contents.count("logreport") == 3
@ -1065,11 +1065,11 @@ def test_log_set_path(pytester: Pytester) -> None:
""" """
) )
pytester.runpytest() pytester.runpytest()
with open(os.path.join(report_dir_base, "test_first")) as rfh: with open(os.path.join(report_dir_base, "test_first"), encoding="utf-8") as rfh:
content = rfh.read() content = rfh.read()
assert "message from test 1" in content assert "message from test 1" in content
with open(os.path.join(report_dir_base, "test_second")) as rfh: with open(os.path.join(report_dir_base, "test_second"), encoding="utf-8") as rfh:
content = rfh.read() content = rfh.read()
assert "message from test 2" in content assert "message from test 2" in content
@ -1234,3 +1234,100 @@ def test_log_disabling_works_with_log_cli(pytester: Pytester) -> None:
"WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed." "WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed."
) )
assert not result.stderr.lines assert not result.stderr.lines
def test_without_date_format_log(pytester: Pytester) -> None:
"""Check that date is not printed by default."""
pytester.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.warning('text')
assert False
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(
["WARNING test_without_date_format_log:test_without_date_format_log.py:6 text"]
)
def test_date_format_log(pytester: Pytester) -> None:
"""Check that log_date_format affects output."""
pytester.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.warning('text')
assert False
"""
)
pytester.makeini(
"""
[pytest]
log_format=%(asctime)s; %(levelname)s; %(message)s
log_date_format=%Y-%m-%d %H:%M:%S
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}; WARNING; text"])
def test_date_format_percentf_log(pytester: Pytester) -> None:
"""Make sure that microseconds are printed in log."""
pytester.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.warning('text')
assert False
"""
)
pytester.makeini(
"""
[pytest]
log_format=%(asctime)s; %(levelname)s; %(message)s
log_date_format=%Y-%m-%d %H:%M:%S.%f
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}; WARNING; text"])
def test_date_format_percentf_tz_log(pytester: Pytester) -> None:
"""Make sure that timezone and microseconds are properly formatted together."""
pytester.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.warning('text')
assert False
"""
)
pytester.makeini(
"""
[pytest]
log_format=%(asctime)s; %(levelname)s; %(message)s
log_date_format=%Y-%m-%d %H:%M:%S.%f%z
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.re_match_lines(
[r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}[+-][0-9\.]+; WARNING; text"]
)

View File

@ -1,15 +1,15 @@
anyio[curio,trio]==3.6.2 anyio[curio,trio]==3.7.0
django==4.2.1 django==4.2.2
pytest-asyncio==0.21.0 pytest-asyncio==0.21.0
pytest-bdd==6.1.1 pytest-bdd==6.1.1
pytest-cov==4.0.0 pytest-cov==4.1.0
pytest-django==4.5.2 pytest-django==4.5.2
pytest-flakes==4.0.5 pytest-flakes==4.0.5
pytest-html==3.2.0 pytest-html==3.2.0
pytest-mock==3.10.0 pytest-mock==3.11.1
pytest-rerunfailures==11.1.2 pytest-rerunfailures==11.1.2
pytest-sugar==0.9.7 pytest-sugar==0.9.7
pytest-trio==0.7.0 pytest-trio==0.7.0
pytest-twisted==1.14.0 pytest-twisted==1.14.0
twisted==22.8.0 twisted==22.8.0
pytest-xvfb==2.0.0 pytest-xvfb==3.0.0

View File

@ -60,7 +60,8 @@ class TestModule:
""".format( """.format(
str(root2) str(root2)
) )
) ),
encoding="utf-8",
) )
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.chdir(root2) mp.chdir(root2)
@ -832,7 +833,8 @@ class TestConftestCustomization:
mod = outcome.get_result() mod = outcome.get_result()
mod.obj.hello = "world" mod.obj.hello = "world"
""" """
) ),
encoding="utf-8",
) )
b.joinpath("test_module.py").write_text( b.joinpath("test_module.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -840,7 +842,8 @@ class TestConftestCustomization:
def test_hello(): def test_hello():
assert hello == "world" assert hello == "world"
""" """
) ),
encoding="utf-8",
) )
reprec = pytester.inline_run() reprec = pytester.inline_run()
reprec.assertoutcome(passed=1) reprec.assertoutcome(passed=1)
@ -861,7 +864,8 @@ class TestConftestCustomization:
for func in result: for func in result:
func._some123 = "world" func._some123 = "world"
""" """
) ),
encoding="utf-8",
) )
b.joinpath("test_module.py").write_text( b.joinpath("test_module.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -874,7 +878,8 @@ class TestConftestCustomization:
def test_hello(obj): def test_hello(obj):
assert obj == "world" assert obj == "world"
""" """
) ),
encoding="utf-8",
) )
reprec = pytester.inline_run() reprec = pytester.inline_run()
reprec.assertoutcome(passed=1) reprec.assertoutcome(passed=1)
@ -897,25 +902,29 @@ class TestConftestCustomization:
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None: def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
"""Ensure we can collect files with weird file extensions as Python """Ensure we can collect files with weird file extensions as Python
modules (#2369)""" modules (#2369)"""
# We'll implement a little finder and loader to import files containing # Implement a little meta path finder to import files containing
# Python source code whose file extension is ".narf". # Python source code whose file extension is ".narf".
pytester.makeconftest( pytester.makeconftest(
""" """
import sys, os, imp import sys
import os.path
from importlib.util import spec_from_loader
from importlib.machinery import SourceFileLoader
from _pytest.python import Module from _pytest.python import Module
class Loader(object): class MetaPathFinder:
def load_module(self, name): def find_spec(self, fullname, path, target=None):
return imp.load_source(name, name + ".narf") if os.path.exists(fullname + ".narf"):
class Finder(object): return spec_from_loader(
def find_module(self, name, path=None): fullname,
if os.path.exists(name + ".narf"): SourceFileLoader(fullname, fullname + ".narf"),
return Loader() )
sys.meta_path.append(Finder()) sys.meta_path.append(MetaPathFinder())
def pytest_collect_file(file_path, parent): def pytest_collect_file(file_path, parent):
if file_path.suffix == ".narf": if file_path.suffix == ".narf":
return Module.from_parent(path=file_path, parent=parent)""" return Module.from_parent(path=file_path, parent=parent)
"""
) )
pytester.makefile( pytester.makefile(
".narf", ".narf",
@ -970,7 +979,8 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
def pytest_runtest_teardown(item): def pytest_runtest_teardown(item):
assert item.path.stem == "test_in_sub1" assert item.path.stem == "test_in_sub1"
""" """
) ),
encoding="utf-8",
) )
sub2.joinpath("conftest.py").write_text( sub2.joinpath("conftest.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -983,10 +993,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
def pytest_runtest_teardown(item): def pytest_runtest_teardown(item):
assert item.path.stem == "test_in_sub2" assert item.path.stem == "test_in_sub2"
""" """
) ),
encoding="utf-8",
) )
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass") sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass", encoding="utf-8")
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass") sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass", encoding="utf-8")
result = pytester.runpytest("-v", "-s") result = pytester.runpytest("-v", "-s")
result.assert_outcomes(passed=2) result.assert_outcomes(passed=2)
@ -1003,9 +1014,9 @@ class TestTracebackCutting:
with pytest.raises(pytest.skip.Exception) as excinfo: with pytest.raises(pytest.skip.Exception) as excinfo:
pytest.skip("xxx") pytest.skip("xxx")
assert excinfo.traceback[-1].frame.code.name == "skip" assert excinfo.traceback[-1].frame.code.name == "skip"
assert excinfo.traceback[-1].ishidden() assert excinfo.traceback[-1].ishidden(excinfo)
assert excinfo.traceback[-2].frame.code.name == "test_skip_simple" assert excinfo.traceback[-2].frame.code.name == "test_skip_simple"
assert not excinfo.traceback[-2].ishidden() assert not excinfo.traceback[-2].ishidden(excinfo)
def test_traceback_argsetup(self, pytester: Pytester) -> None: def test_traceback_argsetup(self, pytester: Pytester) -> None:
pytester.makeconftest( pytester.makeconftest(
@ -1374,7 +1385,8 @@ def test_skip_duplicates_by_default(pytester: Pytester) -> None:
def test_real(): def test_real():
pass pass
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest(str(a), str(a)) result = pytester.runpytest(str(a), str(a))
result.stdout.fnmatch_lines(["*collected 1 item*"]) result.stdout.fnmatch_lines(["*collected 1 item*"])
@ -1394,7 +1406,8 @@ def test_keep_duplicates(pytester: Pytester) -> None:
def test_real(): def test_real():
pass pass
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest("--keep-duplicates", str(a), str(a)) result = pytester.runpytest("--keep-duplicates", str(a), str(a))
result.stdout.fnmatch_lines(["*collected 2 item*"]) result.stdout.fnmatch_lines(["*collected 2 item*"])
@ -1407,10 +1420,15 @@ def test_package_collection_infinite_recursion(pytester: Pytester) -> None:
def test_package_collection_init_given_as_argument(pytester: Pytester) -> None: def test_package_collection_init_given_as_argument(pytester: Pytester) -> None:
"""Regression test for #3749""" """Regression test for #3749, #8976, #9263, #9313.
Specifying an __init__.py file directly should collect only the __init__.py
Module, not the entire package.
"""
p = pytester.copy_example("collect/package_init_given_as_arg") p = pytester.copy_example("collect/package_init_given_as_arg")
result = pytester.runpytest(p / "pkg" / "__init__.py") items, hookrecorder = pytester.inline_genitems(p / "pkg" / "__init__.py")
result.stdout.fnmatch_lines(["*1 passed*"]) assert len(items) == 1
assert items[0].name == "test_init"
def test_package_with_modules(pytester: Pytester) -> None: def test_package_with_modules(pytester: Pytester) -> None:
@ -1439,8 +1457,12 @@ def test_package_with_modules(pytester: Pytester) -> None:
sub2_test = sub2.joinpath("test") sub2_test = sub2.joinpath("test")
sub2_test.mkdir(parents=True) sub2_test.mkdir(parents=True)
sub1_test.joinpath("test_in_sub1.py").write_text("def test_1(): pass") sub1_test.joinpath("test_in_sub1.py").write_text(
sub2_test.joinpath("test_in_sub2.py").write_text("def test_2(): pass") "def test_1(): pass", encoding="utf-8"
)
sub2_test.joinpath("test_in_sub2.py").write_text(
"def test_2(): pass", encoding="utf-8"
)
# Execute from . # Execute from .
result = pytester.runpytest("-v", "-s") result = pytester.runpytest("-v", "-s")
@ -1484,9 +1506,11 @@ def test_package_ordering(pytester: Pytester) -> None:
sub2_test = sub2.joinpath("test") sub2_test = sub2.joinpath("test")
sub2_test.mkdir(parents=True) sub2_test.mkdir(parents=True)
root.joinpath("Test_root.py").write_text("def test_1(): pass") root.joinpath("Test_root.py").write_text("def test_1(): pass", encoding="utf-8")
sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass") sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass", encoding="utf-8")
sub2_test.joinpath("test_sub2.py").write_text("def test_3(): pass") sub2_test.joinpath("test_sub2.py").write_text(
"def test_3(): pass", encoding="utf-8"
)
# Execute from . # Execute from .
result = pytester.runpytest("-v", "-s") result = pytester.runpytest("-v", "-s")

View File

@ -287,7 +287,8 @@ class TestFillFixtures:
def spam(): def spam():
return 'spam' return 'spam'
""" """
) ),
encoding="utf-8",
) )
testfile = subdir.joinpath("test_spam.py") testfile = subdir.joinpath("test_spam.py")
testfile.write_text( testfile.write_text(
@ -296,7 +297,8 @@ class TestFillFixtures:
def test_spam(spam): def test_spam(spam):
assert spam == "spam" assert spam == "spam"
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.fnmatch_lines(["*1 passed*"])
@ -359,7 +361,8 @@ class TestFillFixtures:
def spam(request): def spam(request):
return request.param return request.param
""" """
) ),
encoding="utf-8",
) )
testfile = subdir.joinpath("test_spam.py") testfile = subdir.joinpath("test_spam.py")
testfile.write_text( testfile.write_text(
@ -371,7 +374,8 @@ class TestFillFixtures:
assert spam == params['spam'] assert spam == params['spam']
params['spam'] += 1 params['spam'] += 1
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["*3 passed*"]) result.stdout.fnmatch_lines(["*3 passed*"])
@ -403,7 +407,8 @@ class TestFillFixtures:
def spam(request): def spam(request):
return request.param return request.param
""" """
) ),
encoding="utf-8",
) )
testfile = subdir.joinpath("test_spam.py") testfile = subdir.joinpath("test_spam.py")
testfile.write_text( testfile.write_text(
@ -415,7 +420,8 @@ class TestFillFixtures:
assert spam == params['spam'] assert spam == params['spam']
params['spam'] += 1 params['spam'] += 1
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["*3 passed*"]) result.stdout.fnmatch_lines(["*3 passed*"])
@ -1037,10 +1043,11 @@ class TestRequestBasic:
def arg1(): def arg1():
pass pass
""" """
) ),
encoding="utf-8",
) )
p = b.joinpath("test_module.py") p = b.joinpath("test_module.py")
p.write_text("def test_func(arg1): pass") p.write_text("def test_func(arg1): pass", encoding="utf-8")
result = pytester.runpytest(p, "--fixtures") result = pytester.runpytest(p, "--fixtures")
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
@ -1617,7 +1624,8 @@ class TestFixtureManagerParseFactories:
def one(): def one():
return 1 return 1
""" """
) ),
encoding="utf-8",
) )
package.joinpath("test_x.py").write_text( package.joinpath("test_x.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -1625,7 +1633,8 @@ class TestFixtureManagerParseFactories:
def test_x(one): def test_x(one):
assert one == 1 assert one == 1
""" """
) ),
encoding="utf-8",
) )
sub = package.joinpath("sub") sub = package.joinpath("sub")
sub.mkdir() sub.mkdir()
@ -1638,7 +1647,8 @@ class TestFixtureManagerParseFactories:
def one(): def one():
return 2 return 2
""" """
) ),
encoding="utf-8",
) )
sub.joinpath("test_y.py").write_text( sub.joinpath("test_y.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -1646,7 +1656,8 @@ class TestFixtureManagerParseFactories:
def test_x(one): def test_x(one):
assert one == 2 assert one == 2
""" """
) ),
encoding="utf-8",
) )
reprec = pytester.inline_run() reprec = pytester.inline_run()
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
@ -1671,7 +1682,8 @@ class TestFixtureManagerParseFactories:
def teardown_module(): def teardown_module():
values[:] = [] values[:] = []
""" """
) ),
encoding="utf-8",
) )
package.joinpath("test_x.py").write_text( package.joinpath("test_x.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -1680,7 +1692,8 @@ class TestFixtureManagerParseFactories:
def test_x(): def test_x():
assert values == ["package"] assert values == ["package"]
""" """
) ),
encoding="utf-8",
) )
package = pytester.mkdir("package2") package = pytester.mkdir("package2")
package.joinpath("__init__.py").write_text( package.joinpath("__init__.py").write_text(
@ -1692,7 +1705,8 @@ class TestFixtureManagerParseFactories:
def teardown_module(): def teardown_module():
values[:] = [] values[:] = []
""" """
) ),
encoding="utf-8",
) )
package.joinpath("test_x.py").write_text( package.joinpath("test_x.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -1701,7 +1715,8 @@ class TestFixtureManagerParseFactories:
def test_x(): def test_x():
assert values == ["package2"] assert values == ["package2"]
""" """
) ),
encoding="utf-8",
) )
reprec = pytester.inline_run() reprec = pytester.inline_run()
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
@ -1714,7 +1729,7 @@ class TestFixtureManagerParseFactories:
) )
pytester.syspathinsert(pytester.path.name) pytester.syspathinsert(pytester.path.name)
package = pytester.mkdir("package") package = pytester.mkdir("package")
package.joinpath("__init__.py").write_text("") package.joinpath("__init__.py").write_text("", encoding="utf-8")
package.joinpath("conftest.py").write_text( package.joinpath("conftest.py").write_text(
textwrap.dedent( textwrap.dedent(
"""\ """\
@ -1731,7 +1746,8 @@ class TestFixtureManagerParseFactories:
yield values yield values
values.pop() values.pop()
""" """
) ),
encoding="utf-8",
) )
package.joinpath("test_x.py").write_text( package.joinpath("test_x.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -1742,7 +1758,8 @@ class TestFixtureManagerParseFactories:
def test_package(one): def test_package(one):
assert values == ["package-auto", "package"] assert values == ["package-auto", "package"]
""" """
) ),
encoding="utf-8",
) )
reprec = pytester.inline_run() reprec = pytester.inline_run()
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
@ -1892,8 +1909,12 @@ class TestAutouseDiscovery:
""" """
) )
conftest.rename(a.joinpath(conftest.name)) conftest.rename(a.joinpath(conftest.name))
a.joinpath("test_something.py").write_text("def test_func(): pass") a.joinpath("test_something.py").write_text(
b.joinpath("test_otherthing.py").write_text("def test_func(): pass") "def test_func(): pass", encoding="utf-8"
)
b.joinpath("test_otherthing.py").write_text(
"def test_func(): pass", encoding="utf-8"
)
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" """
@ -1939,7 +1960,8 @@ class TestAutouseManagement:
import sys import sys
sys._myapp = "hello" sys._myapp = "hello"
""" """
) ),
encoding="utf-8",
) )
sub = pkgdir.joinpath("tests") sub = pkgdir.joinpath("tests")
sub.mkdir() sub.mkdir()
@ -1952,7 +1974,8 @@ class TestAutouseManagement:
def test_app(): def test_app():
assert sys._myapp == "hello" assert sys._myapp == "hello"
""" """
) ),
encoding="utf-8",
) )
reprec = pytester.inline_run("-s") reprec = pytester.inline_run("-s")
reprec.assertoutcome(passed=1) reprec.assertoutcome(passed=1)
@ -2882,7 +2905,7 @@ class TestFixtureMarker:
def browser(request): def browser(request):
def finalize(): def finalize():
sys.stdout.write_text('Finalized') sys.stdout.write_text('Finalized', encoding='utf-8')
request.addfinalizer(finalize) request.addfinalizer(finalize)
return {} return {}
""" """
@ -2900,7 +2923,8 @@ class TestFixtureMarker:
def test_browser(browser): def test_browser(browser):
assert browser['visited'] is True assert browser['visited'] is True
""" """
) ),
encoding="utf-8",
) )
reprec = pytester.runpytest("-s") reprec = pytester.runpytest("-s")
for test in ["test_browser"]: for test in ["test_browser"]:
@ -3855,7 +3879,8 @@ class TestParameterizedSubRequest:
def fix_with_param(request): def fix_with_param(request):
return request.param return request.param
""" """
) ),
encoding="utf-8",
) )
testfile = tests_dir.joinpath("test_foos.py") testfile = tests_dir.joinpath("test_foos.py")
@ -3867,7 +3892,8 @@ class TestParameterizedSubRequest:
def test_foo(request): def test_foo(request):
request.getfixturevalue('fix_with_param') request.getfixturevalue('fix_with_param')
""" """
) ),
encoding="utf-8",
) )
os.chdir(tests_dir) os.chdir(tests_dir)
@ -4196,7 +4222,7 @@ class TestScopeOrdering:
test_2.py test_2.py
""" """
root = pytester.mkdir("root") root = pytester.mkdir("root")
root.joinpath("__init__.py").write_text("values = []") root.joinpath("__init__.py").write_text("values = []", encoding="utf-8")
sub1 = root.joinpath("sub1") sub1 = root.joinpath("sub1")
sub1.mkdir() sub1.mkdir()
sub1.joinpath("__init__.py").touch() sub1.joinpath("__init__.py").touch()
@ -4211,7 +4237,8 @@ class TestScopeOrdering:
yield values yield values
assert values.pop() == "pre-sub1" assert values.pop() == "pre-sub1"
""" """
) ),
encoding="utf-8",
) )
sub1.joinpath("test_1.py").write_text( sub1.joinpath("test_1.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -4220,7 +4247,8 @@ class TestScopeOrdering:
def test_1(fix): def test_1(fix):
assert values == ["pre-sub1"] assert values == ["pre-sub1"]
""" """
) ),
encoding="utf-8",
) )
sub2 = root.joinpath("sub2") sub2 = root.joinpath("sub2")
sub2.mkdir() sub2.mkdir()
@ -4236,7 +4264,8 @@ class TestScopeOrdering:
yield values yield values
assert values.pop() == "pre-sub2" assert values.pop() == "pre-sub2"
""" """
) ),
encoding="utf-8",
) )
sub2.joinpath("test_2.py").write_text( sub2.joinpath("test_2.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -4245,7 +4274,8 @@ class TestScopeOrdering:
def test_2(fix): def test_2(fix):
assert values == ["pre-sub2"] assert values == ["pre-sub2"]
""" """
) ),
encoding="utf-8",
) )
reprec = pytester.inline_run() reprec = pytester.inline_run()
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)

View File

@ -19,7 +19,6 @@ from hypothesis import strategies
import pytest import pytest
from _pytest import fixtures from _pytest import fixtures
from _pytest import python from _pytest import python
from _pytest.compat import _format_args
from _pytest.compat import getfuncargnames from _pytest.compat import getfuncargnames
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.outcomes import fail from _pytest.outcomes import fail
@ -1036,27 +1035,6 @@ class TestMetafunc:
""" """
) )
def test_format_args(self) -> None:
def function1():
pass
assert _format_args(function1) == "()"
def function2(arg1):
pass
assert _format_args(function2) == "(arg1)"
def function3(arg1, arg2="qwe"):
pass
assert _format_args(function3) == "(arg1, arg2='qwe')"
def function4(arg1, *args, **kwargs):
pass
assert _format_args(function4) == "(arg1, *args, **kwargs)"
class TestMetafuncFunctional: class TestMetafuncFunctional:
def test_attributes(self, pytester: Pytester) -> None: def test_attributes(self, pytester: Pytester) -> None:
@ -1443,7 +1421,8 @@ class TestMetafuncFunctional:
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
assert metafunc.function.__name__ == "test_1" assert metafunc.function.__name__ == "test_1"
""" """
) ),
encoding="utf-8",
) )
sub2.joinpath("conftest.py").write_text( sub2.joinpath("conftest.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -1451,10 +1430,15 @@ class TestMetafuncFunctional:
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
assert metafunc.function.__name__ == "test_2" assert metafunc.function.__name__ == "test_2"
""" """
) ),
encoding="utf-8",
)
sub1.joinpath("test_in_sub1.py").write_text(
"def test_1(): pass", encoding="utf-8"
)
sub2.joinpath("test_in_sub2.py").write_text(
"def test_2(): pass", encoding="utf-8"
) )
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1) result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1)
result.assert_outcomes(passed=3) result.assert_outcomes(passed=3)

View File

@ -199,8 +199,8 @@ class TestImportHookInstallation:
return check return check
""", """,
"mainwrapper.py": """\ "mainwrapper.py": """\
import importlib.metadata
import pytest import pytest
from _pytest.compat import importlib_metadata
class DummyEntryPoint(object): class DummyEntryPoint(object):
name = 'spam' name = 'spam'
@ -220,7 +220,7 @@ class TestImportHookInstallation:
def distributions(): def distributions():
return (DummyDistInfo(),) return (DummyDistInfo(),)
importlib_metadata.distributions = distributions importlib.metadata.distributions = distributions
pytest.main() pytest.main()
""", """,
"test_foo.py": """\ "test_foo.py": """\
@ -1392,14 +1392,14 @@ def test_sequence_comparison_uses_repr(pytester: Pytester) -> None:
def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None:
pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"]) pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"])
a = pytester.mkdir("a") a = pytester.mkdir("a")
a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2") a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8")
a.joinpath("conftest.py").write_text( a.joinpath("conftest.py").write_text(
'def pytest_assertrepr_compare(): return ["summary a"]' 'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8"
) )
b = pytester.mkdir("b") b = pytester.mkdir("b")
b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2") b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8")
b.joinpath("conftest.py").write_text( b.joinpath("conftest.py").write_text(
'def pytest_assertrepr_compare(): return ["summary b"]' 'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8"
) )
result = pytester.runpytest() result = pytester.runpytest()

View File

@ -131,9 +131,8 @@ class TestAssertionRewrite:
for n in [node, *ast.iter_child_nodes(node)]: for n in [node, *ast.iter_child_nodes(node)]:
assert n.lineno == 3 assert n.lineno == 3
assert n.col_offset == 0 assert n.col_offset == 0
if sys.version_info >= (3, 8): assert n.end_lineno == 6
assert n.end_lineno == 6 assert n.end_col_offset == 3
assert n.end_col_offset == 3
def test_dont_rewrite(self) -> None: def test_dont_rewrite(self) -> None:
s = """'PYTEST_DONT_REWRITE'\nassert 14""" s = """'PYTEST_DONT_REWRITE'\nassert 14"""
@ -160,7 +159,8 @@ class TestAssertionRewrite:
"def special_asserter():\n" "def special_asserter():\n"
" def special_assert(x, y):\n" " def special_assert(x, y):\n"
" assert x == y\n" " assert x == y\n"
" return special_assert\n" " return special_assert\n",
encoding="utf-8",
) )
pytester.makeconftest('pytest_plugins = ["plugin"]') pytester.makeconftest('pytest_plugins = ["plugin"]')
pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n") pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n")
@ -173,7 +173,9 @@ class TestAssertionRewrite:
pytester.makepyfile(test_y="x = 1") pytester.makepyfile(test_y="x = 1")
xdir = pytester.mkdir("x") xdir = pytester.mkdir("x")
pytester.mkpydir(str(xdir.joinpath("test_Y"))) pytester.mkpydir(str(xdir.joinpath("test_Y")))
xdir.joinpath("test_Y").joinpath("__init__.py").write_text("x = 2") xdir.joinpath("test_Y").joinpath("__init__.py").write_text(
"x = 2", encoding="utf-8"
)
pytester.makepyfile( pytester.makepyfile(
"import test_y\n" "import test_y\n"
"import test_Y\n" "import test_Y\n"
@ -726,7 +728,7 @@ class TestAssertionRewrite:
class TestRewriteOnImport: class TestRewriteOnImport:
def test_pycache_is_a_file(self, pytester: Pytester) -> None: def test_pycache_is_a_file(self, pytester: Pytester) -> None:
pytester.path.joinpath("__pycache__").write_text("Hello") pytester.path.joinpath("__pycache__").write_text("Hello", encoding="utf-8")
pytester.makepyfile( pytester.makepyfile(
""" """
def test_rewritten(): def test_rewritten():
@ -903,7 +905,8 @@ def test_rewritten():
pkg.joinpath("test_blah.py").write_text( pkg.joinpath("test_blah.py").write_text(
""" """
def test_rewritten(): def test_rewritten():
assert "@py_builtins" in globals()""" assert "@py_builtins" in globals()""",
encoding="utf-8",
) )
assert pytester.runpytest().ret == 0 assert pytester.runpytest().ret == 0
@ -1066,7 +1069,7 @@ class TestAssertionRewriteHookDetails:
source = tmp_path / "source.py" source = tmp_path / "source.py"
pyc = Path(str(source) + "c") pyc = Path(str(source) + "c")
source.write_text("def test(): pass") source.write_text("def test(): pass", encoding="utf-8")
py_compile.compile(str(source), str(pyc)) py_compile.compile(str(source), str(pyc))
contents = pyc.read_bytes() contents = pyc.read_bytes()
@ -1092,7 +1095,7 @@ class TestAssertionRewriteHookDetails:
fn = tmp_path / "source.py" fn = tmp_path / "source.py"
pyc = Path(str(fn) + "c") pyc = Path(str(fn) + "c")
fn.write_text("def test(): assert True") fn.write_text("def test(): assert True", encoding="utf-8")
source_stat, co = _rewrite_test(fn, config) source_stat, co = _rewrite_test(fn, config)
_write_pyc(state, co, source_stat, pyc) _write_pyc(state, co, source_stat, pyc)
@ -1157,7 +1160,7 @@ class TestAssertionRewriteHookDetails:
return False return False
def rewrite_self(): def rewrite_self():
with open(__file__, 'w') as self: with open(__file__, 'w', encoding='utf-8') as self:
self.write('def reloaded(): return True') self.write('def reloaded(): return True')
""", """,
test_fun=""" test_fun="""
@ -1187,9 +1190,10 @@ class TestAssertionRewriteHookDetails:
data = pkgutil.get_data('foo.test_foo', 'data.txt') data = pkgutil.get_data('foo.test_foo', 'data.txt')
assert data == b'Hey' assert data == b'Hey'
""" """
) ),
encoding="utf-8",
) )
path.joinpath("data.txt").write_text("Hey") path.joinpath("data.txt").write_text("Hey", encoding="utf-8")
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.fnmatch_lines(["*1 passed*"])
@ -1265,9 +1269,6 @@ class TestIssue2121:
result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"]) result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"])
@pytest.mark.skipif(
sys.version_info < (3, 8), reason="walrus operator not available in py<38"
)
class TestIssue10743: class TestIssue10743:
def test_assertion_walrus_operator(self, pytester: Pytester) -> None: def test_assertion_walrus_operator(self, pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(
@ -1436,6 +1437,93 @@ class TestIssue10743:
assert result.ret == 0 assert result.ret == 0
class TestIssue11028:
def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_in_string():
assert (obj := "foo") in obj
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_in_operand_json_dumps(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
import json
def test_json_encoder():
assert (obj := "foo") in json.dumps(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a):
return a
def test_call_other_function_arg():
assert (obj := "foo") == f(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function_keyword_arg(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a='test'):
return a
def test_call_other_function_k_arg():
assert (obj := "foo") == f(a=obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function_arg_as_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a='test'):
return a
def test_function_of_function():
assert (obj := "foo") == f(f(obj))
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_gt_operand_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def add_one(a):
return a + 1
def test_gt():
assert (obj := 4) > add_one(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
@pytest.mark.skipif( @pytest.mark.skipif(
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems" sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
) )
@ -1862,16 +1950,10 @@ class TestPyCacheDir:
) )
def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None: def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None:
monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False)
if prefix is not None and sys.version_info < (3, 8):
pytest.skip("pycache_prefix not available in py<38")
monkeypatch.setattr(sys, "pycache_prefix", prefix, raising=False) monkeypatch.setattr(sys, "pycache_prefix", prefix, raising=False)
assert get_cache_dir(Path(source)) == Path(expected) assert get_cache_dir(Path(source)) == Path(expected)
@pytest.mark.skipif(
sys.version_info < (3, 8), reason="pycache_prefix not available in py<38"
)
@pytest.mark.skipif( @pytest.mark.skipif(
sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"), sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"),
reason="#9298", reason="#9298",

View File

@ -38,7 +38,9 @@ class TestNewAPI:
@pytest.mark.filterwarnings("ignore:could not create cache path") @pytest.mark.filterwarnings("ignore:could not create cache path")
def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None: def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None:
pytester.makeini("[pytest]") pytester.makeini("[pytest]")
pytester.path.joinpath(".pytest_cache").write_text("gone wrong") pytester.path.joinpath(".pytest_cache").write_text(
"gone wrong", encoding="utf-8"
)
config = pytester.parseconfigure() config = pytester.parseconfigure()
cache = config.cache cache = config.cache
assert cache is not None assert cache is not None
@ -87,7 +89,7 @@ class TestNewAPI:
"*= warnings summary =*", "*= warnings summary =*",
"*/cacheprovider.py:*", "*/cacheprovider.py:*",
" */cacheprovider.py:*: PytestCacheWarning: could not create cache path " " */cacheprovider.py:*: PytestCacheWarning: could not create cache path "
f"{unwritable_cache_dir}/v/cache/nodeids", f"{unwritable_cache_dir}/v/cache/nodeids: *",
' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))', ' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))',
"*1 failed, 3 warnings in*", "*1 failed, 3 warnings in*",
] ]
@ -420,7 +422,13 @@ class TestLastFailed:
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 failed in*"]) result.stdout.fnmatch_lines(["*1 failed in*"])
def test_terminal_report_lastfailed(self, pytester: Pytester) -> None: @pytest.mark.parametrize("parent", ("session", "package"))
def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None:
if parent == "package":
pytester.makepyfile(
__init__="",
)
test_a = pytester.makepyfile( test_a = pytester.makepyfile(
test_a=""" test_a="""
def test_a1(): pass def test_a1(): pass
@ -848,6 +856,33 @@ class TestLastFailed:
] ]
) )
def test_lastfailed_skip_collection_with_nesting(self, pytester: Pytester) -> None:
"""Check that file skipping works even when the file with failures is
nested at a different level of the collection tree."""
pytester.makepyfile(
**{
"test_1.py": """
def test_1(): pass
""",
"pkg/__init__.py": "",
"pkg/test_2.py": """
def test_2(): assert False
""",
}
)
# first run
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 2 items", "*1 failed*1 passed*"])
# second run - test_1.py is skipped.
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
"*= 1 failed in *",
]
)
def test_lastfailed_with_known_failures_not_being_selected( def test_lastfailed_with_known_failures_not_being_selected(
self, pytester: Pytester self, pytester: Pytester
) -> None: ) -> None:
@ -1052,6 +1087,28 @@ class TestLastFailed:
result = pytester.runpytest("--lf") result = pytester.runpytest("--lf")
result.assert_outcomes(failed=3) result.assert_outcomes(failed=3)
def test_non_python_file_skipped(
self,
pytester: Pytester,
dummy_yaml_custom_test: None,
) -> None:
pytester.makepyfile(
**{
"test_bad.py": """def test_bad(): assert False""",
},
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
"* 1 failed in *",
]
)
class TestNewFirst: class TestNewFirst:
def test_newfirst_usecase(self, pytester: Pytester) -> None: def test_newfirst_usecase(self, pytester: Pytester) -> None:
@ -1079,7 +1136,9 @@ class TestNewFirst:
["*test_2/test_2.py::test_1 PASSED*", "*test_1/test_1.py::test_1 PASSED*"] ["*test_2/test_2.py::test_1 PASSED*", "*test_1/test_1.py::test_1 PASSED*"]
) )
p1.write_text("def test_1(): assert 1\n" "def test_2(): assert 1\n") p1.write_text(
"def test_1(): assert 1\n" "def test_2(): assert 1\n", encoding="utf-8"
)
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
result = pytester.runpytest("--nf", "--collect-only", "-q") result = pytester.runpytest("--nf", "--collect-only", "-q")
@ -1152,7 +1211,8 @@ class TestNewFirst:
p1.write_text( p1.write_text(
"import pytest\n" "import pytest\n"
"@pytest.mark.parametrize('num', [1, 2, 3])\n" "@pytest.mark.parametrize('num', [1, 2, 3])\n"
"def test_1(num): assert num\n" "def test_1(num): assert num\n",
encoding="utf-8",
) )
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
@ -1204,7 +1264,7 @@ def test_gitignore(pytester: Pytester) -> None:
assert gitignore_path.read_text(encoding="UTF-8") == msg assert gitignore_path.read_text(encoding="UTF-8") == msg
# Does not overwrite existing/custom one. # Does not overwrite existing/custom one.
gitignore_path.write_text("custom") gitignore_path.write_text("custom", encoding="utf-8")
cache.set("something", "else") cache.set("something", "else")
assert gitignore_path.read_text(encoding="UTF-8") == "custom" assert gitignore_path.read_text(encoding="UTF-8") == "custom"

View File

@ -750,9 +750,10 @@ def test_setup_failure_does_not_kill_capturing(pytester: Pytester) -> None:
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
raise ValueError(42) raise ValueError(42)
""" """
) ),
encoding="utf-8",
) )
sub1.joinpath("test_mod.py").write_text("def test_func1(): pass") sub1.joinpath("test_mod.py").write_text("def test_func1(): pass", encoding="utf-8")
result = pytester.runpytest(pytester.path, "--traceconfig") result = pytester.runpytest(pytester.path, "--traceconfig")
result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"]) result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"])
@ -1523,9 +1524,9 @@ def test_global_capture_with_live_logging(pytester: Pytester) -> None:
def pytest_runtest_logreport(report): def pytest_runtest_logreport(report):
if "test_global" in report.nodeid: if "test_global" in report.nodeid:
if report.when == "teardown": if report.when == "teardown":
with open("caplog", "w") as f: with open("caplog", "w", encoding="utf-8") as f:
f.write(report.caplog) f.write(report.caplog)
with open("capstdout", "w") as f: with open("capstdout", "w", encoding="utf-8") as f:
f.write(report.capstdout) f.write(report.capstdout)
""" """
) )
@ -1555,14 +1556,14 @@ def test_global_capture_with_live_logging(pytester: Pytester) -> None:
result = pytester.runpytest_subprocess("--log-cli-level=INFO") result = pytester.runpytest_subprocess("--log-cli-level=INFO")
assert result.ret == 0 assert result.ret == 0
with open("caplog") as f: with open("caplog", encoding="utf-8") as f:
caplog = f.read() caplog = f.read()
assert "fix setup" in caplog assert "fix setup" in caplog
assert "something in test" in caplog assert "something in test" in caplog
assert "fix teardown" in caplog assert "fix teardown" in caplog
with open("capstdout") as f: with open("capstdout", encoding="utf-8") as f:
capstdout = f.read() capstdout = f.read()
assert "fix setup" in capstdout assert "fix setup" in capstdout

View File

@ -140,7 +140,7 @@ class TestCollectFS:
ensure_file(tmp_path / ".bzr" / "test_notfound.py") ensure_file(tmp_path / ".bzr" / "test_notfound.py")
ensure_file(tmp_path / "normal" / "test_found.py") ensure_file(tmp_path / "normal" / "test_found.py")
for x in tmp_path.rglob("test_*.py"): for x in tmp_path.rglob("test_*.py"):
x.write_text("def test_hello(): pass", "utf-8") x.write_text("def test_hello(): pass", encoding="utf-8")
result = pytester.runpytest("--collect-only") result = pytester.runpytest("--collect-only")
s = result.stdout.str() s = result.stdout.str()
@ -162,7 +162,7 @@ class TestCollectFS:
bindir = "Scripts" if sys.platform.startswith("win") else "bin" bindir = "Scripts" if sys.platform.startswith("win") else "bin"
ensure_file(pytester.path / "virtual" / bindir / fname) ensure_file(pytester.path / "virtual" / bindir / fname)
testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py") testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py")
testfile.write_text("def test_hello(): pass") testfile.write_text("def test_hello(): pass", encoding="utf-8")
# by default, ignore tests inside a virtualenv # by default, ignore tests inside a virtualenv
result = pytester.runpytest() result = pytester.runpytest()
@ -192,7 +192,7 @@ class TestCollectFS:
# norecursedirs takes priority # norecursedirs takes priority
ensure_file(pytester.path / ".virtual" / bindir / fname) ensure_file(pytester.path / ".virtual" / bindir / fname)
testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py") testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py")
testfile.write_text("def test_hello(): pass") testfile.write_text("def test_hello(): pass", encoding="utf-8")
result = pytester.runpytest("--collect-in-virtualenv") result = pytester.runpytest("--collect-in-virtualenv")
result.stdout.no_fnmatch_line("*test_invenv*") result.stdout.no_fnmatch_line("*test_invenv*")
# ...unless the virtualenv is explicitly given on the CLI # ...unless the virtualenv is explicitly given on the CLI
@ -231,10 +231,14 @@ class TestCollectFS:
) )
tmp_path = pytester.path tmp_path = pytester.path
ensure_file(tmp_path / "mydir" / "test_hello.py").write_text( ensure_file(tmp_path / "mydir" / "test_hello.py").write_text(
"def test_1(): pass" "def test_1(): pass", encoding="utf-8"
)
ensure_file(tmp_path / "xyz123" / "test_2.py").write_text(
"def test_2(): 0/0", encoding="utf-8"
)
ensure_file(tmp_path / "xy" / "test_ok.py").write_text(
"def test_3(): pass", encoding="utf-8"
) )
ensure_file(tmp_path / "xyz123" / "test_2.py").write_text("def test_2(): 0/0")
ensure_file(tmp_path / "xy" / "test_ok.py").write_text("def test_3(): pass")
rec = pytester.inline_run() rec = pytester.inline_run()
rec.assertoutcome(passed=1) rec.assertoutcome(passed=1)
rec = pytester.inline_run("xyz123/test_2.py") rec = pytester.inline_run("xyz123/test_2.py")
@ -248,12 +252,14 @@ class TestCollectFS:
""" """
) )
tmp_path = pytester.path tmp_path = pytester.path
ensure_file(tmp_path / "a" / "test_1.py").write_text("def test_a(): pass") ensure_file(tmp_path / "a" / "test_1.py").write_text(
"def test_a(): pass", encoding="utf-8"
)
ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text( ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text(
"def test_b(): pass" "def test_b(): pass", encoding="utf-8"
) )
ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text( ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text(
"def test_c(): pass" "def test_c(): pass", encoding="utf-8"
) )
# executing from rootdir only tests from `testpaths` directories # executing from rootdir only tests from `testpaths` directories
@ -349,8 +355,8 @@ class TestCustomConftests:
""" """
) )
sub = pytester.mkdir("xy123") sub = pytester.mkdir("xy123")
ensure_file(sub / "test_hello.py").write_text("syntax error") ensure_file(sub / "test_hello.py").write_text("syntax error", encoding="utf-8")
sub.joinpath("conftest.py").write_text("syntax error") sub.joinpath("conftest.py").write_text("syntax error", encoding="utf-8")
pytester.makepyfile("def test_hello(): pass") pytester.makepyfile("def test_hello(): pass")
pytester.makepyfile(test_one="syntax error") pytester.makepyfile(test_one="syntax error")
result = pytester.runpytest("--fulltrace") result = pytester.runpytest("--fulltrace")
@ -1060,13 +1066,18 @@ def test_fixture_scope_sibling_conftests(pytester: Pytester) -> None:
def fix(): def fix():
return 1 return 1
""" """
) ),
encoding="utf-8",
)
foo_path.joinpath("test_foo.py").write_text(
"def test_foo(fix): assert fix == 1", encoding="utf-8"
) )
foo_path.joinpath("test_foo.py").write_text("def test_foo(fix): assert fix == 1")
# Tests in `food/` should not see the conftest fixture from `foo/` # Tests in `food/` should not see the conftest fixture from `foo/`
food_path = pytester.mkpydir("food") food_path = pytester.mkpydir("food")
food_path.joinpath("test_food.py").write_text("def test_food(fix): assert fix == 1") food_path.joinpath("test_food.py").write_text(
"def test_food(fix): assert fix == 1", encoding="utf-8"
)
res = pytester.runpytest() res = pytester.runpytest()
assert res.ret == 1 assert res.ret == 1
@ -1197,7 +1208,8 @@ def test_collect_with_chdir_during_import(pytester: Pytester) -> None:
os.chdir(%r) os.chdir(%r)
""" """
% (str(subdir),) % (str(subdir),)
) ),
encoding="utf-8",
) )
pytester.makepyfile( pytester.makepyfile(
""" """
@ -1227,8 +1239,12 @@ def test_collect_pyargs_with_testpaths(
) -> None: ) -> None:
testmod = pytester.mkdir("testmod") testmod = pytester.mkdir("testmod")
# NOTE: __init__.py is not collected since it does not match python_files. # NOTE: __init__.py is not collected since it does not match python_files.
testmod.joinpath("__init__.py").write_text("def test_func(): pass") testmod.joinpath("__init__.py").write_text(
testmod.joinpath("test_file.py").write_text("def test_func(): pass") "def test_func(): pass", encoding="utf-8"
)
testmod.joinpath("test_file.py").write_text(
"def test_func(): pass", encoding="utf-8"
)
root = pytester.mkdir("root") root = pytester.mkdir("root")
root.joinpath("pytest.ini").write_text( root.joinpath("pytest.ini").write_text(
@ -1238,7 +1254,8 @@ def test_collect_pyargs_with_testpaths(
addopts = --pyargs addopts = --pyargs
testpaths = testmod testpaths = testmod
""" """
) ),
encoding="utf-8",
) )
monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep) monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@ -1256,7 +1273,8 @@ def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:
def pytest_sessionstart(session): def pytest_sessionstart(session):
raise Exception("pytest_sessionstart hook successfully run") raise Exception("pytest_sessionstart hook successfully run")
""" """
) ),
encoding="utf-8",
) )
pytester.makeini( pytester.makeini(
""" """
@ -1264,11 +1282,18 @@ def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:
testpaths = some_path testpaths = some_path
""" """
) )
# No command line args - falls back to testpaths.
result = pytester.runpytest() result = pytester.runpytest()
assert result.ret == ExitCode.INTERNAL_ERROR
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
"INTERNALERROR* Exception: pytest_sessionstart hook successfully run" "INTERNALERROR* Exception: pytest_sessionstart hook successfully run"
) )
# No fallback.
result = pytester.runpytest(".")
assert result.ret == ExitCode.NO_TESTS_COLLECTED
def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None: def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None:
"""Long option values do not break initial conftests handling (#10169).""" """Long option values do not break initial conftests handling (#10169)."""
@ -1316,6 +1341,7 @@ def test_collect_symlink_out_of_tree(pytester: Pytester) -> None:
assert request.node.nodeid == "test_real.py::test_nodeid" assert request.node.nodeid == "test_real.py::test_nodeid"
""" """
), ),
encoding="utf-8",
) )
out_of_tree = pytester.mkdir("out_of_tree") out_of_tree = pytester.mkdir("out_of_tree")
@ -1344,12 +1370,16 @@ def test_collect_symlink_dir(pytester: Pytester) -> None:
def test_collectignore_via_conftest(pytester: Pytester) -> None: def test_collectignore_via_conftest(pytester: Pytester) -> None:
"""collect_ignore in parent conftest skips importing child (issue #4592).""" """collect_ignore in parent conftest skips importing child (issue #4592)."""
tests = pytester.mkpydir("tests") tests = pytester.mkpydir("tests")
tests.joinpath("conftest.py").write_text("collect_ignore = ['ignore_me']") tests.joinpath("conftest.py").write_text(
"collect_ignore = ['ignore_me']", encoding="utf-8"
)
ignore_me = tests.joinpath("ignore_me") ignore_me = tests.joinpath("ignore_me")
ignore_me.mkdir() ignore_me.mkdir()
ignore_me.joinpath("__init__.py").touch() ignore_me.joinpath("__init__.py").touch()
ignore_me.joinpath("conftest.py").write_text("assert 0, 'should_not_be_called'") ignore_me.joinpath("conftest.py").write_text(
"assert 0, 'should_not_be_called'", encoding="utf-8"
)
result = pytester.runpytest() result = pytester.runpytest()
assert result.ret == ExitCode.NO_TESTS_COLLECTED assert result.ret == ExitCode.NO_TESTS_COLLECTED
@ -1358,23 +1388,31 @@ def test_collectignore_via_conftest(pytester: Pytester) -> None:
def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None: def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None:
subdir = pytester.mkdir("sub") subdir = pytester.mkdir("sub")
init = subdir.joinpath("__init__.py") init = subdir.joinpath("__init__.py")
init.write_text("def test_init(): pass") init.write_text("def test_init(): pass", encoding="utf-8")
p = subdir.joinpath("test_file.py") p = subdir.joinpath("test_file.py")
p.write_text("def test_file(): pass") p.write_text("def test_file(): pass", encoding="utf-8")
# NOTE: without "-o python_files=*.py" this collects test_file.py twice. # Just the package directory, the __init__.py module is filtered out.
# This changed/broke with "Add package scoped fixtures #2283" (2b1410895) result = pytester.runpytest("-v", subdir)
# initially (causing a RecursionError).
result = pytester.runpytest("-v", str(init), str(p))
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"sub/test_file.py::test_file PASSED*", "sub/test_file.py::test_file PASSED*",
"*1 passed in*",
]
)
# But it's included if specified directly.
result = pytester.runpytest("-v", init, p)
result.stdout.fnmatch_lines(
[
"sub/__init__.py::test_init PASSED*",
"sub/test_file.py::test_file PASSED*", "sub/test_file.py::test_file PASSED*",
"*2 passed in*", "*2 passed in*",
] ]
) )
result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init), str(p)) # Or if the pattern allows it.
result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir)
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"sub/__init__.py::test_init PASSED*", "sub/__init__.py::test_init PASSED*",
@ -1387,12 +1425,15 @@ def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None:
def test_collect_pkg_init_only(pytester: Pytester) -> None: def test_collect_pkg_init_only(pytester: Pytester) -> None:
subdir = pytester.mkdir("sub") subdir = pytester.mkdir("sub")
init = subdir.joinpath("__init__.py") init = subdir.joinpath("__init__.py")
init.write_text("def test_init(): pass") init.write_text("def test_init(): pass", encoding="utf-8")
result = pytester.runpytest(str(init)) result = pytester.runpytest(subdir)
result.stdout.fnmatch_lines(["*no tests ran in*"]) result.stdout.fnmatch_lines(["*no tests ran in*"])
result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init)) result = pytester.runpytest("-v", init)
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir)
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
@ -1402,7 +1443,7 @@ def test_collect_sub_with_symlinks(use_pkg: bool, pytester: Pytester) -> None:
sub = pytester.mkdir("sub") sub = pytester.mkdir("sub")
if use_pkg: if use_pkg:
sub.joinpath("__init__.py").touch() sub.joinpath("__init__.py").touch()
sub.joinpath("test_file.py").write_text("def test_file(): pass") sub.joinpath("test_file.py").write_text("def test_file(): pass", encoding="utf-8")
# Create a broken symlink. # Create a broken symlink.
symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py")) symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py"))
@ -1440,7 +1481,7 @@ def test_collector_respects_tbstyle(pytester: Pytester) -> None:
def test_does_not_eagerly_collect_packages(pytester: Pytester) -> None: def test_does_not_eagerly_collect_packages(pytester: Pytester) -> None:
pytester.makepyfile("def test(): pass") pytester.makepyfile("def test(): pass")
pydir = pytester.mkpydir("foopkg") pydir = pytester.mkpydir("foopkg")
pydir.joinpath("__init__.py").write_text("assert False") pydir.joinpath("__init__.py").write_text("assert False", encoding="utf-8")
result = pytester.runpytest() result = pytester.runpytest()
assert result.ret == ExitCode.OK assert result.ret == ExitCode.OK

View File

@ -1,5 +1,6 @@
import enum import enum
import sys import sys
from functools import cached_property
from functools import partial from functools import partial
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -8,7 +9,6 @@ from typing import Union
import pytest import pytest
from _pytest.compat import _PytestWrapper from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never from _pytest.compat import assert_never
from _pytest.compat import cached_property
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr

View File

@ -1,4 +1,5 @@
import dataclasses import dataclasses
import importlib.metadata
import os import os
import re import re
import sys import sys
@ -13,7 +14,6 @@ from typing import Union
import _pytest._code import _pytest._code
import pytest import pytest
from _pytest.compat import importlib_metadata
from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _get_plugin_specs_as_list
from _pytest.config import _iter_rewritable_modules from _pytest.config import _iter_rewritable_modules
from _pytest.config import _strtobool from _pytest.config import _strtobool
@ -87,7 +87,8 @@ class TestParseIni:
[pytest] [pytest]
addopts = --verbose addopts = --verbose
""" """
) ),
encoding="utf-8",
) )
config = pytester.parseconfig(tmp_path) config = pytester.parseconfig(tmp_path)
assert config.option.color == "no" assert config.option.color == "no"
@ -127,7 +128,8 @@ class TestParseIni:
""".format( """.format(
section=section section=section
) )
) ),
encoding="utf-8",
) )
config = pytester.parseconfig() config = pytester.parseconfig()
assert config.getini("minversion") == "3.36" assert config.getini("minversion") == "3.36"
@ -150,7 +152,8 @@ class TestParseIni:
[pytest] [pytest]
minversion = 2.0 minversion = 2.0
""" """
) ),
encoding="utf-8",
) )
pytester.path.joinpath("pytest.ini").write_text( pytester.path.joinpath("pytest.ini").write_text(
textwrap.dedent( textwrap.dedent(
@ -158,13 +161,16 @@ class TestParseIni:
[pytest] [pytest]
minversion = 1.5 minversion = 1.5
""" """
) ),
encoding="utf-8",
) )
config = pytester.parseconfigure(sub) config = pytester.parseconfigure(sub)
assert config.getini("minversion") == "2.0" assert config.getini("minversion") == "2.0"
def test_ini_parse_error(self, pytester: Pytester) -> None: def test_ini_parse_error(self, pytester: Pytester) -> None:
pytester.path.joinpath("pytest.ini").write_text("addopts = -x") pytester.path.joinpath("pytest.ini").write_text(
"addopts = -x", encoding="utf-8"
)
result = pytester.runpytest() result = pytester.runpytest()
assert result.ret != 0 assert result.ret != 0
result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined") result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined")
@ -179,6 +185,23 @@ class TestParseIni:
assert result.ret != 0 assert result.ret != 0
result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*") result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*")
def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None:
# If --confcutdir is not specified, and there is no configfile, default
# to the roothpath.
sub = pytester.mkdir("sub")
os.chdir(sub)
config = pytester.parseconfigure()
assert config.pluginmanager._confcutdir == sub
def test_confcutdir_default_with_configfile(self, pytester: Pytester) -> None:
# If --confcutdir is not specified, and there is a configfile, default
# to the configfile's directory.
pytester.makeini("[pytest]")
sub = pytester.mkdir("sub")
os.chdir(sub)
config = pytester.parseconfigure()
assert config.pluginmanager._confcutdir == pytester.path
@pytest.mark.xfail(reason="probably not needed") @pytest.mark.xfail(reason="probably not needed")
def test_confcutdir(self, pytester: Pytester) -> None: def test_confcutdir(self, pytester: Pytester) -> None:
sub = pytester.mkdir("sub") sub = pytester.mkdir("sub")
@ -452,7 +475,7 @@ class TestParseIni:
pytester.makepyfile(myplugin1_module="# my plugin module") pytester.makepyfile(myplugin1_module="# my plugin module")
pytester.syspathinsert() pytester.syspathinsert()
monkeypatch.setattr(importlib_metadata, "distributions", my_dists) monkeypatch.setattr(importlib.metadata, "distributions", my_dists)
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
pytester.makeini(ini_file_text) pytester.makeini(ini_file_text)
@ -514,6 +537,8 @@ class TestConfigCmdlineParsing:
) )
config = pytester.parseconfig("-c", "custom.ini") config = pytester.parseconfig("-c", "custom.ini")
assert config.getini("custom") == "1" assert config.getini("custom") == "1"
config = pytester.parseconfig("--config-file", "custom.ini")
assert config.getini("custom") == "1"
pytester.makefile( pytester.makefile(
".cfg", ".cfg",
@ -524,6 +549,8 @@ class TestConfigCmdlineParsing:
) )
config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg") config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg")
assert config.getini("custom") == "1" assert config.getini("custom") == "1"
config = pytester.parseconfig("--config-file", "custom_tool_pytest_section.cfg")
assert config.getini("custom") == "1"
pytester.makefile( pytester.makefile(
".toml", ".toml",
@ -536,6 +563,8 @@ class TestConfigCmdlineParsing:
) )
config = pytester.parseconfig("-c", "custom.toml") config = pytester.parseconfig("-c", "custom.toml")
assert config.getini("custom") == "1" assert config.getini("custom") == "1"
config = pytester.parseconfig("--config-file", "custom.toml")
assert config.getini("custom") == "1"
def test_absolute_win32_path(self, pytester: Pytester) -> None: def test_absolute_win32_path(self, pytester: Pytester) -> None:
temp_ini_file = pytester.makefile( temp_ini_file = pytester.makefile(
@ -550,6 +579,8 @@ class TestConfigCmdlineParsing:
temp_ini_file_norm = normpath(str(temp_ini_file)) temp_ini_file_norm = normpath(str(temp_ini_file))
ret = pytest.main(["-c", temp_ini_file_norm]) ret = pytest.main(["-c", temp_ini_file_norm])
assert ret == ExitCode.OK assert ret == ExitCode.OK
ret = pytest.main(["--config-file", temp_ini_file_norm])
assert ret == ExitCode.OK
class TestConfigAPI: class TestConfigAPI:
@ -609,7 +640,7 @@ class TestConfigAPI:
def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None: def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None:
somepath = tmp_path.joinpath("x", "y", "z") somepath = tmp_path.joinpath("x", "y", "z")
p = tmp_path.joinpath("conftest.py") p = tmp_path.joinpath("conftest.py")
p.write_text(f"mylist = {['.', str(somepath)]}") p.write_text(f"mylist = {['.', str(somepath)]}", encoding="utf-8")
config = pytester.parseconfigure(p) config = pytester.parseconfigure(p)
assert ( assert (
config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path) config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path)
@ -885,7 +916,8 @@ class TestConfigFromdictargs:
[pytest] [pytest]
name = value name = value
""" """
) ),
encoding="utf-8",
) )
inifilename = "../../foo/bar.ini" inifilename = "../../foo/bar.ini"
@ -902,7 +934,8 @@ class TestConfigFromdictargs:
name = wrong-value name = wrong-value
should_not_be_set = true should_not_be_set = true
""" """
) ),
encoding="utf-8",
) )
with MonkeyPatch.context() as mp: with MonkeyPatch.context() as mp:
mp.chdir(cwd) mp.chdir(cwd)
@ -970,7 +1003,7 @@ def test_preparse_ordering_with_setuptools(
def my_dists(): def my_dists():
return (Dist,) return (Dist,)
monkeypatch.setattr(importlib_metadata, "distributions", my_dists) monkeypatch.setattr(importlib.metadata, "distributions", my_dists)
pytester.makeconftest( pytester.makeconftest(
""" """
pytest_plugins = "mytestplugin", pytest_plugins = "mytestplugin",
@ -1003,7 +1036,7 @@ def test_setuptools_importerror_issue1479(
def distributions(): def distributions():
return (Distribution(),) return (Distribution(),)
monkeypatch.setattr(importlib_metadata, "distributions", distributions) monkeypatch.setattr(importlib.metadata, "distributions", distributions)
with pytest.raises(ImportError): with pytest.raises(ImportError):
pytester.parseconfig() pytester.parseconfig()
@ -1030,7 +1063,7 @@ def test_importlib_metadata_broken_distribution(
def distributions(): def distributions():
return (Distribution(),) return (Distribution(),)
monkeypatch.setattr(importlib_metadata, "distributions", distributions) monkeypatch.setattr(importlib.metadata, "distributions", distributions)
pytester.parseconfig() pytester.parseconfig()
@ -1058,7 +1091,7 @@ def test_plugin_preparse_prevents_setuptools_loading(
def distributions(): def distributions():
return (Distribution(),) return (Distribution(),)
monkeypatch.setattr(importlib_metadata, "distributions", distributions) monkeypatch.setattr(importlib.metadata, "distributions", distributions)
args = ("-p", "no:mytestplugin") if block_it else () args = ("-p", "no:mytestplugin") if block_it else ()
config = pytester.parseconfig(*args) config = pytester.parseconfig(*args)
config.pluginmanager.import_plugin("mytestplugin") config.pluginmanager.import_plugin("mytestplugin")
@ -1107,7 +1140,7 @@ def test_disable_plugin_autoload(
return (Distribution(),) return (Distribution(),)
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
monkeypatch.setattr(importlib_metadata, "distributions", distributions) monkeypatch.setattr(importlib.metadata, "distributions", distributions)
monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc] monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc]
config = pytester.parseconfig(*parse_args) config = pytester.parseconfig(*parse_args)
has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None
@ -1151,7 +1184,7 @@ def test_cmdline_processargs_simple(pytester: Pytester) -> None:
args.append("-h") args.append("-h")
""" """
) )
result = pytester.runpytest() result = pytester.runpytest("-Wignore::pytest.PytestRemovedIn8Warning")
result.stdout.fnmatch_lines(["*pytest*", "*-h*"]) result.stdout.fnmatch_lines(["*pytest*", "*-h*"])
@ -1362,7 +1395,7 @@ class TestRootdir:
) )
def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
inipath = tmp_path / name inipath = tmp_path / name
inipath.write_text(contents, "utf-8") inipath.write_text(contents, encoding="utf-8")
a = tmp_path / "a" a = tmp_path / "a"
a.mkdir() a.mkdir()
@ -1421,7 +1454,7 @@ class TestRootdir:
) -> None: ) -> None:
p = tmp_path / name p = tmp_path / name
p.touch() p.touch()
p.write_text(contents, "utf-8") p.write_text(contents, encoding="utf-8")
rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)]) rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)])
assert rootpath == tmp_path assert rootpath == tmp_path
assert inipath == p assert inipath == p
@ -1517,7 +1550,8 @@ class TestOverrideIniArgs:
custom = 1.0""".format( custom = 1.0""".format(
section=section section=section
) )
) ),
encoding="utf-8",
) )
pytester.makeconftest( pytester.makeconftest(
""" """
@ -1907,6 +1941,9 @@ class TestSetupCfg:
with pytest.raises(pytest.fail.Exception): with pytest.raises(pytest.fail.Exception):
pytester.runpytest("-c", "custom.cfg") pytester.runpytest("-c", "custom.cfg")
with pytest.raises(pytest.fail.Exception):
pytester.runpytest("--config-file", "custom.cfg")
class TestPytestPluginsVariable: class TestPytestPluginsVariable:
def test_pytest_plugins_in_non_top_level_conftest_unsupported( def test_pytest_plugins_in_non_top_level_conftest_unsupported(

View File

@ -1,4 +1,3 @@
import argparse
import os import os
import textwrap import textwrap
from pathlib import Path from pathlib import Path
@ -7,6 +6,8 @@ from typing import Dict
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Sequence
from typing import Union
import pytest import pytest
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -24,18 +25,18 @@ def ConftestWithSetinitial(path) -> PytestPluginManager:
def conftest_setinitial( def conftest_setinitial(
conftest: PytestPluginManager, args, confcutdir: Optional["os.PathLike[str]"] = None conftest: PytestPluginManager,
args: Sequence[Union[str, Path]],
confcutdir: Optional[Path] = None,
) -> None: ) -> None:
class Namespace: conftest._set_initial_conftests(
def __init__(self) -> None: args=args,
self.file_or_dir = args pyargs=False,
self.confcutdir = os.fspath(confcutdir) if confcutdir is not None else None noconftest=False,
self.noconftest = False rootpath=Path(args[0]),
self.pyargs = False confcutdir=confcutdir,
self.importmode = "prepend" importmode="prepend",
)
namespace = cast(argparse.Namespace, Namespace())
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[])
@pytest.mark.usefixtures("_sys_snapshot") @pytest.mark.usefixtures("_sys_snapshot")
@ -46,8 +47,12 @@ class TestConftestValueAccessGlobal:
) -> Generator[Path, None, None]: ) -> Generator[Path, None, None]:
tmp_path = tmp_path_factory.mktemp("basedir", numbered=True) tmp_path = tmp_path_factory.mktemp("basedir", numbered=True)
tmp_path.joinpath("adir/b").mkdir(parents=True) tmp_path.joinpath("adir/b").mkdir(parents=True)
tmp_path.joinpath("adir/conftest.py").write_text("a=1 ; Directory = 3") tmp_path.joinpath("adir/conftest.py").write_text(
tmp_path.joinpath("adir/b/conftest.py").write_text("b=2 ; a = 1.5") "a=1 ; Directory = 3", encoding="utf-8"
)
tmp_path.joinpath("adir/b/conftest.py").write_text(
"b=2 ; a = 1.5", encoding="utf-8"
)
if request.param == "inpackage": if request.param == "inpackage":
tmp_path.joinpath("adir/__init__.py").touch() tmp_path.joinpath("adir/__init__.py").touch()
tmp_path.joinpath("adir/b/__init__.py").touch() tmp_path.joinpath("adir/b/__init__.py").touch()
@ -122,8 +127,12 @@ class TestConftestValueAccessGlobal:
def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None: def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None:
tmp_path.joinpath("adir-1.0/b").mkdir(parents=True) tmp_path.joinpath("adir-1.0/b").mkdir(parents=True)
tmp_path.joinpath("adir-1.0/conftest.py").write_text("a=1 ; Directory = 3") tmp_path.joinpath("adir-1.0/conftest.py").write_text(
tmp_path.joinpath("adir-1.0/b/conftest.py").write_text("b=2 ; a = 1.5") "a=1 ; Directory = 3", encoding="utf-8"
)
tmp_path.joinpath("adir-1.0/b/conftest.py").write_text(
"b=2 ; a = 1.5", encoding="utf-8"
)
tmp_path.joinpath("adir-1.0/b/__init__.py").touch() tmp_path.joinpath("adir-1.0/b/__init__.py").touch()
tmp_path.joinpath("adir-1.0/__init__.py").touch() tmp_path.joinpath("adir-1.0/__init__.py").touch()
ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b")) ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b"))
@ -166,7 +175,7 @@ def test_conftest_global_import(pytester: Pytester) -> None:
sub = Path("sub") sub = Path("sub")
sub.mkdir() sub.mkdir()
subconf = sub / "conftest.py" subconf = sub / "conftest.py"
subconf.write_text("y=4") subconf.write_text("y=4", encoding="utf-8")
mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd()) mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd())
assert mod != mod2 assert mod != mod2
assert mod2.y == 4 assert mod2.y == 4
@ -245,7 +254,8 @@ def test_conftest_confcutdir(pytester: Pytester) -> None:
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--xyz", action="store_true") parser.addoption("--xyz", action="store_true")
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest("-h", "--confcutdir=%s" % x, x) result = pytester.runpytest("-h", "--confcutdir=%s" % x, x)
result.stdout.fnmatch_lines(["*--xyz*"]) result.stdout.fnmatch_lines(["*--xyz*"])
@ -273,9 +283,12 @@ def test_installed_conftest_is_picked_up(pytester: Pytester, tmp_path: Path) ->
@pytest.fixture @pytest.fixture
def fix(): return None def fix(): return None
""" """
) ),
encoding="utf-8",
)
tmp_path.joinpath("foo", "test_it.py").write_text(
"def test_it(fix): pass", encoding="utf-8"
) )
tmp_path.joinpath("foo", "test_it.py").write_text("def test_it(fix): pass")
result = pytester.runpytest("--pyargs", "foo") result = pytester.runpytest("--pyargs", "foo")
assert result.ret == 0 assert result.ret == 0
@ -400,7 +413,8 @@ def test_conftest_existing_junitxml(pytester: Pytester) -> None:
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--xyz", action="store_true") parser.addoption("--xyz", action="store_true")
""" """
) ),
encoding="utf-8",
) )
pytester.makefile(ext=".xml", junit="") # Writes junit.xml pytester.makefile(ext=".xml", junit="") # Writes junit.xml
result = pytester.runpytest("-h", "--junitxml", "junit.xml") result = pytester.runpytest("-h", "--junitxml", "junit.xml")
@ -411,7 +425,7 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) ->
ct1 = pytester.makeconftest("") ct1 = pytester.makeconftest("")
sub = pytester.mkdir("sub") sub = pytester.mkdir("sub")
ct2 = sub / "conftest.py" ct2 = sub / "conftest.py"
ct2.write_text("") ct2.write_text("", encoding="utf-8")
def impct(p, importmode, root): def impct(p, importmode, root):
return p return p
@ -449,7 +463,8 @@ def test_fixture_dependency(pytester: Pytester) -> None:
def bar(foo): def bar(foo):
return 'bar' return 'bar'
""" """
) ),
encoding="utf-8",
) )
subsub = sub.joinpath("subsub") subsub = sub.joinpath("subsub")
subsub.mkdir() subsub.mkdir()
@ -466,7 +481,8 @@ def test_fixture_dependency(pytester: Pytester) -> None:
def test_event_fixture(bar): def test_event_fixture(bar):
assert bar == 'sub bar' assert bar == 'sub bar'
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest("sub") result = pytester.runpytest("sub")
result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.fnmatch_lines(["*1 passed*"])
@ -480,10 +496,11 @@ def test_conftest_found_with_double_dash(pytester: Pytester) -> None:
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--hello-world", action="store_true") parser.addoption("--hello-world", action="store_true")
""" """
) ),
encoding="utf-8",
) )
p = sub.joinpath("test_hello.py") p = sub.joinpath("test_hello.py")
p.write_text("def test_hello(): pass") p.write_text("def test_hello(): pass", encoding="utf-8")
result = pytester.runpytest(str(p) + "::test_hello", "-h") result = pytester.runpytest(str(p) + "::test_hello", "-h")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" """
@ -507,7 +524,8 @@ class TestConftestVisibility:
def fxtr(): def fxtr():
return "from-package" return "from-package"
""" """
) ),
encoding="utf-8",
) )
package.joinpath("test_pkgroot.py").write_text( package.joinpath("test_pkgroot.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -515,7 +533,8 @@ class TestConftestVisibility:
def test_pkgroot(fxtr): def test_pkgroot(fxtr):
assert fxtr == "from-package" assert fxtr == "from-package"
""" """
) ),
encoding="utf-8",
) )
swc = package.joinpath("swc") swc = package.joinpath("swc")
@ -529,7 +548,8 @@ class TestConftestVisibility:
def fxtr(): def fxtr():
return "from-swc" return "from-swc"
""" """
) ),
encoding="utf-8",
) )
swc.joinpath("test_with_conftest.py").write_text( swc.joinpath("test_with_conftest.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -537,7 +557,8 @@ class TestConftestVisibility:
def test_with_conftest(fxtr): def test_with_conftest(fxtr):
assert fxtr == "from-swc" assert fxtr == "from-swc"
""" """
) ),
encoding="utf-8",
) )
snc = package.joinpath("snc") snc = package.joinpath("snc")
@ -550,7 +571,8 @@ class TestConftestVisibility:
assert fxtr == "from-package" # No local conftest.py, so should assert fxtr == "from-package" # No local conftest.py, so should
# use value from parent dir's # use value from parent dir's
""" """
) ),
encoding="utf-8",
) )
print("created directory structure:") print("created directory structure:")
for x in pytester.path.glob("**/"): for x in pytester.path.glob("**/"):
@ -594,7 +616,13 @@ class TestConftestVisibility:
print("pytestarg : %s" % testarg) print("pytestarg : %s" % testarg)
print("expected pass : %s" % expect_ntests_passed) print("expected pass : %s" % expect_ntests_passed)
os.chdir(dirs[chdir]) os.chdir(dirs[chdir])
reprec = pytester.inline_run(testarg, "-q", "--traceconfig") reprec = pytester.inline_run(
testarg,
"-q",
"--traceconfig",
"--confcutdir",
pytester.path,
)
reprec.assertoutcome(passed=expect_ntests_passed) reprec.assertoutcome(passed=expect_ntests_passed)
@ -610,7 +638,7 @@ def test_search_conftest_up_to_inifile(
root = pytester.path root = pytester.path
src = root.joinpath("src") src = root.joinpath("src")
src.mkdir() src.mkdir()
src.joinpath("pytest.ini").write_text("[pytest]") src.joinpath("pytest.ini").write_text("[pytest]", encoding="utf-8")
src.joinpath("conftest.py").write_text( src.joinpath("conftest.py").write_text(
textwrap.dedent( textwrap.dedent(
"""\ """\
@ -618,7 +646,8 @@ def test_search_conftest_up_to_inifile(
@pytest.fixture @pytest.fixture
def fix1(): pass def fix1(): pass
""" """
) ),
encoding="utf-8",
) )
src.joinpath("test_foo.py").write_text( src.joinpath("test_foo.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -628,7 +657,8 @@ def test_search_conftest_up_to_inifile(
def test_2(out_of_reach): def test_2(out_of_reach):
pass pass
""" """
) ),
encoding="utf-8",
) )
root.joinpath("conftest.py").write_text( root.joinpath("conftest.py").write_text(
textwrap.dedent( textwrap.dedent(
@ -637,7 +667,8 @@ def test_search_conftest_up_to_inifile(
@pytest.fixture @pytest.fixture
def out_of_reach(): pass def out_of_reach(): pass
""" """
) ),
encoding="utf-8",
) )
args = [str(src)] args = [str(src)]
@ -720,7 +751,8 @@ def test_required_option_help(pytester: Pytester) -> None:
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--xyz", action="store_true", required=True) parser.addoption("--xyz", action="store_true", required=True)
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest("-h", x) result = pytester.runpytest("-h", x)
result.stdout.no_fnmatch_line("*argument --xyz is required*") result.stdout.no_fnmatch_line("*argument --xyz is required*")

View File

@ -114,7 +114,7 @@ class TestDoctests:
reprec.assertoutcome(failed=1) reprec.assertoutcome(failed=1)
def test_importmode(self, pytester: Pytester): def test_importmode(self, pytester: Pytester):
p = pytester.makepyfile( pytester.makepyfile(
**{ **{
"namespacepkg/innerpkg/__init__.py": "", "namespacepkg/innerpkg/__init__.py": "",
"namespacepkg/innerpkg/a.py": """ "namespacepkg/innerpkg/a.py": """
@ -132,7 +132,7 @@ class TestDoctests:
""", """,
} }
) )
reprec = pytester.inline_run(p, "--doctest-modules", "--import-mode=importlib") reprec = pytester.inline_run("--doctest-modules", "--import-mode=importlib")
reprec.assertoutcome(passed=1) reprec.assertoutcome(passed=1)
def test_new_pattern(self, pytester: Pytester): def test_new_pattern(self, pytester: Pytester):
@ -357,7 +357,8 @@ class TestDoctests:
>>> 1/0 >>> 1/0
''' '''
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest("--doctest-modules") result = pytester.runpytest("--doctest-modules")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
@ -448,7 +449,8 @@ class TestDoctests:
"""\ """\
import asdalsdkjaslkdjasd import asdalsdkjaslkdjasd
""" """
) ),
encoding="utf-8",
) )
pytester.maketxtfile( pytester.maketxtfile(
""" """
@ -492,7 +494,8 @@ class TestDoctests:
2 2
''' '''
""" """
) ),
encoding="utf-8",
) )
result = pytester.runpytest(p, "--doctest-modules") result = pytester.runpytest(p, "--doctest-modules")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
@ -1566,7 +1569,9 @@ def test_warning_on_unwrap_of_broken_object(
def test_is_setup_py_not_named_setup_py(tmp_path: Path) -> None: def test_is_setup_py_not_named_setup_py(tmp_path: Path) -> None:
not_setup_py = tmp_path.joinpath("not_setup.py") not_setup_py = tmp_path.joinpath("not_setup.py")
not_setup_py.write_text('from setuptools import setup; setup(name="foo")') not_setup_py.write_text(
'from setuptools import setup; setup(name="foo")', encoding="utf-8"
)
assert not _is_setup_py(not_setup_py) assert not _is_setup_py(not_setup_py)

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