Compare commits

..

79 Commits

Author SHA1 Message Date
pre-commit-ci[bot]
3f12087fe0 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-02-11 15:56:37 +00:00
pytest bot
bc3021cdfd Prepare release version 7.0.1 2022-02-11 15:54:45 +00:00
Bruno Oliveira
591d476f14 Merge pull request #9673 from nicoddemus/backport-9511
[7.0.x] Enable testing with Python 3.11 (#9511)
2022-02-11 12:45:35 -03:00
Bruno Oliveira
6ca733e8f1 Enable testing with Python 3.11 (#9511) 2022-02-11 12:26:29 -03:00
Bruno Oliveira
ac37b1b113 Merge pull request #9671 from nicoddemus/backport-9668
[7.0.x] Merge pull request #9668 from hugovk/test-me-latest-3.10
2022-02-11 12:21:36 -03:00
Bruno Oliveira
c891e402ac Merge pull request #9672 from nicoddemus/backport-9669
[7.0.x] Merge pull request #9669 from hugovk/ci-only-update-plugin-list-for-upstream
2022-02-11 10:00:08 -03:00
Bruno Oliveira
e2753a2b8b Merge pull request #9669 from hugovk/ci-only-update-plugin-list-for-upstream 2022-02-11 09:34:30 -03:00
Bruno Oliveira
b5a154c1d9 Merge pull request #9668 from hugovk/test-me-latest-3.10 2022-02-11 09:28:15 -03:00
Bruno Oliveira
0fae45bb6e Merge pull request #9660 from pytest-dev/backport-9646-to-7.0.x 2022-02-10 14:30:27 -03:00
Bruno Oliveira
37d434f5fc [7.0.x] Delay warning about collector/item diamond inheritance 2022-02-10 17:12:02 +00:00
github-actions[bot]
6684110408 [7.0.x] unittest: restore UnitTestFunction.obj to return unbound rather than bound method (#9656)
Co-authored-by: Ran Benita <ran@unusedvar.com>
2022-02-09 23:20:31 +01:00
Ran Benita
ed8255c811 Merge pull request #9653 from pytest-dev/backport-9651-to-7.0.x
[7.0.x] Rename ``pythonpath`` plugin to ``python_path``
2022-02-09 13:54:07 +02:00
Ran Benita
453bcb1b83 [7.0.x] Rename `pythonpath plugin to python_path` 2022-02-09 11:20:53 +00:00
Bruno Oliveira
a24c78b7af Merge pull request #9649 from pytest-dev/backport-9642-to-7.0.x 2022-02-08 13:26:10 -03:00
Anthony Sottile
4bf8aff5b7 [7.0.x] allow running testids which contain :: in the parametrized portion 2022-02-08 16:09:06 +00:00
Ran Benita
462a6f0de6 Merge pull request #9647 from pytest-dev/backport-9609-to-7.0.x
[7.0.x] importlib.readers not valid until python 3.10
2022-02-08 17:54:38 +02:00
Ran Benita
c588a4720e [7.0.x] importlib.readers not valid until python 3.10 2022-02-08 17:36:38 +02:00
Ran Benita
048a10bd96 Merge pull request #9648 from pytest-dev/backport-9638-to-7.0.x
[7.0.x] work around test pollution caused by new setuptools mutating global logger level
2022-02-08 16:52:39 +02:00
Anthony Sottile
3c35477230 [7.0.x] work around test pollution caused by new setuptools mutating global logger level 2022-02-08 14:32:58 +00:00
github-actions[bot]
5eb4d6977c [7.0.x] doc: Hide done training (#9605)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2022-02-04 11:42:53 +00:00
github-actions[bot]
e37fbe5685 Prepare release 7.0.0 (#9598)
* Prepare release version 7.0.0

* Add note to changelog

Co-authored-by: pytest bot <pytestbot@gmail.com>
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2022-02-04 11:23:39 +01:00
github-actions[bot]
737b220516 [7.0.x] releasing: Add template for major releases (#9597)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2022-02-03 17:04:19 +01:00
github-actions[bot]
7fa3972963 [7.0.x] releasing: Always set doc_version (#9590)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2022-02-03 11:07:47 +00:00
github-actions[bot]
b304499925 [7.0.x] Make 'warnings' and 'deselected' in assert_outcomes optional (#9566)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-01-27 14:18:54 +00:00
github-actions[bot]
f17525df26 [7.0.x] doc: Add ellipsis to warning usecase list (#9562)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2022-01-27 10:36:58 -03:00
Florian Bruhin
0a7be971d2 ci: Bump up timeout (#9565)
macOS apparently can be slow, https://github.com/pytest-dev/pytest/runs/4965510831 for #9556 got cancelled at 91%

(cherry picked from commit 7bffcd0ac4)
2022-01-27 10:36:25 -03:00
github-actions[bot]
c17908cdb3 [7.0.x] doc: Recategorize 7.0.0 changelog items (#9564)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2022-01-27 10:36:15 -03:00
github-actions[bot]
ab549bba8a [7.0.x] Add missing cooperative constructor changelog (#9563)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2022-01-27 10:36:04 -03:00
github-actions[bot]
4b1707ff70 [7.0.x] Autouse linearization graph (#9557)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-01-27 11:57:34 +00:00
github-actions[bot]
28e5b3b8b7 [7.0.x] Add additional docs for uncooperative ctor deprecation (#9552)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2022-01-27 11:54:34 +00:00
Bruno Oliveira
e854d05328 Merge pull request #9550 from pytest-dev/backport-9545-to-7.0.x 2022-01-27 07:54:44 -03:00
Ran Benita
48e64feac7 [7.0.x] doc/reference: don't document pytest.__version__ under "Marks" 2022-01-27 08:50:18 +00:00
Bruno Oliveira
b66899d322 Merge pull request #9530 from nicoddemus/backport-9522 2022-01-25 16:53:49 -03:00
Ran Benita
b1b1bd03a8 Merge pull request #9544 from bluetech/backport-9532
[7.0.x] config: avoid stat storm in _getconftestmodules
2022-01-25 21:53:12 +02:00
Ran Benita
6639d6c7d8 Merge pull request #9532 from bluetech/getdir-cache
config: avoid stat storm in _getconftestmodules

(cherry picked from commit 5c69eced6c)

Conflicts:
	src/_pytest/config/__init__.py
2022-01-25 18:46:16 +02:00
Bruno Oliveira
6a1b8e4b28 Merge pull request #9522 from holmanb/rewrite-test 2022-01-20 13:21:48 -03:00
Ran Benita
5c49dea989 Merge pull request #9513 from pytest-dev/backport-9512-to-7.0.x
[7.0.x] testing: avoid private pluggy attributes in test
2022-01-14 18:30:33 +02:00
Ran Benita
a1635ca49a [7.0.x] testing: avoid private pluggy attributes in test 2022-01-14 16:10:54 +00:00
Bruno Oliveira
46c06e7560 Merge pull request #9508 from pytest-dev/backport-9495-to-7.0.x 2022-01-13 17:11:22 -03:00
Olga Matoula
94bcd2ce0f [7.0.x] Add docs on pytest.warns(None) deprecation 2022-01-13 19:51:06 +00:00
Bruno Oliveira
4967a0d084 Merge pull request #9507 from nicoddemus/backport-9506
[7.0.x] Improve on configuration file docs section
2022-01-13 15:14:32 -03:00
Bruno Oliveira
efa02cffdd Merge pull request #9506 from eamanu/fix-9505 2022-01-13 13:55:08 -03:00
Ran Benita
b5a6e30dae Merge pull request #9499 from pytest-dev/backport-9494-to-7.0.x
[7.0.x] python: add back `instance` accessor to all python nodes, not just Function
2022-01-10 21:28:12 +02:00
Ran Benita
d0ae12ca76 [7.0.x] python: add back instance accessor to all python nodes, not just Function 2022-01-10 19:09:14 +00:00
Ran Benita
0c5fc61610 Merge pull request #9485 from pytest-dev/backport-9484-to-7.0.x
[7.0.x] config: fix incorrect cache hit check in _getconftestmodules
2022-01-07 13:44:58 +02:00
Ran Benita
41cb93b549 [7.0.x] config: fix incorrect cache hit check in _getconftestmodules 2022-01-07 11:28:12 +00:00
Bruno Oliveira
7381ce2d83 Merge pull request #9474 from pytest-dev/backport-9400-to-7.0.x 2022-01-04 11:28:51 -03:00
Bruno Oliveira
5d151c0e7c Merge pull request #9476 from pytest-dev/backport-9464-to-7.0.x 2022-01-04 11:27:48 -03:00
Dan Alvizu
f988e070c6 [7.0.x] Doc update: clarify -W syntax 2022-01-04 14:04:18 +00:00
Bruno Oliveira
1a04121f2f [7.0.x] Ensure Config.inifile is available during pytest_cmdline_main 2022-01-04 13:24:11 +00:00
Ran Benita
8542eb47c4 Merge pull request #9453 from bluetech/backport-9451
[7.0.x] Backport "doc: document {Code,TracebackEntry}.path changes as breaking"
2021-12-28 10:55:24 +02:00
Ran Benita
4f79cea72d Merge pull request #9451 from bluetech/code-path-changelog
doc: document {Code,TracebackEntry}.path changes as breaking
(cherry picked from commit 1131f23e04)
2021-12-27 17:01:04 +02:00
Ran Benita
c76989352f Merge pull request #9450 from bluetech/backport-9438
[7.0.x] Backport "pytest: bring back direct imports of TempdirFactory, Testdir"
2021-12-27 16:54:01 +02:00
Ran Benita
74f4aad708 Merge pull request #9449 from bluetech/backport-9447
[7.0.x] Backport "code: accept any `os.PathLike[str]` in `Traceback.cut`"
2021-12-27 15:55:37 +02:00
Ran Benita
5171327e3b Merge pull request #9438 from bluetech/pytest-legacypath-imports
pytest: bring back direct imports of TempdirFactory, Testdir
(cherry picked from commit 69da199f6e)
2021-12-27 15:11:45 +02:00
Ran Benita
9274fa5610 Merge pull request #9447 from bluetech/code-cut-pathlike
code: accept any `os.PathLike[str]` in `Traceback.cut`
(cherry picked from commit fcef7e49fd)
2021-12-27 15:10:13 +02:00
Bruno Oliveira
161841d38e Fix typos (#9424) (#9448)
Co-authored-by: Kian Meng, Ang <kianmeng.ang@gmail.com>
2021-12-27 10:02:26 -03:00
Kian Meng, Ang
e62daed8c4 Fix typos (#9424) 2021-12-27 09:26:25 -03:00
Ran Benita
764f90351a Merge pull request #9446 from bluetech/backport-9443
[7.0.x] Backport "doc/reference: add 4 missing hooks to reference"
2021-12-27 12:11:44 +02:00
Ran Benita
50bf3625c9 Merge pull request #9445 from bluetech/backport-9441
[7.0.x] Backport "python: skip nose setup/teardown fixtures if non-callable"
2021-12-27 11:41:02 +02:00
Ran Benita
0002597ddd Merge pull request #9443 from bluetech/undocumented-hooks
doc/reference: add 4 missing hooks to reference

(cherry picked from commit 7a42db2bf0)
2021-12-27 11:14:58 +02:00
Ran Benita
41e424b172 Merge pull request #9441 from bluetech/nose-setup-callable
python: skip nose setup/teardown fixtures if non-callable
(cherry picked from commit 7fc2cf51c2)
2021-12-27 11:13:12 +02:00
Ran Benita
839ca90c0e Merge pull request #9439 from bluetech/backport-9416
[7.0.x] Backport "doc: fix a reference in "Writing hook functions""
2021-12-25 13:00:34 +02:00
Ran Benita
8f1a8800c8 Merge pull request #9416 from bluetech/doc-stash-fix
doc: fix a reference in "Writing hook functions"
(cherry picked from commit 443aa0219c)
2021-12-25 10:50:36 +02:00
Bruno Oliveira
045713ac2e Merge pull request #9418 from nicoddemus/backport-9417
[7.0.x] Fix test_errors_in_xfail_skip_expressions for Python 3.10.1
2021-12-16 10:57:49 -03:00
Bruno Oliveira
f094355401 Merge pull request #9417 from nicoddemus/fix-py3.10.1-9413 2021-12-16 10:38:43 -03:00
Bruno Oliveira
378baab126 Merge pull request #9409 from bluetech/backport-9401 2021-12-12 13:30:54 -03:00
Ran Benita
74b9f46e40 Merge pull request #9401 from bluetech/doc-hooks-ref
doc: add a `hook` crossref type
(cherry picked from commit 8040cfd965)
2021-12-12 18:05:19 +02:00
Bruno Oliveira
bf913eb4e7 Merge pull request #9398 from The-Compiler/backport-9390
[7.0.x] fix python version in changelog message (#9390)
2021-12-08 07:50:19 -03:00
Bruno Oliveira
813e1e5b54 Merge pull request #9399 from The-Compiler/backport-9394
[7.0.x] Hide internal stack when using pytest.approx() in bool context (#9394)
2021-12-08 07:50:11 -03:00
Bruno Oliveira
3587d6b526 Merge pull request #9397 from The-Compiler/backport-9389
[7.0.x] fix typo
2021-12-08 07:50:00 -03:00
Bruno Oliveira
76e108d06c Hide internal stack when using pytest.approx() in bool context (#9394)
This makes the error traceback point directly to the offending usage, rather
than to the internal `Approx.__bool__` method.

(cherry picked from commit 3ba9c01f9b)
2021-12-08 10:54:47 +01:00
Anthony Sottile
5f70fcba2e fix python version in changelog message (#9390)
(cherry picked from commit 7ae23ff8ae)
2021-12-08 10:54:19 +01:00
Éric
39028fac00 fix typo
(cherry picked from commit b691d31897)
2021-12-08 10:53:30 +01:00
Florian Bruhin
ac0870ebad scripts: Use release branch for changelog URL (#9380) (#9383)
* scripts: Use release branch for changelog URL

With a prerelease, /stable won't show the correct changelog.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
(cherry picked from commit 21a186bbda)
2021-12-07 19:19:42 +01:00
Florian Bruhin
871533322b Fix changelog URL in 7.0.0rc1 announcement (#9379) (#9382)
The changelog does not exist at /stable because an rc isn't stable...

(cherry picked from commit 5cb50fa13c)
2021-12-07 11:44:19 +01:00
Bruno Oliveira
df2c59c07e Merge pull request #9381 from nicoddemus/backport-9359 2021-12-07 07:20:38 -03:00
Yuval Shimon
9bfa02ea07 Fixed error message prints function decorators when using assert in Python 3.9 and above. (#9359) 2021-12-07 07:08:48 -03:00
github-actions[bot]
85897eddc6 Prepare release version 7.0.0rc1 (#9375)
Co-authored-by: pytest bot <pytestbot@gmail.com>
2021-12-07 09:37:18 +01:00
93 changed files with 1524 additions and 2146 deletions

View File

@@ -1,51 +0,0 @@
name: backport
on:
# Note that `pull_request_target` has security implications:
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
# In particular:
# - Only allow triggers that can be used only be trusted users
# - Don't execute any code from the target branch
# - Don't use cache
pull_request_target:
types: [labeled]
# Set permissions at the job level.
permissions: {}
jobs:
backport:
if: startsWith(github.event.label.name, 'backport ') && github.event.pull_request.merged
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: true
- name: Create backport PR
run: |
set -eux
git config --global user.name "pytest bot"
git config --global user.email "pytestbot@gmail.com"
label='${{ github.event.label.name }}'
target_branch="${label#backport }"
backport_branch=backport-${{ github.event.number }}-to-"${target_branch}"
subject="[$target_branch] $(gh pr view --json title -q .title ${{ github.event.number }})"
git checkout origin/"${target_branch}" -b "${backport_branch}"
git cherry-pick -x --mainline 1 ${{ github.event.pull_request.merge_commit_sha }}
git commit --amend --message "$subject"
git push --set-upstream origin --force-with-lease "${backport_branch}"
gh pr create \
--base "${target_branch}" \
--title "${subject}" \
--body "Backport of PR #${{ github.event.number }} to $target_branch branch. PR created by backport workflow."
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -5,7 +5,6 @@ on:
branches:
- main
- "[0-9]+.[0-9]+.x"
- "test-me-*"
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
@@ -32,6 +31,7 @@ jobs:
fail-fast: false
matrix:
name: [
"windows-py36",
"windows-py37",
"windows-py37-pluggy",
"windows-py38",
@@ -39,6 +39,7 @@ jobs:
"windows-py310",
"windows-py311",
"ubuntu-py36",
"ubuntu-py37",
"ubuntu-py37-pluggy",
"ubuntu-py37-freeze",
@@ -50,8 +51,6 @@ jobs:
"macos-py37",
"macos-py38",
"macos-py39",
"macos-py310",
"docs",
"doctesting",
@@ -59,6 +58,10 @@ jobs:
]
include:
- name: "windows-py36"
python: "3.6"
os: windows-latest
tox_env: "py36-xdist"
- name: "windows-py37"
python: "3.7"
os: windows-latest
@@ -85,6 +88,10 @@ jobs:
os: windows-latest
tox_env: "py311"
- name: "ubuntu-py36"
python: "3.6"
os: ubuntu-latest
tox_env: "py36-xdist"
- name: "ubuntu-py37"
python: "3.7"
os: ubuntu-latest
@@ -128,17 +135,9 @@ jobs:
os: macos-latest
tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39"
python: "3.9"
os: macos-latest
tox_env: "py39-xdist"
- name: "macos-py310"
python: "3.10"
os: macos-latest
tox_env: "py310-xdist"
- name: "plugins"
python: "3.9"
python: "3.7"
os: ubuntu-latest
tox_env: "plugins"

View File

@@ -1,16 +1,16 @@
repos:
- repo: https://github.com/psf/black
rev: 22.1.0
rev: 21.11b1
hooks:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: v1.12.1
rev: v1.12.0
hooks:
- id: blacken-docs
additional_dependencies: [black==20.8b1]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.0.1
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -20,32 +20,24 @@ repos:
- id: debug-statements
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/myint/autoflake
rev: v1.4
hooks:
- id: autoflake
name: autoflake
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
language: python
files: \.py$
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
hooks:
- id: flake8
language_version: python3
additional_dependencies:
- flake8-typing-imports==1.12.0
- flake8-typing-imports==1.9.0
- flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.7.1
rev: v2.6.0
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus]
args: ['--application-directories=.:src', --py36-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.0
rev: v2.29.1
hooks:
- id: pyupgrade
args: [--py37-plus]
args: [--py36-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.20.0
hooks:
@@ -56,7 +48,7 @@ repos:
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931
rev: v0.910-1
hooks:
- id: mypy
files: ^(src/|testing/)

View File

@@ -187,7 +187,6 @@ Kevin Cox
Kevin J. Foley
Kian-Meng Ang
Kodi B. Arfer
Kojo Idrissa
Kostis Anagnostopoulos
Kristoffer Nordström
Kyle Altendorf

View File

@@ -391,13 +391,6 @@ actual latest release). The procedure for this is:
request, as described above. An exception to this is if the bug fix is not
applicable to ``main`` anymore.
Automatic method:
Add a ``backport 1.2.x`` label to the PR you want to backport. This will create
a backport PR against the ``1.2.x`` branch.
Manual method:
#. ``git checkout origin/1.2.x -b backport-XXXX`` # use the main PR number here
#. Locate the merge commit on the PR, in the *merged* message, for example:

View File

@@ -100,7 +100,7 @@ Features
- 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
- Python 3.7+ or PyPy3
- Python 3.6+ and PyPy3
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community

View File

@@ -37,7 +37,7 @@ breaking changes or new features.
For a new minor release, first create a new maintenance branch from ``main``::
git fetch upstream
git fetch --all
git branch 7.1.x upstream/main
git push upstream 7.1.x
@@ -63,7 +63,7 @@ Major releases
1. Create a new maintenance branch from ``main``::
git fetch upstream
git fetch --all
git branch 8.0.x upstream/main
git push upstream 8.0.x
@@ -136,31 +136,29 @@ Both automatic and manual processes described above follow the same steps from t
#. After all tests pass and the PR has been approved, tag the release commit
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
git fetch upstream
git fetch --all
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
git push upstream MAJOR.MINOR.PATCH
git push git@github.com:pytest-dev/pytest.git MAJOR.MINOR.PATCH
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
#. Merge the PR.
#. Cherry-pick the CHANGELOG / announce files to the ``main`` branch::
git fetch upstream
git fetch --all --prune
git checkout upstream/main -b cherry-pick-release
git cherry-pick -x -m1 upstream/MAJOR.MINOR.x
#. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step.
#. For major and minor releases (or the first prerelease of it), tag the release cherry-pick merge commit in main with
#. For major and minor releases, tag the release cherry-pick merge commit in main with
a dev tag for the next feature release::
git checkout main
git pull
git tag MAJOR.{MINOR+1}.0.dev0
git push upstream MAJOR.{MINOR+1}.0.dev0
#. For major and minor releases, change the default version in the `Read the Docs Settings <https://readthedocs.org/dashboard/pytest/advanced/>`_ to the new branch.
git push git@github.com:pytest-dev/pytest.git MAJOR.{MINOR+1}.0.dev0
#. Send an email announcement with the contents from::

View File

@@ -6,7 +6,6 @@ Release announcements
:maxdepth: 2
release-7.1.0
release-7.0.1
release-7.0.0
release-7.0.0rc1

View File

@@ -1,48 +0,0 @@
pytest-7.1.0
=======================================
The pytest team is proud to announce the 7.1.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:
* Akuli
* Andrew Svetlov
* Anthony Sottile
* Brett Holman
* Bruno Oliveira
* Chris NeJame
* Dan Alvizu
* Elijah DeLee
* Emmanuel Arias
* Fabian Egli
* Florian Bruhin
* Gabor Szabo
* Hasan Ramezani
* Hugo van Kemenade
* Kian Meng, Ang
* Kojo Idrissa
* Masaru Tsuchiyama
* Olga Matoula
* P. L. Lim
* Ran Benita
* Tobias Deiminger
* Yuval Shimon
* eduardo naufel schettino
* Éric
Happy testing,
The pytest Development Team

View File

@@ -65,7 +65,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1334
pytestconfig [session scope] -- .../_pytest/fixtures.py:1365
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@@ -134,7 +134,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:487
caplog -- .../_pytest/logging.py:483
Access and control log capturing.
Captured logs are available through the following properties/methods::

View File

@@ -28,98 +28,6 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 7.1.0 (2022-03-13)
=========================
Breaking Changes
----------------
- `#8838 <https://github.com/pytest-dev/pytest/issues/8838>`_: As per our policy, the following features have been deprecated in the 6.X series and are now
removed:
* ``pytest._fillfuncargs`` function.
* ``pytest_warning_captured`` hook - use ``pytest_warning_recorded`` instead.
* ``-k -foobar`` syntax - use ``-k 'not foobar'`` instead.
* ``-k foobar:`` syntax.
* ``pytest.collect`` module - import from ``pytest`` directly.
For more information consult
`Deprecations and Removals <https://docs.pytest.org/en/latest/deprecations.html>`__ in the docs.
- `#9437 <https://github.com/pytest-dev/pytest/issues/9437>`_: Dropped support for Python 3.6, which reached `end-of-life <https://devguide.python.org/#status-of-python-branches>`__ at 2021-12-23.
Improvements
------------
- `#5192 <https://github.com/pytest-dev/pytest/issues/5192>`_: Fixed test output for some data types where ``-v`` would show less information.
Also, when showing diffs for sequences, ``-q`` would produce full diffs instead of the expected diff.
- `#9362 <https://github.com/pytest-dev/pytest/issues/9362>`_: pytest now avoids specialized assert formatting when it is detected that the default ``__eq__`` is overridden in ``attrs`` or ``dataclasses``.
- `#9536 <https://github.com/pytest-dev/pytest/issues/9536>`_: When ``-vv`` is given on command line, show skipping and xfail reasons in full instead of truncating them to fit the terminal width.
- `#9644 <https://github.com/pytest-dev/pytest/issues/9644>`_: More information about the location of resources that led Python to raise :class:`ResourceWarning` can now
be obtained by enabling :mod:`tracemalloc`.
See :ref:`resource-warnings` for more information.
- `#9678 <https://github.com/pytest-dev/pytest/issues/9678>`_: More types are now accepted in the ``ids`` argument to ``@pytest.mark.parametrize``.
Previously only `str`, `float`, `int` and `bool` were accepted;
now `bytes`, `complex`, `re.Pattern`, `Enum` and anything with a `__name__` are also accepted.
- `#9692 <https://github.com/pytest-dev/pytest/issues/9692>`_: :func:`pytest.approx` now raises a :class:`TypeError` when given an unordered sequence (such as :class:`set`).
Note that this implies that custom classes which only implement ``__iter__`` and ``__len__`` are no longer supported as they don't guarantee order.
Bug Fixes
---------
- `#8242 <https://github.com/pytest-dev/pytest/issues/8242>`_: The deprecation of raising :class:`unittest.SkipTest` to skip collection of
tests during the pytest collection phase is reverted - this is now a supported
feature again.
- `#9493 <https://github.com/pytest-dev/pytest/issues/9493>`_: Symbolic link components are no longer resolved in conftest paths.
This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice.
For example, given
tests/real/conftest.py
tests/real/test_it.py
tests/link -> tests/real
running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``.
This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details).
- `#9626 <https://github.com/pytest-dev/pytest/issues/9626>`_: Fixed count of selected tests on terminal collection summary when there were errors or skipped modules.
If there were errors or skipped modules on collection, pytest would mistakenly subtract those from the selected count.
- `#9645 <https://github.com/pytest-dev/pytest/issues/9645>`_: Fixed regression where ``--import-mode=importlib`` used together with :envvar:`PYTHONPATH` or :confval:`pythonpath` would cause import errors in test suites.
- `#9708 <https://github.com/pytest-dev/pytest/issues/9708>`_: :fixture:`pytester` now requests a :fixture:`monkeypatch` fixture instead of creating one internally. This solves some issues with tests that involve pytest environment variables.
- `#9730 <https://github.com/pytest-dev/pytest/issues/9730>`_: Malformed ``pyproject.toml`` files now produce a clearer error message.
pytest 7.0.1 (2022-02-11)
=========================
@@ -303,8 +211,6 @@ Deprecations
:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` /
:func:`unittest.skip` in unittest test cases is fully supported.
.. note:: This deprecation has been reverted in pytest 7.1.0.
- `#8315 <https://github.com/pytest-dev/pytest/issues/8315>`_: Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):

View File

@@ -382,6 +382,7 @@ texinfo_documents = [
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
"pluggy": ("https://pluggy.readthedocs.io/en/stable", None),
"python": ("https://docs.python.org/3", None),
@@ -389,6 +390,10 @@ intersphinx_mapping = {
"pip": ("https://pip.pypa.io/en/stable", None),
"tox": ("https://tox.wiki/en/stable", None),
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
"django": (
"http://docs.djangoproject.com/en/stable",
"http://docs.djangoproject.com/en/stable/_objects",
),
"setuptools": ("https://setuptools.pypa.io/en/stable", None),
}

View File

@@ -16,7 +16,7 @@ Deprecated Features
-------------------
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
.. _instance-collector-deprecation:
@@ -241,6 +241,19 @@ scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
Raising ``unittest.SkipTest`` during collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Raising :class:`unittest.SkipTest` to skip collection of tests during the
pytest collection phase is deprecated. Use :func:`pytest.skip` instead.
Note: This deprecation only relates to using `unittest.SkipTest` during test
collection. You are probably not doing that. Ordinary usage of
:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` /
:func:`unittest.skip` in unittest test cases is fully supported.
Using ``pytest.warns(None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -275,42 +288,29 @@ The ``yield_fixture`` function/decorator
It has been so for a very long time, so can be search/replaced safely.
Removed Features
----------------
The ``pytest_warning_captured`` hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
an appropriate period of deprecation has passed.
.. deprecated:: 6.0
This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``.
Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter
by a ``nodeid`` parameter.
The ``pytest.collect`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.0
.. versionremoved:: 7.0
The ``pytest.collect`` module is no longer part of the public API, all its names
should now be imported from ``pytest`` directly instead.
The ``pytest_warning_captured`` hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.0
.. versionremoved:: 7.0
This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``.
Use the ``pytest_warning_recorded`` hook instead, which replaces the ``item`` parameter
by a ``nodeid`` parameter.
The ``pytest._fillfuncargs`` function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.0
.. versionremoved:: 7.0
This function was kept for backward compatibility with an older plugin.
@@ -319,6 +319,12 @@ it, use `function._request._fillfixtures()` instead, though note this is not
a public API and may break in the future.
Removed Features
----------------
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
an appropriate period of deprecation has passed.
``--no-print-logs`` command-line option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -155,7 +155,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert [0, 1, 2] == [0, 1, 3]
E assert [0, 1, 2] == [0, 1, 3]
E At index 2 diff: 2 != 3
E Use -v to get more diff
E Use -v to get the full diff
failure_demo.py:63: AssertionError
______________ TestSpecialisedExplanations.test_eq_list_long _______________
@@ -168,7 +168,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert a == b
E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...]
E At index 100 diff: 1 != 2
E Use -v to get more diff
E Use -v to get the full diff
failure_demo.py:68: AssertionError
_________________ TestSpecialisedExplanations.test_eq_dict _________________
@@ -215,7 +215,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert [1, 2] == [1, 2, 3]
E assert [1, 2] == [1, 2, 3]
E Right contains one more item: 3
E Use -v to get more diff
E Use -v to get the full diff
failure_demo.py:77: AssertionError
_________________ TestSpecialisedExplanations.test_in_list _________________

View File

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

View File

@@ -201,7 +201,7 @@ if you run this module:
E '1'
E Extra items in the right set:
E '5'
E Use -v to get more diff
E Use -v to get the full diff
test_assert2.py:4: AssertionError
========================= short test summary info ==========================

View File

@@ -5,7 +5,7 @@ How to set up bash completion
=============================
When using bash as your shell, ``pytest`` can use argcomplete
(https://kislyuk.github.io/argcomplete/) for auto-completion.
(https://argcomplete.readthedocs.io/) for auto-completion.
For this ``argcomplete`` needs to be installed **and** enabled.
Install argcomplete using:

View File

@@ -358,7 +358,7 @@ Additional use cases of warnings in tests
Here are some use cases involving warnings that often come up in tests, and suggestions on how to deal with them:
- To ensure that **at least one** warning is emitted, use:
- To ensure that **any** warning is emitted, use:
.. code-block:: python
@@ -441,18 +441,3 @@ Please read our :ref:`backwards-compatibility` to learn how we proceed about dep
features.
The full list of warnings is listed in :ref:`the reference documentation <warnings ref>`.
.. _`resource-warnings`:
Resource Warnings
-----------------
Additional information of the source of a :class:`ResourceWarning` can be obtained when captured by pytest if
:mod:`tracemalloc` module is enabled.
One convenient way to enable :mod:`tracemalloc` when running tests is to set the :envvar:`PYTHONTRACEMALLOC` to a large
enough number of frames (say ``20``, but that number is application dependent).
For more information, consult the `Python Development Mode <https://docs.python.org/3/library/devmode.html>`__
section in the Python documentation.

View File

@@ -84,7 +84,7 @@ Executing pytest normally gives us this output (we are skipping the header to fo
> assert fruits1 == fruits2
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
E At index 2 diff: 'grapes' != 'orange'
E Use -v to get more diff
E Use -v to get the full diff
test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail _____________________________
@@ -99,7 +99,7 @@ Executing pytest normally gives us this output (we are skipping the header to fo
E {'1': 1, '2': 2, '3': 3, '4': 4}
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E Use -v to get more diff
E Use -v to get the full diff
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________

View File

@@ -21,7 +21,7 @@ there is no need to activate it.
Here is a little annotated list for some popular plugins:
* :pypi:`pytest-django`: write tests
for `django <https://docs.djangoproject.com/>`_ apps, using pytest integration.
for :std:doc:`django <django:index>` apps, using pytest integration.
* :pypi:`pytest-twisted`: write tests
for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and
@@ -51,6 +51,9 @@ Here is a little annotated list for some popular plugins:
* :pypi:`pytest-flakes`:
check source code with pyflakes.
* :pypi:`oejskit`:
a plugin to run javascript unittests in live browsers.
To see a complete list of all plugins with their latest testing
status against different pytest and Python versions, please visit
:ref:`plugin-list`.

View File

@@ -84,14 +84,14 @@ It is also possible to skip the whole module using
If you wish to skip something conditionally then you can use ``skipif`` instead.
Here is an example of marking a test function to be skipped
when run on an interpreter earlier than Python3.10:
when run on an interpreter earlier than Python3.6:
.. code-block:: python
import sys
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_function():
...

View File

@@ -121,10 +121,6 @@ the system temporary directory. The base name will be ``pytest-NUM`` where
``NUM`` will be incremented with each test run. Moreover, entries older
than 3 temporary directories will be removed.
The number of entries currently cannot be changed, but using the ``--basetemp``
option will remove the directory before every run, effectively meaning the temporary directories
of only the most recent run will be kept.
You can override the default temporary directory setting like this:
.. code-block:: bash

View File

@@ -1,16 +1,11 @@
:orphan:
.. sidebar:: Next Open Trainings
- `PyConDE <https://2022.pycon.de/program/W93DBJ/>`__, April 11th 2022 (3h), Berlin, Germany
- `PyConIT <https://pycon.it/en/talk/pytest-simple-rapid-and-fun-testing-with-python>`__, June 3rd 2022 (4h), Florence, Italy
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
Also see :doc:`previous talks and blogposts <talks>`.
..
- `Europython <https://ep2022.europython.eu/>`__, July 11th to 17th (3h), Dublin, Ireland
- `CH Open Workshoptage <https://workshoptage.ch/>`__ (German), September 6th to 8th (1 day), Bern, Switzerland
.. sidebar:: Next Open Trainings
- `Professional Testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, February 1st to 3rd, 2022, Leipzig (Germany) and remote.
Also see `previous talks and blogposts <talks.html>`_.
.. _features:
@@ -23,7 +18,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can
scale to support complex functional testing for applications and libraries.
``pytest`` requires: Python 3.7+ or PyPy3.
**Pythons**: ``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3.
**PyPI package name**: :pypi:`pytest`
@@ -84,7 +79,7 @@ Features
- 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.6+ and PyPy 3
- 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

@@ -808,6 +808,8 @@ Session related reporting hooks:
.. autofunction:: pytest_fixture_setup
.. hook:: pytest_fixture_post_finalizer
.. autofunction:: pytest_fixture_post_finalizer
.. hook:: pytest_warning_captured
.. autofunction:: pytest_warning_captured
.. hook:: pytest_warning_recorded
.. autofunction:: pytest_warning_recorded

View File

@@ -11,14 +11,9 @@ Books
- `Python Testing with pytest, by Brian Okken (2017)
<https://pragprog.com/book/bopytest/python-testing-with-pytest>`_.
- `Python Testing with pytest, Second Edition, by Brian Okken (2022)
<https://pragprog.com/titles/bopytest2/python-testing-with-pytest-second-edition>`_.
Talks and blog postings
---------------------------------------------
- `pytest: Simple, rapid and fun testing with Python, <https://youtu.be/cSJ-X3TbQ1c?t=15752>`_ (@ 4:22:32), Florian Bruhin, WeAreDevelopers World Congress 2021
- Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020
- Webinar: `Simplify Your Tests with Fixtures <https://blog.jetbrains.com/pycharm/2020/08/webinar-recording-simplify-your-tests-with-fixtures-with-oliver-bestwalter/>`_, Oliver Bestwalter, via JetBrains, 2020

View File

@@ -28,6 +28,8 @@ filterwarnings = [
"default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*",
# distutils is deprecated in 3.10, scheduled for removal in 3.12
"ignore:The distutils package is deprecated:DeprecationWarning",
# produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)."
"ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))",
# produced by pytest-xdist
"ignore:.*type argument to addoption.*:DeprecationWarning",
# produced on execnet (pytest-xdist)
@@ -111,4 +113,4 @@ template = "changelog/_template.rst"
showcontent = true
[tool.black]
target-version = ['py37']
target-version = ['py36']

View File

@@ -17,6 +17,7 @@ classifiers =
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
@@ -50,7 +51,7 @@ install_requires =
atomicwrites>=1.0;sys_platform=="win32"
colorama;sys_platform=="win32"
importlib-metadata>=0.12;python_version<"3.8"
python_requires = >=3.7
python_requires = >=3.6
package_dir =
=src
setup_requires =

View File

@@ -108,6 +108,7 @@ if os.environ.get("_ARGCOMPLETE"):
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
argcomplete.autocomplete(parser, always_complete_options=False)
else:
def try_argcomplete(parser: argparse.ArgumentParser) -> None:

View File

@@ -100,6 +100,9 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
spec is None
# this is a namespace package (without `__init__.py`)
# there's nothing to rewrite there
# python3.6: `namespace`
# python3.7+: `None`
or spec.origin == "namespace"
or spec.origin is None
# we can only rewrite source files
or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
@@ -292,8 +295,9 @@ def _write_pyc_fp(
# import. However, there's little reason to deviate.
fp.write(importlib.util.MAGIC_NUMBER)
# https://www.python.org/dev/peps/pep-0552/
flags = b"\x00\x00\x00\x00"
fp.write(flags)
if sys.version_info >= (3, 7):
flags = b"\x00\x00\x00\x00"
fp.write(flags)
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
size = source_stat.st_size & 0xFFFFFFFF
@@ -322,6 +326,7 @@ if sys.platform == "win32":
return False
return True
else:
def _write_pyc(
@@ -374,29 +379,31 @@ def _read_pyc(
except OSError:
return None
with fp:
# https://www.python.org/dev/peps/pep-0552/
has_flags = sys.version_info >= (3, 7)
try:
stat_result = os.stat(source)
mtime = int(stat_result.st_mtime)
size = stat_result.st_size
data = fp.read(16)
data = fp.read(16 if has_flags else 12)
except OSError as e:
trace(f"_read_pyc({source}): OSError {e}")
return None
# Check for invalid or out of date pyc file.
if len(data) != (16):
if len(data) != (16 if has_flags else 12):
trace("_read_pyc(%s): invalid pyc (too short)" % source)
return None
if data[:4] != importlib.util.MAGIC_NUMBER:
trace("_read_pyc(%s): invalid pyc (bad magic number)" % source)
return None
if data[4:8] != b"\x00\x00\x00\x00":
if has_flags and data[4:8] != b"\x00\x00\x00\x00":
trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source)
return None
mtime_data = data[8:12]
mtime_data = data[8 if has_flags else 4 : 12 if has_flags else 8]
if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
trace("_read_pyc(%s): out of date" % source)
return None
size_data = data[12:16]
size_data = data[12 if has_flags else 8 : 16 if has_flags else 12]
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
return None

View File

@@ -135,27 +135,6 @@ def isiterable(obj: Any) -> bool:
return False
def has_default_eq(
obj: object,
) -> bool:
"""Check if an instance of an object contains the default eq
First, we check if the object's __eq__ attribute has __code__,
if so, we check the equally of the method code filename (__code__.co_filename)
to the default one generated by the dataclass and attr module
for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
"""
# inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
code_filename = obj.__eq__.__code__.co_filename
if isattrs(obj):
return "attrs generated eq" in code_filename
return code_filename == "<string>" # data class
return True
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
"""Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose")
@@ -223,6 +202,8 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
explanation = _compare_eq_set(left, right, verbose)
elif isdict(left) and isdict(right):
explanation = _compare_eq_dict(left, right, verbose)
elif verbose > 0:
explanation = _compare_eq_verbose(left, right)
if isiterable(left) and isiterable(right):
expl = _compare_eq_iterable(left, right, verbose)
@@ -279,6 +260,18 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
return explanation
def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
keepends = True
left_lines = repr(left).splitlines(keepends)
right_lines = repr(right).splitlines(keepends)
explanation: List[str] = []
explanation += ["+" + line for line in left_lines]
explanation += ["-" + line for line in right_lines]
return explanation
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
"""Move opening/closing parenthesis/bracket to own lines."""
opening = lines[0][:1]
@@ -294,8 +287,8 @@ def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
def _compare_eq_iterable(
left: Iterable[Any], right: Iterable[Any], verbose: int = 0
) -> List[str]:
if verbose <= 0 and not running_on_ci():
return ["Use -v to get more diff"]
if not verbose and not running_on_ci():
return ["Use -v to get the full diff"]
# dynamic import to speedup pytest
import difflib
@@ -434,8 +427,6 @@ def _compare_eq_dict(
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
if not has_default_eq(left):
return []
if isdatacls(left):
all_fields = left.__dataclass_fields__
fields_to_check = [field for field, info in all_fields.items() if info.compare]

View File

@@ -68,8 +68,8 @@ def _colorama_workaround() -> None:
pass
def _windowsconsoleio_workaround(stream: TextIO) -> None:
"""Workaround for Windows Unicode console handling.
def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
"""Workaround for Windows Unicode console handling on Python>=3.6.
Python 3.6 implemented Unicode console handling for Windows. This works
by reading/writing to the raw console handle using
@@ -112,7 +112,7 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
buffering = -1
return io.TextIOWrapper(
open(os.dup(f.fileno()), mode, buffering),
open(os.dup(f.fileno()), mode, buffering), # type: ignore[arg-type]
f.encoding,
f.errors,
f.newlines,
@@ -128,7 +128,7 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
def pytest_load_initial_conftests(early_config: Config):
ns = early_config.known_args_namespace
if ns.capture == "fd":
_windowsconsoleio_workaround(sys.stdout)
_py36_windowsconsoleio_workaround(sys.stdout)
_colorama_workaround()
pluginmanager = early_config.pluginmanager
capman = CaptureManager(ns.capture)

View File

@@ -4,6 +4,7 @@ import functools
import inspect
import os
import sys
from contextlib import contextmanager
from inspect import Parameter
from inspect import signature
from pathlib import Path
@@ -185,6 +186,17 @@ def getfuncargnames(
return arg_names
if sys.version_info < (3, 7):
@contextmanager
def nullcontext():
yield
else:
from contextlib import nullcontext as nullcontext # noqa: F401
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
# Note: this code intentionally mirrors the code at the beginning of
# getfuncargnames, to get the arguments which were excluded from its result

View File

@@ -1,6 +1,7 @@
"""Command line options, ini-file and conftest.py processing."""
import argparse
import collections.abc
import contextlib
import copy
import enum
import inspect
@@ -352,17 +353,13 @@ class PytestPluginManager(PluginManager):
import _pytest.assertion
super().__init__("pytest")
# -- State related to local conftest plugins.
# All loaded conftest modules.
# The objects are module objects, only used generically.
self._conftest_plugins: Set[types.ModuleType] = set()
# All conftest modules applicable for a directory.
# This includes the directory's own conftest modules as well
# as those of its parent directories.
# State related to local conftest plugins.
self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
# Cutoff directory above which conftests are no longer discovered.
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
self._confcutdir: Optional[Path] = None
# If set, conftest loading is skipped.
self._noconftest = False
# _getconftestmodules()'s call to _get_directory() causes a stat
@@ -531,19 +528,6 @@ class PytestPluginManager(PluginManager):
if not foundanchor:
self._try_load_conftest(current, namespace.importmode, rootpath)
def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether a path is within the confcutdir.
When false, should not load conftest.
"""
if self._confcutdir is None:
return True
try:
path.relative_to(self._confcutdir)
except ValueError:
return False
return True
def _try_load_conftest(
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> None:
@@ -556,7 +540,7 @@ class PytestPluginManager(PluginManager):
def _getconftestmodules(
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> Sequence[types.ModuleType]:
) -> List[types.ModuleType]:
if self._noconftest:
return []
@@ -572,12 +556,14 @@ class PytestPluginManager(PluginManager):
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir.
clist = []
confcutdir_parents = self._confcutdir.parents if self._confcutdir else []
for parent in reversed((directory, *directory.parents)):
if self._is_in_confcutdir(parent):
conftestpath = parent / "conftest.py"
if conftestpath.is_file():
mod = self._importconftest(conftestpath, importmode, rootpath)
clist.append(mod)
if parent in confcutdir_parents:
continue
conftestpath = parent / "conftest.py"
if conftestpath.is_file():
mod = self._importconftest(conftestpath, importmode, rootpath)
clist.append(mod)
self._dirpath2confmods[directory] = clist
return clist
@@ -599,9 +585,15 @@ class PytestPluginManager(PluginManager):
def _importconftest(
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> types.ModuleType:
existing = self.get_plugin(str(conftestpath))
if existing is not None:
return cast(types.ModuleType, existing)
# Use a resolved Path object as key to avoid loading the same conftest
# twice with build systems that create build directories containing
# symlinks to actual files.
# Using Path().resolve() is better than py.path.realpath because
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
key = conftestpath.resolve()
with contextlib.suppress(KeyError):
return self._conftestpath2mod[key]
pkgpath = resolve_package_path(conftestpath)
if pkgpath is None:
@@ -617,10 +609,11 @@ class PytestPluginManager(PluginManager):
self._check_non_top_pytest_plugins(mod, conftestpath)
self._conftest_plugins.add(mod)
self._conftestpath2mod[key] = mod
dirpath = conftestpath.parent
if dirpath in self._dirpath2confmods:
for path, mods in self._dirpath2confmods.items():
if dirpath in path.parents or path == dirpath:
if path and dirpath in path.parents or path == dirpath:
assert mod not in mods
mods.append(mod)
self.trace(f"loading conftestmodule {mod!r}")
@@ -1348,6 +1341,14 @@ class Config:
if records:
frame = sys._getframe(stacklevel - 1)
location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
self.hook.pytest_warning_captured.call_historic(
kwargs=dict(
warning_message=records[0],
when="config",
item=None,
location=location,
)
)
self.hook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=records[0],
@@ -1447,7 +1448,6 @@ class Config:
)
except KeyError:
return None
assert mod.__file__ is not None
modpath = Path(mod.__file__).parent
values: List[Path] = []
for relroot in relroots:
@@ -1593,7 +1593,7 @@ def _strtobool(val: str) -> bool:
@lru_cache(maxsize=50)
def parse_warning_filter(
arg: str, *, escape: bool
) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]:
) -> Tuple[str, str, Type[Warning], str, int]:
"""Parse a warnings filter string.
This is copied from warnings._setoption with the following changes:
@@ -1635,7 +1635,7 @@ def parse_warning_filter(
parts.append("")
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
try:
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
except warnings._OptionError as e:
raise UsageError(error_template.format(error=str(e)))
try:

View File

@@ -70,7 +70,7 @@ def load_config_dict_from_file(
try:
config = tomli.loads(toml_text)
except tomli.TOMLDecodeError as exc:
raise UsageError(f"{filepath}: {exc}") from exc
raise UsageError(str(exc)) from exc
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
if result is not None:

View File

@@ -11,6 +11,7 @@ in case of warnings which need to format their messages.
from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestRemovedIn7Warning
from _pytest.warning_types import PytestRemovedIn8Warning
from _pytest.warning_types import UnformattedWarning
@@ -23,6 +24,18 @@ DEPRECATED_EXTERNAL_PLUGINS = {
}
FILLFUNCARGS = UnformattedWarning(
PytestRemovedIn7Warning,
"{name} is deprecated, use "
"function._request._fillfixtures() instead if you cannot avoid reaching into internals.",
)
PYTEST_COLLECT_MODULE = UnformattedWarning(
PytestRemovedIn7Warning,
"pytest.collect.{name} was moved to pytest.{name}\n"
"Please update to the new name.",
)
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
# * If you're in the future: "could have been".
YIELD_FIXTURE = PytestDeprecationWarning(
@@ -30,6 +43,20 @@ YIELD_FIXTURE = PytestDeprecationWarning(
"Use @pytest.fixture instead; they are the same."
)
MINUS_K_DASH = PytestRemovedIn7Warning(
"The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead."
)
MINUS_K_COLON = PytestRemovedIn7Warning(
"The `-k 'expr:'` syntax to -k is deprecated.\n"
"Please open an issue if you use this and want a replacement."
)
WARNING_CAPTURED_HOOK = PytestRemovedIn7Warning(
"The pytest_warning_captured is deprecated and will be removed in a future release.\n"
"Please use pytest_warning_recorded instead."
)
WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning(
"The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n"
"Please use pytest_load_initial_conftests hook instead."
@@ -47,6 +74,11 @@ STRICT_OPTION = PytestRemovedIn8Warning(
# This deprecation is never really meant to be removed.
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
UNITTEST_SKIP_DURING_COLLECTION = PytestRemovedIn8Warning(
"Raising unittest.SkipTest to skip tests during collection is deprecated. "
"Use pytest.skip() instead."
)
ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
'pytest now uses argparse. "%default" should be changed to "%(default)s"',
)

View File

@@ -656,7 +656,7 @@ def _init_checker_class() -> Type["doctest.OutputChecker"]:
precision = 0 if fraction is None else len(fraction)
if exponent is not None:
precision -= int(exponent)
if float(w.group()) == approx(float(g.group()), abs=10**-precision):
if float(w.group()) == approx(float(g.group()), abs=10 ** -precision):
# They're close enough. Replace the text we actually
# got with the text we want, so that it will match when we
# check the string literally.

View File

@@ -52,6 +52,7 @@ from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FILLFUNCARGS
from _pytest.deprecated import YIELD_FIXTURE
from _pytest.mark import Mark
from _pytest.mark import ParameterSet
@@ -72,6 +73,7 @@ if TYPE_CHECKING:
from _pytest.scope import _ScopeName
from _pytest.main import Session
from _pytest.python import CallSpec2
from _pytest.python import Function
from _pytest.python import Metafunc
@@ -350,6 +352,41 @@ def reorder_items_atscope(
return items_done
def _fillfuncargs(function: "Function") -> None:
"""Fill missing fixtures for a test function, old public API (deprecated)."""
warnings.warn(FILLFUNCARGS.format(name="pytest._fillfuncargs()"), stacklevel=2)
_fill_fixtures_impl(function)
def fillfixtures(function: "Function") -> None:
"""Fill missing fixtures for a test function (deprecated)."""
warnings.warn(
FILLFUNCARGS.format(name="_pytest.fixtures.fillfixtures()"), stacklevel=2
)
_fill_fixtures_impl(function)
def _fill_fixtures_impl(function: "Function") -> None:
"""Internal implementation to fill fixtures on the given function object."""
try:
request = function._request
except AttributeError:
# XXX this special code path is only expected to execute
# with the oejskit plugin. It uses classes with funcargs
# and we thus have to work a bit to allow this.
fm = function.session._fixturemanager
assert function.parent is not None
fi = fm.getfixtureinfo(function.parent, function.obj, None)
function._fixtureinfo = fi
request = function._request = FixtureRequest(function, _ispytest=True)
fm.session._setupstate.setup(function)
request._fillfixtures()
# Prune out funcargs for jstests.
function.funcargs = {name: function.funcargs[name] for name in fi.argnames}
else:
request._fillfixtures()
def get_direct_param_fixture_func(request):
return request.param
@@ -597,17 +634,8 @@ class FixtureRequest:
funcitem = self._pyfuncitem
scope = fixturedef._scope
try:
callspec = funcitem.callspec
except AttributeError:
callspec = None
if callspec is not None and argname in callspec.params:
param = callspec.params[argname]
param_index = callspec.indices[argname]
# If a parametrize invocation set a scope it will override
# the static scope defined with the fixture function.
with suppress(KeyError):
scope = callspec._arg2scope[argname]
else:
param = funcitem.callspec.getparam(argname)
except (AttributeError, ValueError):
param = NOTSET
param_index = 0
has_params = fixturedef.params is not None
@@ -647,6 +675,12 @@ class FixtureRequest:
)
)
fail(msg, pytrace=False)
else:
param_index = funcitem.callspec.indices[argname]
# If a parametrize invocation set a scope it will override
# the static scope defined with the fixture function.
with suppress(KeyError):
scope = funcitem.callspec._arg2scope[argname]
subrequest = SubRequest(
self, scope, param, param_index, fixturedef, _ispytest=True
@@ -930,7 +964,7 @@ def _eval_scope_callable(
@final
class FixtureDef(Generic[FixtureValue]):
"""A container for a fixture definition."""
"""A container for a factory definition."""
def __init__(
self,
@@ -942,56 +976,33 @@ class FixtureDef(Generic[FixtureValue]):
params: Optional[Sequence[object]],
unittest: bool = False,
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
Union[
Tuple[Union[None, str, float, int, bool], ...],
Callable[[Any], Optional[object]],
]
] = None,
) -> None:
self._fixturemanager = fixturemanager
# The "base" node ID for the fixture.
#
# This is a node ID prefix. A fixture is only available to a node (e.g.
# a `Function` item) if the fixture's baseid is a parent of the node's
# nodeid (see the `iterparentnodeids` function for what constitutes a
# "parent" and a "prefix" in this context).
#
# For a fixture found in a Collector's object (e.g. a `Module`s module,
# a `Class`'s class), the baseid is the Collector's nodeid.
#
# For a fixture found in a conftest plugin, the baseid is the conftest's
# directory path relative to the rootdir.
#
# For other plugins, the baseid is the empty string (always matches).
self.baseid = baseid or ""
# Whether the fixture was found from a node or a conftest in the
# collection tree. Will be false for fixtures defined in non-conftest
# plugins.
self.has_location = baseid is not None
# The fixture factory function.
self.func = func
# The name by which the fixture may be requested.
self.argname = argname
if scope is None:
scope = Scope.Function
elif callable(scope):
scope = _eval_scope_callable(scope, argname, fixturemanager.config)
if isinstance(scope, str):
scope = Scope.from_user(
scope, descr=f"Fixture '{func.__name__}'", where=baseid
)
self._scope = scope
# If the fixture is directly parametrized, the parameter values.
self.params: Optional[Sequence[object]] = params
# If the fixture is directly parametrized, a tuple of explicit IDs to
# assign to the parameter values, or a callable to generate an ID given
# a parameter value.
self.ids = ids
# The names requested by the fixtures.
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
# Whether the fixture was collected from a unittest TestCase class.
# Note that it really only makes sense to define autouse fixtures in
# unittest TestCases.
self.argnames: Tuple[str, ...] = getfuncargnames(
func, name=argname, is_method=unittest
)
self.unittest = unittest
# If the fixture was executed, the current value of the fixture.
# Can change if the fixture is executed with different parameters.
self.ids = ids
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
self._finalizers: List[Callable[[], object]] = []
@@ -1018,8 +1029,8 @@ class FixtureDef(Generic[FixtureValue]):
if exc:
raise exc
finally:
ihook = request.node.ihook
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
hook = self._fixturemanager.session.gethookproxy(request.node.path)
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
# Even if finalization fails, we invalidate the cached fixture
# value and remove all finalizers because they may be bound methods
# which will keep instances alive.
@@ -1053,8 +1064,8 @@ class FixtureDef(Generic[FixtureValue]):
self.finish(request)
assert self.cached_result is None
ihook = request.node.ihook
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
hook = self._fixturemanager.session.gethookproxy(request.node.path)
result = hook.pytest_fixture_setup(fixturedef=self, request=request)
return result
def cache_key(self, request: SubRequest) -> object:
@@ -1119,8 +1130,18 @@ def pytest_fixture_setup(
def _ensure_immutable_ids(
ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
],
) -> Optional[
Union[
Tuple[Union[None, str, float, int, bool], ...],
Callable[[Any], Optional[object]],
]
]:
if ids is None:
return None
if callable(ids):
@@ -1164,8 +1185,9 @@ class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
autouse: bool = False
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
ids: Union[
Tuple[Union[None, str, float, int, bool], ...],
Callable[[Any], Optional[object]],
] = attr.ib(
default=None,
converter=_ensure_immutable_ids,
@@ -1206,7 +1228,10 @@ def fixture(
params: Optional[Iterable[object]] = ...,
autouse: bool = ...,
ids: Optional[
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = ...,
name: Optional[str] = ...,
) -> FixtureFunction:
@@ -1221,7 +1246,10 @@ def fixture(
params: Optional[Iterable[object]] = ...,
autouse: bool = ...,
ids: Optional[
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = ...,
name: Optional[str] = None,
) -> FixtureFunctionMarker:
@@ -1235,7 +1263,10 @@ def fixture(
params: Optional[Iterable[object]] = None,
autouse: bool = False,
ids: Optional[
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = None,
name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, FixtureFunction]:
@@ -1277,7 +1308,7 @@ def fixture(
the fixture.
:param ids:
Sequence of ids each corresponding to the params so that they are
List of string ids each corresponding to the params so that they are
part of the test id. If no ids are provided they will be generated
automatically from the params.

View File

@@ -13,6 +13,7 @@ from typing import Union
from pluggy import HookspecMarker
from _pytest.deprecated import WARNING_CAPTURED_HOOK
from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
if TYPE_CHECKING:
@@ -33,10 +34,10 @@ if TYPE_CHECKING:
from _pytest.nodes import Collector
from _pytest.nodes import Item
from _pytest.outcomes import Exit
from _pytest.python import Class
from _pytest.python import Function
from _pytest.python import Metafunc
from _pytest.python import Module
from _pytest.python import PyCollector
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
from _pytest.runner import CallInfo
@@ -359,7 +360,7 @@ def pytest_pycollect_makemodule(
@hookspec(firstresult=True)
def pytest_pycollect_makeitem(
collector: Union["Module", "Class"], name: str, obj: object
collector: "PyCollector", name: str, obj: object
) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]:
"""Return a custom item/collector for a Python object in a module, or None.
@@ -776,6 +777,41 @@ def pytest_terminal_summary(
"""
@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
def pytest_warning_captured(
warning_message: "warnings.WarningMessage",
when: "Literal['config', 'collect', 'runtest']",
item: Optional["Item"],
location: Optional[Tuple[str, int, str]],
) -> None:
"""(**Deprecated**) Process a warning captured by the internal pytest warnings plugin.
.. deprecated:: 6.0
This hook is considered deprecated and will be removed in a future pytest version.
Use :func:`pytest_warning_recorded` instead.
:param warnings.WarningMessage warning_message:
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
the same attributes as the parameters of :py:func:`warnings.showwarning`.
:param str when:
Indicates when the warning was captured. Possible values:
* ``"config"``: during pytest configuration/initialization stage.
* ``"collect"``: during test collection.
* ``"runtest"``: during test execution.
:param pytest.Item|None item:
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
:param tuple location:
When available, holds information about the execution context of the captured
warning (filename, linenumber, function). ``function`` evaluates to <module>
when the execution context is at the module level.
"""
@hookspec(historic=True)
def pytest_warning_recorded(
warning_message: "warnings.WarningMessage",

View File

@@ -92,7 +92,7 @@ class _NodeReporter:
self.xml = xml
self.add_stats = self.xml.add_stats
self.family = self.xml.family
self.duration = 0.0
self.duration = 0
self.properties: List[Tuple[str, str]] = []
self.nodes: List[ET.Element] = []
self.attrs: Dict[str, str] = {}

View File

@@ -1,10 +1,9 @@
"""Access and control log capturing."""
import io
import logging
import os
import re
import sys
from contextlib import contextmanager
from contextlib import nullcontext
from io import StringIO
from pathlib import Path
from typing import AbstractSet
@@ -14,7 +13,6 @@ from typing import List
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@@ -22,6 +20,7 @@ from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.capture import CaptureManager
from _pytest.compat import final
from _pytest.compat import nullcontext
from _pytest.config import _strtobool
from _pytest.config import Config
from _pytest.config import create_terminal_writer
@@ -35,11 +34,6 @@ from _pytest.main import Session
from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter
if TYPE_CHECKING:
logging_StreamHandler = logging.StreamHandler[StringIO]
else:
logging_StreamHandler = logging.StreamHandler
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
@@ -328,9 +322,11 @@ class catching_logs:
root_logger.removeHandler(self.handler)
class LogCaptureHandler(logging_StreamHandler):
class LogCaptureHandler(logging.StreamHandler):
"""A logging handler that stores log records and the log text."""
stream: StringIO
def __init__(self) -> None:
"""Create a new log handler."""
super().__init__(StringIO())
@@ -625,11 +621,20 @@ class LoggingPlugin:
if not fpath.parent.exists():
fpath.parent.mkdir(exist_ok=True, parents=True)
# https://github.com/python/mypy/issues/11193
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
old_stream = self.log_file_handler.setStream(stream)
stream = fpath.open(mode="w", encoding="UTF-8")
if sys.version_info >= (3, 7):
old_stream = self.log_file_handler.setStream(stream)
else:
old_stream = self.log_file_handler.stream
self.log_file_handler.acquire()
try:
self.log_file_handler.flush()
self.log_file_handler.stream = stream
finally:
self.log_file_handler.release()
if old_stream:
old_stream.close()
# https://github.com/python/typeshed/pull/5663
old_stream.close() # type:ignore[attr-defined]
def _log_cli_enabled(self):
"""Return whether live logging is enabled."""
@@ -753,7 +758,7 @@ class _FileHandler(logging.FileHandler):
pass
class _LiveLoggingStreamHandler(logging_StreamHandler):
class _LiveLoggingStreamHandler(logging.StreamHandler):
"""A logging StreamHandler used by the live logging feature: it will
write a newline before the first log message in each test.

View File

@@ -689,8 +689,9 @@ class Session(nodes.FSCollector):
# No point in finding packages when collecting doctests.
if not self.config.getoption("doctestmodules", False):
pm = self.config.pluginmanager
confcutdir = pm._confcutdir
for parent in (argpath, *argpath.parents):
if not pm._is_in_confcutdir(argpath):
if confcutdir and parent in confcutdir.parents:
break
if parent.is_dir():

View File

@@ -1,4 +1,5 @@
"""Generic mechanism for marking and selecting python functions."""
import warnings
from typing import AbstractSet
from typing import Collection
from typing import List
@@ -22,6 +23,8 @@ from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
from _pytest.deprecated import MINUS_K_COLON
from _pytest.deprecated import MINUS_K_DASH
from _pytest.stash import StashKey
if TYPE_CHECKING:
@@ -186,14 +189,27 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
if not keywordexpr:
return
if keywordexpr.startswith("-"):
# To be removed in pytest 8.0.0.
warnings.warn(MINUS_K_DASH, stacklevel=2)
keywordexpr = "not " + keywordexpr[1:]
selectuntil = False
if keywordexpr[-1:] == ":":
# To be removed in pytest 8.0.0.
warnings.warn(MINUS_K_COLON, stacklevel=2)
selectuntil = True
keywordexpr = keywordexpr[:-1]
expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'")
remaining = []
deselected = []
for colitem in items:
if not expr.evaluate(KeywordMatcher.from_item(colitem)):
if keywordexpr and not expr.evaluate(KeywordMatcher.from_item(colitem)):
deselected.append(colitem)
else:
if selectuntil:
keywordexpr = None
remaining.append(colitem)
if deselected:

View File

@@ -72,11 +72,16 @@ def get_empty_parameterset_mark(
return mark
class ParameterSet(NamedTuple):
values: Sequence[Union[object, NotSetType]]
marks: Collection[Union["MarkDecorator", "Mark"]]
id: Optional[str]
class ParameterSet(
NamedTuple(
"ParameterSet",
[
("values", Sequence[Union[object, NotSetType]]),
("marks", Collection[Union["MarkDecorator", "Mark"]]),
("id", Optional[str]),
],
)
):
@classmethod
def param(
cls,

View File

@@ -55,7 +55,7 @@ def resolve(name: str) -> object:
parts = name.split(".")
used = parts.pop(0)
found: object = __import__(used)
found = __import__(used)
for part in parts:
used += "." + part
try:

View File

@@ -539,9 +539,6 @@ def import_path(
ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
if ignore != "1":
module_file = mod.__file__
if module_file is None:
raise ImportPathMismatchError(module_name, module_file, path)
if module_file.endswith((".pyc", ".pyo")):
module_file = module_file[:-1]
if module_file.endswith(os.path.sep + "__init__.py"):
@@ -565,6 +562,7 @@ if sys.platform.startswith("win"):
def _is_same(f1: str, f2: str) -> bool:
return Path(f1) == Path(f2) or os.path.samefile(f1, f2)
else:
def _is_same(f1: str, f2: str) -> bool:
@@ -603,20 +601,11 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
module_parts = module_name.split(".")
while module_name:
if module_name not in modules:
try:
# If sys.meta_path is empty, calling import_module will issue
# a warning and raise ModuleNotFoundError. To avoid the
# warning, we check sys.meta_path explicitly and raise the error
# ourselves to fall back to creating a dummy module.
if not sys.meta_path:
raise ModuleNotFoundError
importlib.import_module(module_name)
except ModuleNotFoundError:
module = ModuleType(
module_name,
doc="Empty module created by pytest's importmode=importlib.",
)
modules[module_name] = module
module = ModuleType(
module_name,
doc="Empty module created by pytest's importmode=importlib.",
)
modules[module_name] = module
module_parts.pop(-1)
module_name = ".".join(module_parts)

View File

@@ -128,7 +128,7 @@ class LsofFdLeakChecker:
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=True,
text=True,
universal_newlines=True,
).stdout
def isopen(line: str) -> bool:
@@ -477,9 +477,7 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
@fixture
def pytester(
request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
) -> "Pytester":
def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester":
"""
Facilities to write tests/configuration files, execute pytest in isolation, and match
against expected output, perfect for black-box testing of pytest plugins.
@@ -490,7 +488,7 @@ def pytester(
It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
fixture but provides methods which aid in testing pytest itself.
"""
return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True)
return Pytester(request, tmp_path_factory, _ispytest=True)
@fixture
@@ -685,7 +683,6 @@ class Pytester:
self,
request: FixtureRequest,
tmp_path_factory: TempPathFactory,
monkeypatch: MonkeyPatch,
*,
_ispytest: bool = False,
) -> None:
@@ -709,7 +706,7 @@ class Pytester:
self._method = self._request.config.getoption("--runpytest")
self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
self._monkeypatch = mp = monkeypatch
self._monkeypatch = mp = MonkeyPatch()
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
# Ensure no unexpected caching via tox.
mp.delenv("TOX_ENV_DIR", raising=False)
@@ -741,6 +738,7 @@ class Pytester:
self._sys_modules_snapshot.restore()
self._sys_path_snapshot.restore()
self._cwd_snapshot.restore()
self._monkeypatch.undo()
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
# Some zope modules used by twisted-related tests keep internal state

View File

@@ -224,15 +224,11 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
@hookimpl(trylast=True)
def pytest_pycollect_makeitem(
collector: Union["Module", "Class"], name: str, obj: object
) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]:
assert isinstance(collector, (Class, Module)), type(collector)
def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
# Nothing was collected elsewhere, let's do it here.
if safe_isclass(obj):
if collector.istestclass(obj, name):
klass: Class = Class.from_parent(collector, name=name, obj=obj)
return klass
return Class.from_parent(collector, name=name, obj=obj)
elif collector.istestfunction(obj, name):
# mock seems to store unbound methods (issue473), normalize it.
obj = getattr(obj, "__func__", obj)
@@ -251,16 +247,15 @@ def pytest_pycollect_makeitem(
)
elif getattr(obj, "__test__", True):
if is_generator(obj):
res: Function = Function.from_parent(collector, name=name)
res = Function.from_parent(collector, name=name)
reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
name=name
)
res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
res.warn(PytestCollectionWarning(reason))
return res
else:
return list(collector._genfunctions(name, obj))
return None
res = list(collector._genfunctions(name, obj))
return res
class PyobjMixin(nodes.Node):
@@ -303,9 +298,6 @@ class PyobjMixin(nodes.Node):
# used to avoid Function marker duplication
if self._ALLOW_MARKERS:
self.own_markers.extend(get_unpacked_marks(self.obj))
# This assumes that `obj` is called before there is a chance
# to add custom keys to `self.keywords`, so no fear of overriding.
self.keywords.update((mark.name, mark) for mark in self.own_markers)
return obj
@obj.setter
@@ -343,7 +335,6 @@ class PyobjMixin(nodes.Node):
if isinstance(compat_co_firstlineno, int):
# nose compatibility
file_path = sys.modules[obj.__module__].__file__
assert file_path is not None
if file_path.endswith(".pyc"):
file_path = file_path[:-1]
path: Union["os.PathLike[str]", str] = file_path
@@ -428,7 +419,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
for basecls in self.obj.__mro__:
dicts.append(basecls.__dict__)
# In each class, nodes should be definition ordered.
# In each class, nodes should be definition ordered. Since Python 3.6,
# __dict__ is definition ordered.
seen: Set[str] = set()
dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = []
@@ -905,7 +896,11 @@ class InstanceDummy:
only to ignore it; this dummy class keeps them working. This will be removed
in pytest 8."""
pass
# Note: module __getattr__ only works on Python>=3.7. Unfortunately
# we can't provide this deprecation warning on Python 3.6.
def __getattr__(name: str) -> object:
if name == "Instance":
warnings.warn(INSTANCE_COLLECTOR, 2)
@@ -927,159 +922,6 @@ def hasnew(obj: object) -> bool:
return False
@final
@attr.s(frozen=True, auto_attribs=True, slots=True)
class IdMaker:
"""Make IDs for a parametrization."""
# The argnames of the parametrization.
argnames: Sequence[str]
# The ParameterSets of the parametrization.
parametersets: Sequence[ParameterSet]
# Optionally, a user-provided callable to make IDs for parameters in a
# ParameterSet.
idfn: Optional[Callable[[Any], Optional[object]]]
# Optionally, explicit IDs for ParameterSets by index.
ids: Optional[Sequence[Optional[object]]]
# Optionally, the pytest config.
# Used for controlling ASCII escaping, and for calling the
# :hook:`pytest_make_parametrize_id` hook.
config: Optional[Config]
# Optionally, the ID of the node being parametrized.
# Used only for clearer error messages.
nodeid: Optional[str]
# Optionally, the ID of the function being parametrized.
# Used only for clearer error messages.
func_name: Optional[str]
def make_unique_parameterset_ids(self) -> List[str]:
"""Make a unique identifier for each ParameterSet, that may be used to
identify the parametrization in a node ID.
Format is <prm_1_token>-...-<prm_n_token>[counter], where prm_x_token is
- user-provided id, if given
- else an id derived from the value, applicable for certain types
- else <argname><parameterset index>
The counter suffix is appended only in case a string wouldn't be unique
otherwise.
"""
resolved_ids = list(self._resolve_ids())
# All IDs must be unique!
if len(resolved_ids) != len(set(resolved_ids)):
# Record the number of occurrences of each ID.
id_counts = Counter(resolved_ids)
# Map the ID to its next suffix.
id_suffixes: Dict[str, int] = defaultdict(int)
# Suffix non-unique IDs to make them unique.
for index, id in enumerate(resolved_ids):
if id_counts[id] > 1:
resolved_ids[index] = f"{id}{id_suffixes[id]}"
id_suffixes[id] += 1
return resolved_ids
def _resolve_ids(self) -> Iterable[str]:
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
for idx, parameterset in enumerate(self.parametersets):
if parameterset.id is not None:
# ID provided directly - pytest.param(..., id="...")
yield parameterset.id
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
# ID provided in the IDs list - parametrize(..., ids=[...]).
yield self._idval_from_value_required(self.ids[idx], idx)
else:
# ID not provided - generate it.
yield "-".join(
self._idval(val, argname, idx)
for val, argname in zip(parameterset.values, self.argnames)
)
def _idval(self, val: object, argname: str, idx: int) -> str:
"""Make an ID for a parameter in a ParameterSet."""
idval = self._idval_from_function(val, argname, idx)
if idval is not None:
return idval
idval = self._idval_from_hook(val, argname)
if idval is not None:
return idval
idval = self._idval_from_value(val)
if idval is not None:
return idval
return self._idval_from_argname(argname, idx)
def _idval_from_function(
self, val: object, argname: str, idx: int
) -> Optional[str]:
"""Try to make an ID for a parameter in a ParameterSet using the
user-provided id callable, if given."""
if self.idfn is None:
return None
try:
id = self.idfn(val)
except Exception as e:
prefix = f"{self.nodeid}: " if self.nodeid is not None else ""
msg = "error raised while trying to determine id of parameter '{}' at position {}"
msg = prefix + msg.format(argname, idx)
raise ValueError(msg) from e
if id is None:
return None
return self._idval_from_value(id)
def _idval_from_hook(self, val: object, argname: str) -> Optional[str]:
"""Try to make an ID for a parameter in a ParameterSet by calling the
:hook:`pytest_make_parametrize_id` hook."""
if self.config:
id: Optional[str] = self.config.hook.pytest_make_parametrize_id(
config=self.config, val=val, argname=argname
)
return id
return None
def _idval_from_value(self, val: object) -> Optional[str]:
"""Try to make an ID for a parameter in a ParameterSet from its value,
if the value type is supported."""
if isinstance(val, STRING_TYPES):
return _ascii_escaped_by_config(val, self.config)
elif val is None or isinstance(val, (float, int, bool, complex)):
return str(val)
elif isinstance(val, Pattern):
return ascii_escaped(val.pattern)
elif val is NOTSET:
# Fallback to default. Note that NOTSET is an enum.Enum.
pass
elif isinstance(val, enum.Enum):
return str(val)
elif isinstance(getattr(val, "__name__", None), str):
# Name of a class, function, module, etc.
name: str = getattr(val, "__name__")
return name
return None
def _idval_from_value_required(self, val: object, idx: int) -> str:
"""Like _idval_from_value(), but fails if the type is not supported."""
id = self._idval_from_value(val)
if id is not None:
return id
# Fail.
if self.func_name is not None:
prefix = f"In {self.func_name}: "
elif self.nodeid is not None:
prefix = f"In {self.nodeid}: "
else:
prefix = ""
msg = (
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
)
fail(msg, pytrace=False)
@staticmethod
def _idval_from_argname(argname: str, idx: int) -> str:
"""Make an ID for a parameter in a ParameterSet from the argument name
and the index of the ParameterSet."""
return str(argname) + str(idx)
@final
@attr.s(frozen=True, slots=True, auto_attribs=True)
class CallSpec2:
@@ -1202,7 +1044,10 @@ class Metafunc:
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
indirect: Union[bool, Sequence[str]] = False,
ids: Optional[
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = None,
scope: "Optional[_ScopeName]" = None,
*,
@@ -1269,7 +1114,7 @@ class Metafunc:
It will also override any fixture-function defined scope, allowing
to set a dynamic scope using test context or configuration.
"""
argnames, parametersets = ParameterSet._for_parametrize(
argnames, parameters = ParameterSet._for_parametrize(
argnames,
argvalues,
self.function,
@@ -1301,8 +1146,8 @@ class Metafunc:
if generated_ids is not None:
ids = generated_ids
ids = self._resolve_parameter_set_ids(
argnames, ids, parametersets, nodeid=self.definition.nodeid
ids = self._resolve_arg_ids(
argnames, ids, parameters, nodeid=self.definition.nodeid
)
# Store used (possibly generated) ids with parametrize Marks.
@@ -1314,9 +1159,7 @@ class Metafunc:
# of all calls.
newcalls = []
for callspec in self._calls or [CallSpec2()]:
for param_index, (param_id, param_set) in enumerate(
zip(ids, parametersets)
):
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
newcallspec = callspec.setmulti(
valtypes=arg_values_types,
argnames=argnames,
@@ -1329,29 +1172,27 @@ class Metafunc:
newcalls.append(newcallspec)
self._calls = newcalls
def _resolve_parameter_set_ids(
def _resolve_arg_ids(
self,
argnames: Sequence[str],
ids: Optional[
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
],
parametersets: Sequence[ParameterSet],
parameters: Sequence[ParameterSet],
nodeid: str,
) -> List[str]:
"""Resolve the actual ids for the given parameter sets.
"""Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
to ``parametrize``.
:param argnames:
Argument names passed to ``parametrize()``.
:param ids:
The `ids` parameter of the ``parametrize()`` call (see docs).
:param parametersets:
The parameter sets, each containing a set of values corresponding
to ``argnames``.
:param nodeid str:
The nodeid of the definition item that generated this
parametrization.
:returns:
List with ids for each parameter set given.
:param List[str] argnames: List of argument names passed to ``parametrize()``.
:param ids: The ids parameter of the parametrized call (see docs).
:param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``.
:param str str: The nodeid of the item that generated this parametrized call.
:rtype: List[str]
:returns: The list of ids for each argname given.
"""
if ids is None:
idfn = None
@@ -1361,24 +1202,15 @@ class Metafunc:
ids_ = None
else:
idfn = None
ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
id_maker = IdMaker(
argnames,
parametersets,
idfn,
ids_,
self.config,
nodeid=nodeid,
func_name=self.function.__name__,
)
return id_maker.make_unique_parameterset_ids()
ids_ = self._validate_ids(ids, parameters, self.function.__name__)
return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid)
def _validate_ids(
self,
ids: Iterable[Optional[object]],
parametersets: Sequence[ParameterSet],
ids: Iterable[Union[None, str, float, int, bool]],
parameters: Sequence[ParameterSet],
func_name: str,
) -> List[Optional[object]]:
) -> List[Union[None, str]]:
try:
num_ids = len(ids) # type: ignore[arg-type]
except TypeError:
@@ -1386,14 +1218,29 @@ class Metafunc:
iter(ids)
except TypeError as e:
raise TypeError("ids must be a callable or an iterable") from e
num_ids = len(parametersets)
num_ids = len(parameters)
# num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
if num_ids != len(parametersets) and num_ids != 0:
if num_ids != len(parameters) and num_ids != 0:
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
fail(msg.format(func_name, len(parameters), num_ids), pytrace=False)
return list(itertools.islice(ids, num_ids))
new_ids = []
for idx, id_value in enumerate(itertools.islice(ids, num_ids)):
if id_value is None or isinstance(id_value, str):
new_ids.append(id_value)
elif isinstance(id_value, (float, int, bool)):
new_ids.append(str(id_value))
else:
msg = ( # type: ignore[unreachable]
"In {}: ids must be list of string/float/int/bool, "
"found: {} (type: {!r}) at index {}"
)
fail(
msg.format(func_name, saferepr(id_value), type(id_value), idx),
pytrace=False,
)
return new_ids
def _resolve_arg_value_types(
self,
@@ -1513,6 +1360,105 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
return val if escape_option else ascii_escaped(val) # type: ignore
def _idval(
val: object,
argname: str,
idx: int,
idfn: Optional[Callable[[Any], Optional[object]]],
nodeid: Optional[str],
config: Optional[Config],
) -> str:
if idfn:
try:
generated_id = idfn(val)
if generated_id is not None:
val = generated_id
except Exception as e:
prefix = f"{nodeid}: " if nodeid is not None else ""
msg = "error raised while trying to determine id of parameter '{}' at position {}"
msg = prefix + msg.format(argname, idx)
raise ValueError(msg) from e
elif config:
hook_id: Optional[str] = config.hook.pytest_make_parametrize_id(
config=config, val=val, argname=argname
)
if hook_id:
return hook_id
if isinstance(val, STRING_TYPES):
return _ascii_escaped_by_config(val, config)
elif val is None or isinstance(val, (float, int, bool, complex)):
return str(val)
elif isinstance(val, Pattern):
return ascii_escaped(val.pattern)
elif val is NOTSET:
# Fallback to default. Note that NOTSET is an enum.Enum.
pass
elif isinstance(val, enum.Enum):
return str(val)
elif isinstance(getattr(val, "__name__", None), str):
# Name of a class, function, module, etc.
name: str = getattr(val, "__name__")
return name
return str(argname) + str(idx)
def _idvalset(
idx: int,
parameterset: ParameterSet,
argnames: Iterable[str],
idfn: Optional[Callable[[Any], Optional[object]]],
ids: Optional[List[Union[None, str]]],
nodeid: Optional[str],
config: Optional[Config],
) -> str:
if parameterset.id is not None:
return parameterset.id
id = None if ids is None or idx >= len(ids) else ids[idx]
if id is None:
this_id = [
_idval(val, argname, idx, idfn, nodeid=nodeid, config=config)
for val, argname in zip(parameterset.values, argnames)
]
return "-".join(this_id)
else:
return _ascii_escaped_by_config(id, config)
def idmaker(
argnames: Iterable[str],
parametersets: Iterable[ParameterSet],
idfn: Optional[Callable[[Any], Optional[object]]] = None,
ids: Optional[List[Union[None, str]]] = None,
config: Optional[Config] = None,
nodeid: Optional[str] = None,
) -> List[str]:
resolved_ids = [
_idvalset(
valindex, parameterset, argnames, idfn, ids, config=config, nodeid=nodeid
)
for valindex, parameterset in enumerate(parametersets)
]
# All IDs must be unique!
unique_ids = set(resolved_ids)
if len(unique_ids) != len(resolved_ids):
# Record the number of occurrences of each test ID.
test_id_counts = Counter(resolved_ids)
# Map the test ID to its next suffix.
test_id_suffixes: Dict[str, int] = defaultdict(int)
# Suffix non-unique IDs to make them unique.
for index, test_id in enumerate(resolved_ids):
if test_id_counts[test_id] > 1:
resolved_ids[index] = f"{test_id}{test_id_suffixes[test_id]}"
test_id_suffixes[test_id] += 1
return resolved_ids
def _pretty_fixture_path(func) -> str:
cwd = Path.cwd()
loc = Path(getlocation(func, str(cwd)))
@@ -1684,7 +1630,7 @@ class Function(PyobjMixin, nodes.Item):
config: Optional[Config] = None,
callspec: Optional[CallSpec2] = None,
callobj=NOTSET,
keywords: Optional[Mapping[str, Any]] = None,
keywords=None,
session: Optional[Session] = None,
fixtureinfo: Optional[FuncFixtureInfo] = None,
originalname: Optional[str] = None,
@@ -1705,20 +1651,31 @@ class Function(PyobjMixin, nodes.Item):
# Note: when FunctionDefinition is introduced, we should change ``originalname``
# to a readonly property that returns FunctionDefinition.name.
self.keywords.update(self.obj.__dict__)
self.own_markers.extend(get_unpacked_marks(self.obj))
if callspec:
self.callspec = callspec
self.own_markers.extend(callspec.marks)
# this is total hostile and a mess
# keywords are broken by design by now
# this will be redeemed later
for mark in callspec.marks:
# feel free to cry, this was broken for years before
# and keywords can't fix it per design
self.keywords[mark.name] = mark
self.own_markers.extend(normalize_mark_list(callspec.marks))
if keywords:
self.keywords.update(keywords)
# todo: this is a hell of a hack
# https://github.com/pytest-dev/pytest/issues/4569
# Note: the order of the updates is important here; indicates what
# takes priority (ctor argument over function attributes over markers).
# Take own_markers only; NodeKeywords handles parent traversal on its own.
self.keywords.update((mark.name, mark) for mark in self.own_markers)
self.keywords.update(self.obj.__dict__)
if keywords:
self.keywords.update(keywords)
self.keywords.update(
{
mark.name: True
for mark in self.iter_markers()
if mark.name not in self.keywords
}
)
if fixtureinfo is None:
fixtureinfo = self.session._fixturemanager.getfixtureinfo(

View File

@@ -1,6 +1,5 @@
import math
import pprint
from collections.abc import Collection
from collections.abc import Sized
from decimal import Decimal
from numbers import Complex
@@ -9,6 +8,7 @@ from typing import Any
from typing import Callable
from typing import cast
from typing import Generic
from typing import Iterable
from typing import List
from typing import Mapping
from typing import Optional
@@ -131,6 +131,7 @@ class ApproxBase:
# a numeric type. For this reason, the default is to do nothing. The
# classes that deal with sequences should reimplement this method to
# raise if there are any non-numeric elements in the sequence.
pass
def _recursive_list_map(f, x):
@@ -306,12 +307,12 @@ class ApproxMapping(ApproxBase):
raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
class ApproxSequenceLike(ApproxBase):
class ApproxSequencelike(ApproxBase):
"""Perform approximate comparisons where the expected value is a sequence of numbers."""
def __repr__(self) -> str:
seq_type = type(self.expected)
if seq_type not in (tuple, list):
if seq_type not in (tuple, list, set):
seq_type = list
return "approx({!r})".format(
seq_type(self._approx_scalar(x) for x in self.expected)
@@ -515,7 +516,7 @@ class ApproxDecimal(ApproxScalar):
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
"""Assert that two numbers (or two ordered sequences of numbers) are equal to each other
"""Assert that two numbers (or two sets of numbers) are equal to each other
within some tolerance.
Due to the :std:doc:`tutorial/floatingpoint`, numbers that we
@@ -547,11 +548,16 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
>>> 0.1 + 0.2 == approx(0.3)
True
The same syntax also works for ordered sequences of numbers::
The same syntax also works for sequences of numbers::
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
True
Dictionary *values*::
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
True
``numpy`` arrays::
>>> import numpy as np # doctest: +SKIP
@@ -564,20 +570,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
True
Only ordered sequences are supported, because ``approx`` needs
to infer the relative position of the sequences without ambiguity. This means
``sets`` and other unordered sequences are not supported.
Finally, dictionary *values* can also be compared::
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
True
The comparision will be true if both mappings have the same keys and their
respective values match the expected tolerances.
**Tolerances**
By default, ``approx`` considers numbers within a relative tolerance of
``1e-6`` (i.e. one part in a million) of its expected value to be equal.
This treatment would lead to surprising results if the expected value was
@@ -717,19 +709,12 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
expected = _as_numpy_array(expected)
cls = ApproxNumpy
elif (
hasattr(expected, "__getitem__")
isinstance(expected, Iterable)
and isinstance(expected, Sized)
# Type ignored because the error is wrong -- not unreachable.
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
):
cls = ApproxSequenceLike
elif (
isinstance(expected, Collection)
# Type ignored because the error is wrong -- not unreachable.
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
):
msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
raise TypeError(msg)
cls = ApproxSequencelike
else:
cls = ApproxScalar

View File

@@ -7,7 +7,6 @@ from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import Type
@@ -255,7 +254,7 @@ class TestReport(BaseReport):
self,
nodeid: str,
location: Tuple[str, Optional[int], str],
keywords: Mapping[str, Any],
keywords,
outcome: "Literal['passed', 'failed', 'skipped']",
longrepr: Union[
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr

View File

@@ -2,6 +2,7 @@
import bdb
import os
import sys
import warnings
from typing import Callable
from typing import cast
from typing import Dict
@@ -27,6 +28,7 @@ from _pytest._code.code import TerminalRepr
from _pytest.compat import final
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import UNITTEST_SKIP_DURING_COLLECTION
from _pytest.nodes import Collector
from _pytest.nodes import Item
from _pytest.nodes import Node
@@ -377,6 +379,11 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
# Type ignored because unittest is loaded dynamically.
skip_exceptions.append(unittest.SkipTest) # type: ignore
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
if unittest is not None and isinstance(
call.excinfo.value, unittest.SkipTest # type: ignore[attr-defined]
):
warnings.warn(UNITTEST_SKIP_DURING_COLLECTION, stacklevel=2)
outcome = "skipped"
r_ = collector._repr_failure_py(call.excinfo, "line")
assert isinstance(r_, ExceptionChainRepr), repr(r_)

View File

@@ -542,21 +542,15 @@ class TerminalReporter:
if not running_xdist:
self.write_ensure_prefix(line, word, **markup)
if rep.skipped or hasattr(report, "wasxfail"):
available_width = (
(self._tw.fullwidth - self._tw.width_of_current_line)
- len(" [100%]")
- 1
)
reason = _get_raw_skip_reason(rep)
if self.config.option.verbose < 2:
available_width = (
(self._tw.fullwidth - self._tw.width_of_current_line)
- len(" [100%]")
- 1
)
formatted_reason = _format_trimmed(
" ({})", reason, available_width
)
else:
formatted_reason = f" ({reason})"
if reason and formatted_reason is not None:
self._tw.write(formatted_reason)
reason_ = _format_trimmed(" ({})", reason, available_width)
if reason and reason_ is not None:
self._tw.write(reason_)
if self._show_progress_info:
self._write_progress_information_filling_space()
else:
@@ -663,7 +657,7 @@ class TerminalReporter:
errors = len(self.stats.get("error", []))
skipped = len(self.stats.get("skipped", []))
deselected = len(self.stats.get("deselected", []))
selected = self._numcollected - deselected
selected = self._numcollected - errors - skipped - deselected
line = "collected " if final else "collecting "
line += (
str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
@@ -674,7 +668,7 @@ class TerminalReporter:
line += " / %d deselected" % deselected
if skipped:
line += " / %d skipped" % skipped
if self._numcollected > selected:
if self._numcollected > selected > 0:
line += " / %d selected" % selected
if self.isatty:
self.rewrite(line, bold=True, erase=True)

View File

@@ -27,7 +27,7 @@ from _pytest.outcomes import skip
from _pytest.outcomes import xfail
from _pytest.python import Class
from _pytest.python import Function
from _pytest.python import Module
from _pytest.python import PyCollector
from _pytest.runner import CallInfo
from _pytest.scope import Scope
@@ -42,7 +42,7 @@ if TYPE_CHECKING:
def pytest_pycollect_makeitem(
collector: Union[Module, Class], name: str, obj: object
collector: PyCollector, name: str, obj: object
) -> Optional["UnitTestCase"]:
# Has unittest been imported and is obj a subclass of its TestCase?
try:

View File

@@ -48,6 +48,13 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
__module__ = "pytest"
@final
class PytestRemovedIn7Warning(PytestDeprecationWarning):
"""Warning class for features that will be removed in pytest 7."""
__module__ = "pytest"
@final
class PytestRemovedIn8Warning(PytestDeprecationWarning):
"""Warning class for features that will be removed in pytest 8."""

View File

@@ -49,6 +49,8 @@ def catch_warnings_for_item(
warnings.filterwarnings("always", category=DeprecationWarning)
warnings.filterwarnings("always", category=PendingDeprecationWarning)
warnings.filterwarnings("error", category=pytest.PytestRemovedIn7Warning)
apply_warning_filters(config_filters, cmdline_filters)
# apply filters from "filterwarnings" marks
@@ -61,6 +63,14 @@ def catch_warnings_for_item(
yield
for warning_message in log:
ihook.pytest_warning_captured.call_historic(
kwargs=dict(
warning_message=warning_message,
when=when,
item=item,
location=None,
)
)
ihook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=warning_message,
@@ -81,23 +91,6 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
warning_message.lineno,
warning_message.line,
)
if warning_message.source is not None:
try:
import tracemalloc
except ImportError:
pass
else:
tb = tracemalloc.get_object_traceback(warning_message.source)
if tb is not None:
formatted_tb = "\n".join(tb.format())
# Use a leading new line to better separate the (large) output
# from the traceback to the previous warning text.
msg += f"\nObject allocated at:\n{formatted_tb}"
else:
# No need for a leading new line.
url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
msg += "Enable tracemalloc to get traceback where the object was allocated.\n"
msg += f"See {url} for more info."
return msg

View File

@@ -1,5 +1,6 @@
# PYTHON_ARGCOMPLETE_OK
"""pytest: unit and functional testing with Python."""
from . import collect
from _pytest import __version__
from _pytest import version_tuple
from _pytest._code import ExceptionInfo
@@ -18,6 +19,7 @@ from _pytest.config import UsageError
from _pytest.config.argparsing import OptionGroup
from _pytest.config.argparsing import Parser
from _pytest.debugging import pytestPDB as __pytestPDB
from _pytest.fixtures import _fillfuncargs
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureLookupError
from _pytest.fixtures import FixtureRequest
@@ -68,6 +70,7 @@ from _pytest.warning_types import PytestCollectionWarning
from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestExperimentalApiWarning
from _pytest.warning_types import PytestRemovedIn7Warning
from _pytest.warning_types import PytestRemovedIn8Warning
from _pytest.warning_types import PytestUnhandledCoroutineWarning
from _pytest.warning_types import PytestUnhandledThreadExceptionWarning
@@ -80,12 +83,14 @@ set_trace = __pytestPDB.set_trace
__all__ = [
"__version__",
"_fillfuncargs",
"approx",
"Cache",
"CallInfo",
"CaptureFixture",
"Class",
"cmdline",
"collect",
"Collector",
"CollectReport",
"Config",
@@ -126,6 +131,7 @@ __all__ = [
"PytestConfigWarning",
"PytestDeprecationWarning",
"PytestExperimentalApiWarning",
"PytestRemovedIn7Warning",
"PytestRemovedIn8Warning",
"Pytester",
"PytestPluginManager",

38
src/pytest/collect.py Normal file
View File

@@ -0,0 +1,38 @@
import sys
import warnings
from types import ModuleType
from typing import Any
from typing import List
import pytest
from _pytest.deprecated import PYTEST_COLLECT_MODULE
COLLECT_FAKEMODULE_ATTRIBUTES = [
"Collector",
"Module",
"Function",
"Session",
"Item",
"Class",
"File",
"_fillfuncargs",
]
class FakeCollectModule(ModuleType):
def __init__(self) -> None:
super().__init__("pytest.collect")
self.__all__ = list(COLLECT_FAKEMODULE_ATTRIBUTES)
self.__pytest = pytest
def __dir__(self) -> List[str]:
return dir(super()) + self.__all__
def __getattr__(self, name: str) -> Any:
if name not in self.__all__:
raise AttributeError(name)
warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2)
return getattr(pytest, name)
sys.modules["pytest.collect"] = FakeCollectModule()

View File

@@ -1238,6 +1238,8 @@ def test_pdb_can_be_rewritten(pytester: Pytester) -> None:
" def check():",
"> assert 1 == 2",
"E assert 1 == 2",
"E +1",
"E -2",
"",
"pdb.py:2: AssertionError",
"*= 1 failed in *",

View File

@@ -1,13 +1,16 @@
# flake8: noqa
# disable flake check on this file because some constructs are strange
# or redundant on purpose and can't be disable on a line-by-line basis
import ast
import inspect
import linecache
import sys
import textwrap
from pathlib import Path
from types import CodeType
from typing import Any
from typing import Dict
from typing import Optional
import pytest
from _pytest._code import Code
@@ -329,7 +332,8 @@ def test_findsource(monkeypatch) -> None:
lines = ["if 1:\n", " def x():\n", " pass\n"]
co = compile("".join(lines), filename, "exec")
monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename))
# Type ignored because linecache.cache is private.
monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename)) # type: ignore[attr-defined]
src, lineno = findsource(co)
assert src is not None

View File

@@ -2,6 +2,7 @@ import re
import sys
import warnings
from pathlib import Path
from unittest import mock
import pytest
from _pytest import deprecated
@@ -10,6 +11,13 @@ from _pytest.pytester import Pytester
from pytest import PytestDeprecationWarning
@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore
# false positive due to dynamic attribute
def test_pytest_collect_module_deprecated(attribute) -> None:
with pytest.warns(DeprecationWarning, match=attribute):
getattr(pytest.collect, attribute)
@pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS))
@pytest.mark.filterwarnings("default")
def test_external_plugins_integrated(pytester: Pytester, plugin) -> None:
@@ -20,6 +28,54 @@ def test_external_plugins_integrated(pytester: Pytester, plugin) -> None:
pytester.parseconfig("-p", plugin)
def test_fillfuncargs_is_deprecated() -> None:
with pytest.warns(
pytest.PytestDeprecationWarning,
match=re.escape(
"pytest._fillfuncargs() is deprecated, use "
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
),
):
pytest._fillfuncargs(mock.Mock())
def test_fillfixtures_is_deprecated() -> None:
import _pytest.fixtures
with pytest.warns(
pytest.PytestDeprecationWarning,
match=re.escape(
"_pytest.fixtures.fillfixtures() is deprecated, use "
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
),
):
_pytest.fixtures.fillfixtures(mock.Mock())
def test_minus_k_dash_is_deprecated(pytester: Pytester) -> None:
threepass = pytester.makepyfile(
test_threepass="""
def test_one(): assert 1
def test_two(): assert 1
def test_three(): assert 1
"""
)
result = pytester.runpytest("-k=-test_two", threepass)
result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"])
def test_minus_k_colon_is_deprecated(pytester: Pytester) -> None:
threepass = pytester.makepyfile(
test_threepass="""
def test_one(): assert 1
def test_two(): assert 1
def test_three(): assert 1
"""
)
result = pytester.runpytest("-k", "test_two:", threepass)
result.stdout.fnmatch_lines(["*The `-k 'expr:'` syntax*deprecated*"])
def test_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None:
module = pytester.getmodulecol(
"""
@@ -86,6 +142,23 @@ def test_private_is_deprecated() -> None:
PrivateInit(10, _ispytest=True)
def test_raising_unittest_skiptest_during_collection_is_deprecated(
pytester: Pytester,
) -> None:
pytester.makepyfile(
"""
import unittest
raise unittest.SkipTest()
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(
[
"*PytestRemovedIn8Warning: Raising unittest.SkipTest*",
]
)
@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request):
path = legacy_path(tmp_path)
@@ -219,6 +292,10 @@ def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
)
@pytest.mark.skipif(
sys.version_info < (3, 7),
reason="This deprecation can only be emitted on python>=3.7",
)
def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
with pytest.warns(
pytest.PytestDeprecationWarning,

View File

@@ -1,17 +0,0 @@
from dataclasses import dataclass
from dataclasses import field
def test_dataclasses() -> None:
@dataclass
class SimpleDataObject:
field_a: int = field()
field_b: str = field()
def __eq__(self, __o: object) -> bool:
return super().__eq__(__o)
left = SimpleDataObject(1, "b")
right = SimpleDataObject(1, "c")
assert left == right

View File

@@ -56,7 +56,7 @@ def test_terminalwriter_not_unicode() -> None:
file = io.TextIOWrapper(buffer, encoding="cp1252")
tw = terminalwriter.TerminalWriter(file)
tw.write("hello 🌀 wôrld אבג", flush=True)
assert buffer.getvalue() == rb"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2"
assert buffer.getvalue() == br"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2"
win32 = int(sys.platform == "win32")

View File

@@ -1,6 +1,5 @@
[pytest]
addopts = --strict-markers
asyncio_mode = strict
filterwarnings =
error::pytest.PytestWarning
ignore:.*.fspath is deprecated and will be replaced by .*.path.*:pytest.PytestDeprecationWarning

View File

@@ -1,15 +1,15 @@
anyio[curio,trio]==3.5.0
django==4.0.3
pytest-asyncio==0.18.2
anyio[curio,trio]==3.4.0
django==3.2.9
pytest-asyncio==0.16.0
pytest-bdd==5.0.0
pytest-cov==3.0.0
pytest-django==4.5.2
pytest-django==4.5.1
pytest-flakes==4.0.5
pytest-html==3.1.1
pytest-mock==3.7.0
pytest-mock==3.6.1
pytest-rerunfailures==10.2
pytest-sugar==0.9.4
pytest-trio==0.7.0
pytest-twisted==1.13.4
twisted==22.1.0
twisted==21.7.0
pytest-xvfb==2.0.0

View File

@@ -1,4 +1,5 @@
import operator
import sys
from contextlib import contextmanager
from decimal import Decimal
from fractions import Fraction
@@ -771,7 +772,7 @@ class TestApprox:
def test_expected_value_type_error(self, x, name):
with pytest.raises(
TypeError,
match=rf"pytest.approx\(\) does not support nested {name}:",
match=fr"pytest.approx\(\) does not support nested {name}:",
):
approx(x)
@@ -809,6 +810,7 @@ class TestApprox:
assert 1.0 != approx([None])
assert None != approx([1.0]) # noqa: E711
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires ordered dicts")
def test_nonnumeric_dict_repr(self):
"""Dicts with non-numerics and infinites have no tolerances"""
x1 = {"foo": 1.0000005, "bar": None, "foobar": inf}
@@ -858,21 +860,13 @@ class TestApprox:
assert approx(expected, rel=5e-7, abs=0) == actual
assert approx(expected, rel=5e-8, abs=0) != actual
def test_generic_ordered_sequence(self):
class MySequence:
def __getitem__(self, i):
return [1, 2, 3, 4][i]
def test_generic_sized_iterable_object(self):
class MySizedIterable:
def __iter__(self):
return iter([1, 2, 3, 4])
def __len__(self):
return 4
expected = MySequence()
assert [1, 2, 3, 4] == approx(expected, abs=1e-4)
expected_repr = "approx([1 ± 1.0e-06, 2 ± 2.0e-06, 3 ± 3.0e-06, 4 ± 4.0e-06])"
assert repr(approx(expected)) == expected_repr
def test_allow_ordered_sequences_only(self) -> None:
"""pytest.approx() should raise an error on unordered sequences (#9692)."""
with pytest.raises(TypeError, match="only supports ordered sequences"):
assert {1, 2, 3} == approx({1, 2, 3})
expected = MySizedIterable()
assert [1, 2, 3, 4] == approx(expected)

View File

@@ -103,6 +103,10 @@ def test_getfuncargnames_staticmethod_partial():
@pytest.mark.pytester_example_path("fixtures/fill_fixtures")
class TestFillFixtures:
def test_fillfuncargs_exposed(self):
# used by oejskit, kept for compatibility
assert pytest._fillfuncargs == fixtures._fillfuncargs
def test_funcarg_lookupfails(self, pytester: Pytester) -> None:
pytester.copy_example()
result = pytester.runpytest() # "--collect-only")

View File

@@ -1,10 +1,84 @@
from typing import Any
import pytest
from _pytest import runner
from _pytest._code import getfslineno
from _pytest.fixtures import getfixturemarker
from _pytest.pytester import Pytester
from _pytest.python import Function
class TestOEJSKITSpecials:
def test_funcarg_non_pycollectobj(
self, pytester: Pytester, recwarn
) -> None: # rough jstests usage
pytester.makeconftest(
"""
import pytest
def pytest_pycollect_makeitem(collector, name, obj):
if name == "MyClass":
return MyCollector.from_parent(collector, name=name)
class MyCollector(pytest.Collector):
def reportinfo(self):
return self.path, 3, "xyz"
"""
)
modcol = pytester.getmodulecol(
"""
import pytest
@pytest.fixture
def arg1(request):
return 42
class MyClass(object):
pass
"""
)
# this hook finds funcarg factories
rep = runner.collect_one_node(collector=modcol)
# TODO: Don't treat as Any.
clscol: Any = rep.result[0]
clscol.obj = lambda arg1: None
clscol.funcargs = {}
pytest._fillfuncargs(clscol)
assert clscol.funcargs["arg1"] == 42
def test_autouse_fixture(
self, pytester: Pytester, recwarn
) -> None: # rough jstests usage
pytester.makeconftest(
"""
import pytest
def pytest_pycollect_makeitem(collector, name, obj):
if name == "MyClass":
return MyCollector.from_parent(collector, name=name)
class MyCollector(pytest.Collector):
def reportinfo(self):
return self.path, 3, "xyz"
"""
)
modcol = pytester.getmodulecol(
"""
import pytest
@pytest.fixture(autouse=True)
def hello():
pass
@pytest.fixture
def arg1(request):
return 42
class MyClass(object):
pass
"""
)
# this hook finds funcarg factories
rep = runner.collect_one_node(modcol)
# TODO: Don't treat as Any.
clscol: Any = rep.result[0]
clscol.obj = lambda: None
clscol.funcargs = {}
pytest._fillfuncargs(clscol)
assert not clscol.funcargs
def test_wrapped_getfslineno() -> None:
def func():
pass

View File

@@ -24,7 +24,8 @@ from _pytest.compat import getfuncargnames
from _pytest.compat import NOTSET
from _pytest.outcomes import fail
from _pytest.pytester import Pytester
from _pytest.python import IdMaker
from _pytest.python import _idval
from _pytest.python import idmaker
from _pytest.scope import Scope
@@ -106,8 +107,8 @@ class TestMetafunc:
with pytest.raises(
fail.Exception,
match=(
r"In func: ids contains unsupported value Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2. "
r"Supported types are: .*"
r"In func: ids must be list of string/float/int/bool, found:"
r" Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2"
),
):
metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type]
@@ -285,7 +286,7 @@ class TestMetafunc:
deadline=400.0
) # very close to std deadline and CI boxes are not reliable in CPU power
def test_idval_hypothesis(self, value) -> None:
escaped = IdMaker([], [], None, None, None, None, None)._idval(value, "a", 6)
escaped = _idval(value, "a", 6, None, nodeid=None, config=None)
assert isinstance(escaped, str)
escaped.encode("ascii")
@@ -307,10 +308,7 @@ class TestMetafunc:
),
]
for val, expected in values:
assert (
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
)
assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
def test_unicode_idval_with_config(self) -> None:
"""Unit test for expected behavior to obtain ids with
@@ -338,7 +336,7 @@ class TestMetafunc:
("ação", MockConfig({option: False}), "a\\xe7\\xe3o"),
]
for val, config, expected in values:
actual = IdMaker([], [], None, None, config, None, None)._idval(val, "a", 6)
actual = _idval(val, "a", 6, None, nodeid=None, config=config)
assert actual == expected
def test_bytes_idval(self) -> None:
@@ -351,10 +349,7 @@ class TestMetafunc:
("αρά".encode(), r"\xce\xb1\xcf\x81\xce\xac"),
]
for val, expected in values:
assert (
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
)
assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected
def test_class_or_function_idval(self) -> None:
"""Unit test for the expected behavior to obtain ids for parametrized
@@ -368,10 +363,7 @@ class TestMetafunc:
values = [(TestClass, "TestClass"), (test_function, "test_function")]
for val, expected in values:
assert (
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
)
assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
def test_notset_idval(self) -> None:
"""Test that a NOTSET value (used by an empty parameterset) generates
@@ -379,47 +371,29 @@ class TestMetafunc:
Regression test for #7686.
"""
assert (
IdMaker([], [], None, None, None, None, None)._idval(NOTSET, "a", 0) == "a0"
)
assert _idval(NOTSET, "a", 0, None, nodeid=None, config=None) == "a0"
def test_idmaker_autoname(self) -> None:
"""#250"""
result = IdMaker(
("a", "b"),
[pytest.param("string", 1.0), pytest.param("st-ring", 2.0)],
None,
None,
None,
None,
None,
).make_unique_parameterset_ids()
result = idmaker(
("a", "b"), [pytest.param("string", 1.0), pytest.param("st-ring", 2.0)]
)
assert result == ["string-1.0", "st-ring-2.0"]
result = IdMaker(
("a", "b"),
[pytest.param(object(), 1.0), pytest.param(object(), object())],
None,
None,
None,
None,
None,
).make_unique_parameterset_ids()
result = idmaker(
("a", "b"), [pytest.param(object(), 1.0), pytest.param(object(), object())]
)
assert result == ["a0-1.0", "a1-b1"]
# unicode mixing, issue250
result = IdMaker(
("a", "b"), [pytest.param({}, b"\xc3\xb4")], None, None, None, None, None
).make_unique_parameterset_ids()
result = idmaker(("a", "b"), [pytest.param({}, b"\xc3\xb4")])
assert result == ["a0-\\xc3\\xb4"]
def test_idmaker_with_bytes_regex(self) -> None:
result = IdMaker(
("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None, None
).make_unique_parameterset_ids()
result = idmaker(("a"), [pytest.param(re.compile(b"foo"), 1.0)])
assert result == ["foo"]
def test_idmaker_native_strings(self) -> None:
result = IdMaker(
result = idmaker(
("a", "b"),
[
pytest.param(1.0, -1.1),
@@ -436,12 +410,7 @@ class TestMetafunc:
pytest.param(b"\xc3\xb4", "other"),
pytest.param(1.0j, -2.0j),
],
None,
None,
None,
None,
None,
).make_unique_parameterset_ids()
)
assert result == [
"1.0--1.1",
"2--202",
@@ -459,7 +428,7 @@ class TestMetafunc:
]
def test_idmaker_non_printable_characters(self) -> None:
result = IdMaker(
result = idmaker(
("s", "n"),
[
pytest.param("\x00", 1),
@@ -469,35 +438,23 @@ class TestMetafunc:
pytest.param("\t", 5),
pytest.param(b"\t", 6),
],
None,
None,
None,
None,
None,
).make_unique_parameterset_ids()
)
assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"]
def test_idmaker_manual_ids_must_be_printable(self) -> None:
result = IdMaker(
result = idmaker(
("s",),
[
pytest.param("x00", id="hello \x00"),
pytest.param("x05", id="hello \x05"),
],
None,
None,
None,
None,
None,
).make_unique_parameterset_ids()
)
assert result == ["hello \\x00", "hello \\x05"]
def test_idmaker_enum(self) -> None:
enum = pytest.importorskip("enum")
e = enum.Enum("Foo", "one, two")
result = IdMaker(
("a", "b"), [pytest.param(e.one, e.two)], None, None, None, None, None
).make_unique_parameterset_ids()
result = idmaker(("a", "b"), [pytest.param(e.one, e.two)])
assert result == ["Foo.one-Foo.two"]
def test_idmaker_idfn(self) -> None:
@@ -508,19 +465,15 @@ class TestMetafunc:
return repr(val)
return None
result = IdMaker(
result = idmaker(
("a", "b"),
[
pytest.param(10.0, IndexError()),
pytest.param(20, KeyError()),
pytest.param("three", [1, 2, 3]),
],
ids,
None,
None,
None,
None,
).make_unique_parameterset_ids()
idfn=ids,
)
assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"]
def test_idmaker_idfn_unique_names(self) -> None:
@@ -529,19 +482,15 @@ class TestMetafunc:
def ids(val: object) -> str:
return "a"
result = IdMaker(
result = idmaker(
("a", "b"),
[
pytest.param(10.0, IndexError()),
pytest.param(20, KeyError()),
pytest.param("three", [1, 2, 3]),
],
ids,
None,
None,
None,
None,
).make_unique_parameterset_ids()
idfn=ids,
)
assert result == ["a-a0", "a-a1", "a-a2"]
def test_idmaker_with_idfn_and_config(self) -> None:
@@ -571,15 +520,12 @@ class TestMetafunc:
(MockConfig({option: False}), "a\\xe7\\xe3o"),
]
for config, expected in values:
result = IdMaker(
result = idmaker(
("a",),
[pytest.param("string")],
lambda _: "ação",
None,
config,
None,
None,
).make_unique_parameterset_ids()
idfn=lambda _: "ação",
config=config,
)
assert result == [expected]
def test_idmaker_with_ids_and_config(self) -> None:
@@ -609,9 +555,12 @@ class TestMetafunc:
(MockConfig({option: False}), "a\\xe7\\xe3o"),
]
for config, expected in values:
result = IdMaker(
("a",), [pytest.param("string")], None, ["ação"], config, None, None
).make_unique_parameterset_ids()
result = idmaker(
("a",),
[pytest.param("string")],
ids=["ação"],
config=config,
)
assert result == [expected]
def test_parametrize_ids_exception(self, pytester: Pytester) -> None:
@@ -668,39 +617,23 @@ class TestMetafunc:
)
def test_idmaker_with_ids(self) -> None:
result = IdMaker(
("a", "b"),
[pytest.param(1, 2), pytest.param(3, 4)],
None,
["a", None],
None,
None,
None,
).make_unique_parameterset_ids()
result = idmaker(
("a", "b"), [pytest.param(1, 2), pytest.param(3, 4)], ids=["a", None]
)
assert result == ["a", "3-4"]
def test_idmaker_with_paramset_id(self) -> None:
result = IdMaker(
result = idmaker(
("a", "b"),
[pytest.param(1, 2, id="me"), pytest.param(3, 4, id="you")],
None,
["a", None],
None,
None,
None,
).make_unique_parameterset_ids()
ids=["a", None],
)
assert result == ["me", "you"]
def test_idmaker_with_ids_unique_names(self) -> None:
result = IdMaker(
("a"),
list(map(pytest.param, [1, 2, 3, 4, 5])),
None,
["a", "a", "b", "c", "b"],
None,
None,
None,
).make_unique_parameterset_ids()
result = idmaker(
("a"), map(pytest.param, [1, 2, 3, 4, 5]), ids=["a", "a", "b", "c", "b"]
)
assert result == ["a0", "a1", "b0", "c", "b1"]
def test_parametrize_indirect(self) -> None:
@@ -1339,7 +1272,7 @@ class TestMetafuncFunctional:
"""
import pytest
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, OSError()))
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type))
def test_ids_numbers(x,expected):
assert x * 2 == expected
"""
@@ -1347,8 +1280,8 @@ class TestMetafuncFunctional:
result = pytester.runpytest()
result.stdout.fnmatch_lines(
[
"In test_ids_numbers: ids contains unsupported value OSError() (type: <class 'OSError'>) at index 2. "
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
"In test_ids_numbers: ids must be list of string/float/int/bool,"
" found: <class 'type'> (type: <class 'type'>) at index 2"
]
)

View File

@@ -83,7 +83,7 @@ class TestImportHookInstallation:
"E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}",
"E Omitting 1 identical items, use -vv to show",
"E Differing items:",
"E Use -v to get more diff",
"E Use -v to get the full diff",
]
)
# XXX: unstable output.
@@ -376,7 +376,7 @@ class TestAssert_reprcompare:
assert diff == [
"b'spam' == b'eggs'",
"At index 0 diff: b's' != b'e'",
"Use -v to get more diff",
"Use -v to get the full diff",
]
def test_bytes_diff_verbose(self) -> None:
@@ -444,19 +444,11 @@ class TestAssert_reprcompare:
"""
expl = callequal(left, right, verbose=0)
assert expl is not None
assert expl[-1] == "Use -v to get more diff"
assert expl[-1] == "Use -v to get the full diff"
verbose_expl = callequal(left, right, verbose=1)
assert verbose_expl is not None
assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip())
def test_iterable_quiet(self) -> None:
expl = callequal([1, 2], [10, 2], verbose=-1)
assert expl == [
"[1, 2] == [10, 2]",
"At index 0 diff: 1 != 10",
"Use -v to get more diff",
]
def test_iterable_full_diff_ci(
self, monkeypatch: MonkeyPatch, pytester: Pytester
) -> None:
@@ -474,7 +466,7 @@ class TestAssert_reprcompare:
monkeypatch.delenv("CI", raising=False)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["E Use -v to get more diff"])
result.stdout.fnmatch_lines(["E Use -v to get the full diff"])
def test_list_different_lengths(self) -> None:
expl = callequal([0, 1], [0, 1, 2])
@@ -707,6 +699,32 @@ class TestAssert_reprcompare:
assert expl is not None
assert len(expl) > 1
def test_repr_verbose(self) -> None:
class Nums:
def __init__(self, nums):
self.nums = nums
def __repr__(self):
return str(self.nums)
list_x = list(range(5000))
list_y = list(range(5000))
list_y[len(list_y) // 2] = 3
nums_x = Nums(list_x)
nums_y = Nums(list_y)
assert callequal(nums_x, nums_y) is None
expl = callequal(nums_x, nums_y, verbose=1)
assert expl is not None
assert "+" + repr(nums_x) in expl
assert "-" + repr(nums_y) in expl
expl = callequal(nums_x, nums_y, verbose=2)
assert expl is not None
assert "+" + repr(nums_x) in expl
assert "-" + repr(nums_y) in expl
def test_list_bad_repr(self) -> None:
class A:
def __repr__(self):
@@ -778,6 +796,7 @@ class TestAssert_reprcompare:
class TestAssert_reprcompare_dataclass:
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
def test_dataclasses(self, pytester: Pytester) -> None:
p = pytester.copy_example("dataclasses/test_compare_dataclasses.py")
result = pytester.runpytest(p)
@@ -796,6 +815,7 @@ class TestAssert_reprcompare_dataclass:
consecutive=True,
)
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
def test_recursive_dataclasses(self, pytester: Pytester) -> None:
p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py")
result = pytester.runpytest(p)
@@ -814,6 +834,7 @@ class TestAssert_reprcompare_dataclass:
consecutive=True,
)
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None:
p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py")
result = pytester.runpytest(p, "-vv")
@@ -833,6 +854,8 @@ class TestAssert_reprcompare_dataclass:
"E ",
"E Drill down into differing attribute a:",
"E a: 10 != 20",
"E +10",
"E -20",
"E ",
"E Drill down into differing attribute b:",
"E b: 'ten' != 'xxx'",
@@ -844,6 +867,7 @@ class TestAssert_reprcompare_dataclass:
consecutive=True,
)
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
def test_dataclasses_verbose(self, pytester: Pytester) -> None:
p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py")
result = pytester.runpytest(p, "-vv")
@@ -857,6 +881,7 @@ class TestAssert_reprcompare_dataclass:
]
)
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
def test_dataclasses_with_attribute_comparison_off(
self, pytester: Pytester
) -> None:
@@ -866,6 +891,7 @@ class TestAssert_reprcompare_dataclass:
result = pytester.runpytest(p, "-vv")
result.assert_outcomes(failed=0, passed=1)
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
def test_comparing_two_different_data_classes(self, pytester: Pytester) -> None:
p = pytester.copy_example(
"dataclasses/test_compare_two_different_dataclasses.py"
@@ -873,15 +899,6 @@ class TestAssert_reprcompare_dataclass:
result = pytester.runpytest(p, "-vv")
result.assert_outcomes(failed=0, passed=1)
def test_data_classes_with_custom_eq(self, pytester: Pytester) -> None:
p = pytester.copy_example(
"dataclasses/test_compare_dataclasses_with_custom_eq.py"
)
# issue 9362
result = pytester.runpytest(p, "-vv")
result.assert_outcomes(failed=1, passed=0)
result.stdout.no_re_match_line(".*Differing attributes.*")
class TestAssert_reprcompare_attrsclass:
def test_attrs(self) -> None:
@@ -965,6 +982,7 @@ class TestAssert_reprcompare_attrsclass:
right = SimpleDataObject(1, "b")
lines = callequal(left, right, verbose=2)
print(lines)
assert lines is not None
assert lines[2].startswith("Matching attributes:")
assert "Omitting" not in lines[1]
@@ -989,36 +1007,6 @@ class TestAssert_reprcompare_attrsclass:
lines = callequal(left, right)
assert lines is None
def test_attrs_with_auto_detect_and_custom_eq(self) -> None:
@attr.s(
auto_detect=True
) # attr.s doesnt ignore a custom eq if auto_detect=True
class SimpleDataObject:
field_a = attr.ib()
def __eq__(self, other): # pragma: no cover
return super().__eq__(other)
left = SimpleDataObject(1)
right = SimpleDataObject(2)
# issue 9362
lines = callequal(left, right, verbose=2)
assert lines is None
def test_attrs_with_custom_eq(self) -> None:
@attr.define(slots=False)
class SimpleDataObject:
field_a = attr.ib()
def __eq__(self, other): # pragma: no cover
return super().__eq__(other)
left = SimpleDataObject(1)
right = SimpleDataObject(2)
# issue 9362
lines = callequal(left, right, verbose=2)
assert lines is None
class TestAssert_reprcompare_namedtuple:
def test_namedtuple(self) -> None:
@@ -1039,7 +1027,7 @@ class TestAssert_reprcompare_namedtuple:
" b: 'b' != 'c'",
" - c",
" + b",
"Use -v to get more diff",
"Use -v to get the full diff",
]
def test_comparing_two_different_namedtuple(self) -> None:
@@ -1054,7 +1042,7 @@ class TestAssert_reprcompare_namedtuple:
assert lines == [
"NT1(a=1, b='b') == NT2(a=2, b='b')",
"At index 0 diff: 1 != 2",
"Use -v to get more diff",
"Use -v to get the full diff",
]

View File

@@ -13,12 +13,10 @@ from functools import partial
from pathlib import Path
from typing import cast
from typing import Dict
from typing import Generator
from typing import List
from typing import Mapping
from typing import Optional
from typing import Set
from unittest import mock
import _pytest._code
import pytest
@@ -1059,7 +1057,7 @@ class TestAssertionRewriteHookDetails:
e = OSError()
e.errno = 10
raise e
yield
yield # type:ignore[unreachable]
monkeypatch.setattr(
_pytest.assertion.rewrite, "atomic_write", atomic_write_failed
@@ -1147,6 +1145,9 @@ class TestAssertionRewriteHookDetails:
_write_pyc(state, co, source_stat, pyc)
assert _read_pyc(fn, pyc, state.trace) is not None
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="Only the Python 3.7 format for simplicity"
)
def test_read_pyc_more_invalid(self, tmp_path: Path) -> None:
from _pytest.assertion.rewrite import _read_pyc
@@ -1315,7 +1316,7 @@ class TestIssue2121:
@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"
)
@pytest.mark.parametrize("offset", [-1, +1])
def test_source_mtime_long_long(pytester: Pytester, offset) -> None:
@@ -1334,7 +1335,7 @@ def test_source_mtime_long_long(pytester: Pytester, offset) -> None:
# use unsigned long timestamp which overflows signed long,
# which was the cause of the bug
# +1 offset also tests masking of 0xFFFFFFFF
timestamp = 2**32 + offset
timestamp = 2 ** 32 + offset
os.utime(str(p), (timestamp, timestamp))
result = pytester.runpytest()
assert result.ret == 0
@@ -1378,7 +1379,7 @@ class TestEarlyRewriteBailout:
@pytest.fixture
def hook(
self, pytestconfig, monkeypatch, pytester: Pytester
) -> Generator[AssertionRewritingHook, None, None]:
) -> AssertionRewritingHook:
"""Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track
if PathFinder.find_spec has been called.
"""
@@ -1399,11 +1400,11 @@ class TestEarlyRewriteBailout:
hook = AssertionRewritingHook(pytestconfig)
# use default patterns, otherwise we inherit pytest's testing config
with mock.patch.object(hook, "fnpats", ["test_*.py", "*_test.py"]):
monkeypatch.setattr(hook, "_find_spec", spy_find_spec)
hook.set_session(StubSession()) # type: ignore[arg-type]
pytester.syspathinsert()
yield hook
hook.fnpats[:] = ["test_*.py", "*_test.py"]
monkeypatch.setattr(hook, "_find_spec", spy_find_spec)
hook.set_session(StubSession()) # type: ignore[arg-type]
pytester.syspathinsert()
return hook
def test_basic(self, pytester: Pytester, hook: AssertionRewritingHook) -> None:
"""
@@ -1453,9 +1454,9 @@ class TestEarlyRewriteBailout:
}
)
pytester.syspathinsert("tests")
with mock.patch.object(hook, "fnpats", ["tests/**.py"]):
assert hook.find_spec("file") is not None
assert self.find_spec_calls == ["file"]
hook.fnpats[:] = ["tests/**.py"]
assert hook.find_spec("file") is not None
assert self.find_spec_calls == ["file"]
@pytest.mark.skipif(
sys.platform.startswith("win32"), reason="cannot remove cwd on Windows"

View File

@@ -773,7 +773,7 @@ class TestLastFailed:
result = pytester.runpytest("--lf", "--lfnf", "none")
result.stdout.fnmatch_lines(
[
"collected 2 items / 2 deselected / 0 selected",
"collected 2 items / 2 deselected",
"run-last-failure: no previously failed tests, deselecting all items.",
"deselected=2",
"* 2 deselected in *",

View File

@@ -1433,19 +1433,19 @@ def test_error_attribute_issue555(pytester: Pytester) -> None:
not sys.platform.startswith("win"),
reason="only on windows",
)
def test_windowsconsoleio_workaround_non_standard_streams() -> None:
def test_py36_windowsconsoleio_workaround_non_standard_streams() -> None:
"""
Ensure _windowsconsoleio_workaround function works with objects that
Ensure _py36_windowsconsoleio_workaround function works with objects that
do not implement the full ``io``-based stream protocol, for example execnet channels (#2666).
"""
from _pytest.capture import _windowsconsoleio_workaround
from _pytest.capture import _py36_windowsconsoleio_workaround
class DummyStream:
def write(self, s):
pass
stream = cast(TextIO, DummyStream())
_windowsconsoleio_workaround(stream)
_py36_windowsconsoleio_workaround(stream)
def test_dontreadfrominput_has_encoding(pytester: Pytester) -> None:

View File

@@ -881,36 +881,6 @@ class TestNodeKeywords:
assert item.keywords["kw"] == "method"
assert len(item.keywords) == len(set(item.keywords))
def test_unpacked_marks_added_to_keywords(self, pytester: Pytester) -> None:
item = pytester.getitem(
"""
import pytest
pytestmark = pytest.mark.foo
class TestClass:
pytestmark = pytest.mark.bar
def test_method(self): pass
test_method.pytestmark = pytest.mark.baz
""",
"test_method",
)
assert isinstance(item, pytest.Function)
cls = item.getparent(pytest.Class)
assert cls is not None
mod = item.getparent(pytest.Module)
assert mod is not None
assert item.keywords["foo"] == pytest.mark.foo.mark
assert item.keywords["bar"] == pytest.mark.bar.mark
assert item.keywords["baz"] == pytest.mark.baz.mark
assert cls.keywords["foo"] == pytest.mark.foo.mark
assert cls.keywords["bar"] == pytest.mark.bar.mark
assert "baz" not in cls.keywords
assert mod.keywords["foo"] == pytest.mark.foo.mark
assert "bar" not in mod.keywords
assert "baz" not in mod.keywords
COLLECTION_ERROR_PY_FILES = dict(
test_01_failure="""
@@ -1507,35 +1477,6 @@ class TestImportModeImportlib:
]
)
def test_using_python_path(self, pytester: Pytester) -> None:
"""
Dummy modules created by insert_missing_modules should not get in
the way of modules that could be imported via python path (#9645).
"""
pytester.makeini(
"""
[pytest]
pythonpath = .
addopts = --import-mode importlib
"""
)
pytester.makepyfile(
**{
"tests/__init__.py": "",
"tests/conftest.py": "",
"tests/subpath/__init__.py": "",
"tests/subpath/helper.py": "",
"tests/subpath/test_something.py": """
import tests.subpath.helper
def test_something():
assert True
""",
}
)
result = pytester.runpytest()
result.stdout.fnmatch_lines("*1 passed in*")
def test_does_not_crash_on_error_from_decorated_function(pytester: Pytester) -> None:
"""Regression test for an issue around bad exception formatting due to

View File

@@ -135,7 +135,7 @@ def test_is_generator_async_gen_syntax(pytester: Pytester) -> None:
pytester.makepyfile(
"""
from _pytest.compat import is_generator
def test_is_generator():
def test_is_generator_py36():
async def foo():
yield
await foo()

View File

@@ -163,17 +163,7 @@ class TestParseIni:
pytester.path.joinpath("pytest.ini").write_text("addopts = -x")
result = pytester.runpytest()
assert result.ret != 0
result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined")
def test_toml_parse_error(self, pytester: Pytester) -> None:
pytester.makepyprojecttoml(
"""
\\"
"""
)
result = pytester.runpytest()
assert result.ret != 0
result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*")
result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"])
@pytest.mark.xfail(reason="probably not needed")
def test_confcutdir(self, pytester: Pytester) -> None:

View File

@@ -114,7 +114,6 @@ class TestConftestValueAccessGlobal:
"a", startdir, importmode="prepend", rootpath=Path(basedir)
)
assert value == 1.5
assert mod.__file__ is not None
path = Path(mod.__file__)
assert path.parent == basedir / "adir" / "b"
assert path.stem == "conftest"
@@ -146,9 +145,10 @@ def test_issue151_load_all_conftests(pytester: Pytester) -> None:
p = pytester.mkdir(name)
p.joinpath("conftest.py").touch()
pm = PytestPluginManager()
conftest_setinitial(pm, names)
assert len(set(pm.get_plugins()) - {pm}) == len(names)
conftest = PytestPluginManager()
conftest_setinitial(conftest, names)
d = list(conftest._conftestpath2mod.values())
assert len(d) == len(names)
def test_conftest_global_import(pytester: Pytester) -> None:
@@ -191,20 +191,18 @@ def test_conftestcutdir(pytester: Pytester) -> None:
conf.parent, importmode="prepend", rootpath=pytester.path
)
assert len(values) == 0
assert not conftest.has_plugin(str(conf))
assert Path(conf) not in conftest._conftestpath2mod
# but we can still import a conftest directly
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
values = conftest._getconftestmodules(
conf.parent, importmode="prepend", rootpath=pytester.path
)
assert values[0].__file__ is not None
assert values[0].__file__.startswith(str(conf))
# and all sub paths get updated properly
values = conftest._getconftestmodules(
p, importmode="prepend", rootpath=pytester.path
)
assert len(values) == 1
assert values[0].__file__ is not None
assert values[0].__file__.startswith(str(conf))
@@ -216,7 +214,6 @@ def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None:
conf.parent, importmode="prepend", rootpath=pytester.path
)
assert len(values) == 1
assert values[0].__file__ is not None
assert values[0].__file__.startswith(str(conf))
@@ -225,15 +222,15 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None:
sub = pytester.mkdir(name)
subconftest = sub.joinpath("conftest.py")
subconftest.touch()
pm = PytestPluginManager()
conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path)
conftest = PytestPluginManager()
conftest_setinitial(conftest, [sub.parent], confcutdir=pytester.path)
key = subconftest.resolve()
if name not in ("whatever", ".dotdir"):
assert pm.has_plugin(str(key))
assert len(set(pm.get_plugins()) - {pm}) == 1
assert key in conftest._conftestpath2mod
assert len(conftest._conftestpath2mod) == 1
else:
assert not pm.has_plugin(str(key))
assert len(set(pm.get_plugins()) - {pm}) == 0
assert key not in conftest._conftestpath2mod
assert len(conftest._conftestpath2mod) == 0
def test_conftest_confcutdir(pytester: Pytester) -> None:

View File

@@ -8,6 +8,14 @@ from _pytest.debugging import _validate_usepdb_cls
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
try:
# Type ignored for Python <= 3.6.
breakpoint # type: ignore
except NameError:
SUPPORTS_BREAKPOINT_BUILTIN = False
else:
SUPPORTS_BREAKPOINT_BUILTIN = True
_ENVIRON_PYTHONBREAKPOINT = os.environ.get("PYTHONBREAKPOINT", "")
@@ -903,6 +911,14 @@ class TestPDB:
class TestDebuggingBreakpoints:
def test_supports_breakpoint_module_global(self) -> None:
"""Test that supports breakpoint global marks on Python 3.7+."""
if sys.version_info >= (3, 7):
assert SUPPORTS_BREAKPOINT_BUILTIN is True
@pytest.mark.skipif(
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
)
@pytest.mark.parametrize("arg", ["--pdb", ""])
def test_sys_breakpointhook_configure_and_unconfigure(
self, pytester: Pytester, arg: str
@@ -936,6 +952,9 @@ class TestDebuggingBreakpoints:
result = pytester.runpytest_subprocess(*args)
result.stdout.fnmatch_lines(["*1 passed in *"])
@pytest.mark.skipif(
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
)
def test_pdb_custom_cls(self, pytester: Pytester, custom_debugger_hook) -> None:
p1 = pytester.makepyfile(
"""
@@ -950,6 +969,9 @@ class TestDebuggingBreakpoints:
assert custom_debugger_hook == ["init", "set_trace"]
@pytest.mark.parametrize("arg", ["--pdb", ""])
@pytest.mark.skipif(
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
)
def test_environ_custom_class(
self, pytester: Pytester, custom_debugger_hook, arg: str
) -> None:
@@ -980,6 +1002,9 @@ class TestDebuggingBreakpoints:
result = pytester.runpytest_subprocess(*args)
result.stdout.fnmatch_lines(["*1 passed in *"])
@pytest.mark.skipif(
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
)
@pytest.mark.skipif(
not _ENVIRON_PYTHONBREAKPOINT == "",
reason="Requires breakpoint() default value",
@@ -1000,6 +1025,9 @@ class TestDebuggingBreakpoints:
assert "reading from stdin while output" not in rest
TestPDB.flush(child)
@pytest.mark.skipif(
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
)
def test_pdb_not_altered(self, pytester: Pytester) -> None:
p1 = pytester.makepyfile(
"""

View File

@@ -803,8 +803,8 @@ class TestDoctests:
"""
p = pytester.makepyfile(
setup="""
from setuptools import setup, find_packages
if __name__ == '__main__':
from setuptools import setup, find_packages
setup(name='sample',
version='0.0',
description='description',

View File

@@ -4,6 +4,8 @@ Tests and examples for correct "+/-" usage in error diffs.
See https://github.com/pytest-dev/pytest/issues/3333 for details.
"""
import sys
import pytest
from _pytest.pytester import Pytester
@@ -208,61 +210,68 @@ TESTCASES = [
""",
id='Test "not in" string',
),
pytest.param(
"""
from dataclasses import dataclass
@dataclass
class A:
a: int
b: str
def test_this():
result = A(1, 'spam')
expected = A(2, 'spam')
assert result == expected
""",
"""
> assert result == expected
E AssertionError: assert A(a=1, b='spam') == A(a=2, b='spam')
E Matching attributes:
E ['b']
E Differing attributes:
E ['a']
E Drill down into differing attribute a:
E a: 1 != 2
""",
id="Compare data classes",
),
pytest.param(
"""
import attr
@attr.s(auto_attribs=True)
class A:
a: int
b: str
def test_this():
result = A(1, 'spam')
expected = A(1, 'eggs')
assert result == expected
""",
"""
> assert result == expected
E AssertionError: assert A(a=1, b='spam') == A(a=1, b='eggs')
E Matching attributes:
E ['a']
E Differing attributes:
E ['b']
E Drill down into differing attribute b:
E b: 'spam' != 'eggs'
E - eggs
E + spam
""",
id="Compare attrs classes",
),
]
if sys.version_info[:2] >= (3, 7):
TESTCASES.extend(
[
pytest.param(
"""
from dataclasses import dataclass
@dataclass
class A:
a: int
b: str
def test_this():
result = A(1, 'spam')
expected = A(2, 'spam')
assert result == expected
""",
"""
> assert result == expected
E AssertionError: assert A(a=1, b='spam') == A(a=2, b='spam')
E Matching attributes:
E ['b']
E Differing attributes:
E ['a']
E Drill down into differing attribute a:
E a: 1 != 2
E +1
E -2
""",
id="Compare data classes",
),
pytest.param(
"""
import attr
@attr.s(auto_attribs=True)
class A:
a: int
b: str
def test_this():
result = A(1, 'spam')
expected = A(1, 'eggs')
assert result == expected
""",
"""
> assert result == expected
E AssertionError: assert A(a=1, b='spam') == A(a=1, b='eggs')
E Matching attributes:
E ['a']
E Differing attributes:
E ['b']
E Drill down into differing attribute b:
E b: 'spam' != 'eggs'
E - eggs
E + spam
""",
id="Compare attrs classes",
),
]
)
@pytest.mark.parametrize("code, expected", TESTCASES)

View File

@@ -14,7 +14,7 @@ def test_item_fspath(pytester: pytest.Pytester) -> None:
items2, hookrec = pytester.inline_genitems(item.nodeid)
(item2,) = items2
assert item2.name == item.name
assert item2.fspath == item.fspath
assert item2.fspath == item.fspath # type: ignore[attr-defined]
assert item2.path == item.path

View File

@@ -823,6 +823,25 @@ class TestKeywordSelection:
assert len(dlist) == 1
assert dlist[0].items[0].name == "test_1"
def test_select_starton(self, pytester: Pytester) -> None:
threepass = pytester.makepyfile(
test_threepass="""
def test_one(): assert 1
def test_two(): assert 1
def test_three(): assert 1
"""
)
reprec = pytester.inline_run(
"-Wignore::pytest.PytestRemovedIn7Warning", "-k", "test_two:", threepass
)
passed, skipped, failed = reprec.listoutcomes()
assert len(passed) == 2
assert not failed
dlist = reprec.getcalls("pytest_deselected")
assert len(dlist) == 1
item = dlist[0].items[0]
assert item.name == "test_one"
def test_keyword_extra(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""

View File

@@ -50,24 +50,21 @@ def test_setattr() -> None:
class TestSetattrWithImportPath:
def test_string_expression(self, monkeypatch: MonkeyPatch) -> None:
with monkeypatch.context() as mp:
mp.setattr("os.path.abspath", lambda x: "hello2")
assert os.path.abspath("123") == "hello2"
monkeypatch.setattr("os.path.abspath", lambda x: "hello2")
assert os.path.abspath("123") == "hello2"
def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None:
with monkeypatch.context() as mp:
mp.setattr("_pytest.config.Config", 42)
import _pytest
monkeypatch.setattr("_pytest.config.Config", 42)
import _pytest
assert _pytest.config.Config == 42 # type: ignore
assert _pytest.config.Config == 42 # type: ignore
def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None:
with monkeypatch.context() as mp:
mp.setattr("_pytest.config.Config", 42)
import _pytest
monkeypatch.setattr("_pytest.config.Config", 42)
import _pytest
assert _pytest.config.Config == 42 # type: ignore
mp.delattr("_pytest.config.Config")
assert _pytest.config.Config == 42 # type: ignore
monkeypatch.delattr("_pytest.config.Config")
def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None:
with pytest.raises(TypeError):
@@ -83,16 +80,14 @@ class TestSetattrWithImportPath:
def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None:
# https://github.com/pytest-dev/pytest/issues/746
with monkeypatch.context() as mp:
mp.setattr("os.path.qweqwe", 42, raising=False)
assert os.path.qweqwe == 42 # type: ignore
monkeypatch.setattr("os.path.qweqwe", 42, raising=False)
assert os.path.qweqwe == 42 # type: ignore
def test_delattr(self, monkeypatch: MonkeyPatch) -> None:
with monkeypatch.context() as mp:
mp.delattr("os.path.abspath")
assert not hasattr(os.path, "abspath")
mp.undo()
assert os.path.abspath
monkeypatch.delattr("os.path.abspath")
assert not hasattr(os.path, "abspath")
monkeypatch.undo()
assert os.path.abspath
def test_delattr() -> None:

View File

@@ -345,7 +345,7 @@ def test_SkipTest_during_collection(pytester: Pytester) -> None:
"""
)
result = pytester.runpytest(p)
result.assert_outcomes(skipped=1, warnings=0)
result.assert_outcomes(skipped=1, warnings=1)
def test_SkipTest_in_test(pytester: Pytester) -> None:

View File

@@ -295,7 +295,7 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=True,
text=True,
universal_newlines=True,
).stdout
except (OSError, subprocess.CalledProcessError):
pytest.skip("bash is not available")

View File

@@ -1,4 +1,3 @@
import email.message
import io
from typing import List
from typing import Union
@@ -99,9 +98,7 @@ class TestPaste:
def mocked(url, data):
calls.append((url, data))
raise urllib.error.HTTPError(
url, 400, "Bad request", email.message.Message(), io.BytesIO()
)
raise urllib.error.HTTPError(url, 400, "Bad request", {}, io.BytesIO())
monkeypatch.setattr(urllib.request, "urlopen", mocked)
return calls

View File

@@ -143,10 +143,6 @@ class TestImportPath:
assert obj.x == 42 # type: ignore[attr-defined]
assert obj.__name__ == "execfile"
def test_import_path_missing_file(self, path1: Path) -> None:
with pytest.raises(ImportPathMismatchError):
import_path(path1 / "sampledir", root=path1)
def test_renamed_dir_creates_mismatch(
self, tmp_path: Path, monkeypatch: MonkeyPatch
) -> None:
@@ -454,6 +450,7 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N
class TestImportLibMode:
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
def test_importmode_importlib_with_dataclass(self, tmp_path: Path) -> None:
"""Ensure that importlib mode works with a module containing dataclasses (#7856)."""
fn = tmp_path.joinpath("_src/tests/test_dataclass.py")
@@ -562,20 +559,15 @@ class TestImportLibMode:
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
assert result == "home.foo.test_foo"
def test_insert_missing_modules(
self, monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.chdir(tmp_path)
# Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and
# don't end up being imported.
modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")}
insert_missing_modules(modules, "xxx.tests.foo")
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
def test_insert_missing_modules(self) -> None:
modules = {"src.tests.foo": ModuleType("src.tests.foo")}
insert_missing_modules(modules, "src.tests.foo")
assert sorted(modules) == ["src", "src.tests", "src.tests.foo"]
mod = ModuleType("mod", doc="My Module")
modules = {"xxy": mod}
insert_missing_modules(modules, "xxy")
assert modules == {"xxy": mod}
modules = {"src": mod}
insert_missing_modules(modules, "src")
assert modules == {"src": mod}
modules = {}
insert_missing_modules(modules, "")

View File

@@ -618,9 +618,14 @@ def test_linematcher_string_api() -> None:
def test_pytest_addopts_before_pytester(request, monkeypatch: MonkeyPatch) -> None:
orig = os.environ.get("PYTEST_ADDOPTS", None)
monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused")
_: Pytester = request.getfixturevalue("pytester")
pytester: Pytester = request.getfixturevalue("pytester")
assert "PYTEST_ADDOPTS" not in os.environ
pytester._finalize()
assert os.environ.get("PYTEST_ADDOPTS") == "--orig-unused"
monkeypatch.undo()
assert os.environ.get("PYTEST_ADDOPTS") == orig
def test_run_stdin(pytester: Pytester) -> None:

View File

@@ -114,13 +114,13 @@ class TestDeprecatedCall:
# Type ignored because `onceregistry` and `filters` are not
# documented API.
onceregistry = warnings.onceregistry.copy() # type: ignore
filters = warnings.filters[:]
filters = warnings.filters[:] # type: ignore
warn = warnings.warn
warn_explicit = warnings.warn_explicit
self.test_deprecated_call_raises()
self.test_deprecated_call()
assert onceregistry == warnings.onceregistry # type: ignore
assert filters == warnings.filters
assert filters == warnings.filters # type: ignore
assert warn is warnings.warn
assert warn_explicit is warnings.warn_explicit

View File

@@ -335,54 +335,6 @@ def test_sessionfinish_with_start(pytester: Pytester) -> None:
assert res.ret == ExitCode.NO_TESTS_COLLECTED
def test_collection_args_do_not_duplicate_modules(pytester: Pytester) -> None:
"""Test that when multiple collection args are specified on the command line
for the same module, only a single Module collector is created.
Regression test for #723, #3358.
"""
pytester.makepyfile(
**{
"d/test_it": """
def test_1(): pass
def test_2(): pass
"""
}
)
result = pytester.runpytest(
"--collect-only",
"d/test_it.py::test_1",
"d/test_it.py::test_2",
)
result.stdout.fnmatch_lines(
[
"<Module d/test_it.py>",
" <Function test_1>",
" <Function test_2>",
],
consecutive=True,
)
# Different, but related case.
result = pytester.runpytest(
"--collect-only",
"--keep-duplicates",
"d",
"d",
)
result.stdout.fnmatch_lines(
[
"<Module d/test_it.py>",
" <Function test_1>",
" <Function test_2>",
" <Function test_1>",
" <Function test_2>",
],
consecutive=True,
)
@pytest.mark.parametrize("path", ["root", "{relative}/root", "{environment}/root"])
def test_rootdir_option_arg(
pytester: Pytester, monkeypatch: MonkeyPatch, path: str

View File

@@ -385,55 +385,21 @@ class TestTerminal:
def test_10():
pytest.xfail("It's 🕙 o'clock")
@pytest.mark.skip(
reason="cannot do foobar because baz is missing due to I don't know what"
)
def test_long_skip():
pass
@pytest.mark.xfail(
reason="cannot do foobar because baz is missing due to I don't know what"
)
def test_long_xfail():
print(1 / 0)
"""
)
common_output = [
"test_verbose_skip_reason.py::test_1 SKIPPED (123) *",
"test_verbose_skip_reason.py::test_2 XPASS (456) *",
"test_verbose_skip_reason.py::test_3 XFAIL (789) *",
"test_verbose_skip_reason.py::test_4 XFAIL *",
"test_verbose_skip_reason.py::test_5 SKIPPED (unconditional skip) *",
"test_verbose_skip_reason.py::test_6 XPASS *",
"test_verbose_skip_reason.py::test_7 SKIPPED *",
"test_verbose_skip_reason.py::test_8 SKIPPED (888 is great) *",
"test_verbose_skip_reason.py::test_9 XFAIL *",
"test_verbose_skip_reason.py::test_10 XFAIL (It's 🕙 o'clock) *",
]
result = pytester.runpytest("-v")
result.stdout.fnmatch_lines(
common_output
+ [
"test_verbose_skip_reason.py::test_long_skip SKIPPED (cannot *...) *",
"test_verbose_skip_reason.py::test_long_xfail XFAIL (cannot *...) *",
]
)
result = pytester.runpytest("-vv")
result.stdout.fnmatch_lines(
common_output
+ [
(
"test_verbose_skip_reason.py::test_long_skip SKIPPED"
" (cannot do foobar because baz is missing due to I don't know what) *"
),
(
"test_verbose_skip_reason.py::test_long_xfail XFAIL"
" (cannot do foobar because baz is missing due to I don't know what) *"
),
[
"test_verbose_skip_reason.py::test_1 SKIPPED (123) *",
"test_verbose_skip_reason.py::test_2 XPASS (456) *",
"test_verbose_skip_reason.py::test_3 XFAIL (789) *",
"test_verbose_skip_reason.py::test_4 XFAIL *",
"test_verbose_skip_reason.py::test_5 SKIPPED (unconditional skip) *",
"test_verbose_skip_reason.py::test_6 XPASS *",
"test_verbose_skip_reason.py::test_7 SKIPPED *",
"test_verbose_skip_reason.py::test_8 SKIPPED (888 is great) *",
"test_verbose_skip_reason.py::test_9 XFAIL *",
"test_verbose_skip_reason.py::test_10 XFAIL (It's 🕙 o'clock) *",
]
)
@@ -716,7 +682,9 @@ class TestTerminalFunctional:
pass
"""
)
result = pytester.runpytest("-k", "test_t", testpath)
result = pytester.runpytest(
"-Wignore::pytest.PytestRemovedIn7Warning", "-k", "test_two:", testpath
)
result.stdout.fnmatch_lines(
["collected 3 items / 1 deselected / 2 selected", "*test_deselected.py ..*"]
)
@@ -783,33 +751,6 @@ class TestTerminalFunctional:
result.stdout.no_fnmatch_line("*= 1 deselected =*")
assert result.ret == 0
def test_selected_count_with_error(self, pytester: Pytester) -> None:
pytester.makepyfile(
test_selected_count_3="""
def test_one():
pass
def test_two():
pass
def test_three():
pass
""",
test_selected_count_error="""
5/0
def test_foo():
pass
def test_bar():
pass
""",
)
result = pytester.runpytest("-k", "test_t")
result.stdout.fnmatch_lines(
[
"collected 3 items / 1 error / 1 deselected / 2 selected",
"* ERROR collecting test_selected_count_error.py *",
]
)
assert result.ret == ExitCode.INTERRUPTED
def test_no_skip_summary_if_failure(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""

View File

@@ -1498,30 +1498,3 @@ def test_traceback_pruning(pytester: Pytester) -> None:
assert passed == 1
assert failed == 1
assert reprec.ret == 1
def test_raising_unittest_skiptest_during_collection(
pytester: Pytester,
) -> None:
pytester.makepyfile(
"""
import unittest
class TestIt(unittest.TestCase):
def test_it(self): pass
def test_it2(self): pass
raise unittest.SkipTest()
class TestIt2(unittest.TestCase):
def test_it(self): pass
def test_it2(self): pass
"""
)
reprec = pytester.inline_run()
passed, skipped, failed = reprec.countoutcomes()
assert passed == 0
# Unittest reports one fake test for a skipped module.
assert skipped == 1
assert failed == 0
assert reprec.ret == ExitCode.NO_TESTS_COLLECTED

View File

@@ -1,5 +1,4 @@
import os
import sys
import warnings
from typing import List
from typing import Optional
@@ -240,7 +239,7 @@ def test_filterwarnings_mark_registration(pytester: Pytester) -> None:
@pytest.mark.filterwarnings("always::UserWarning")
def test_warning_recorded_hook(pytester: Pytester) -> None:
def test_warning_captured_hook(pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_configure(config):
@@ -277,9 +276,9 @@ def test_warning_recorded_hook(pytester: Pytester) -> None:
expected = [
("config warning", "config", ""),
("collect warning", "collect", ""),
("setup warning", "runtest", "test_warning_recorded_hook.py::test_func"),
("call warning", "runtest", "test_warning_recorded_hook.py::test_func"),
("teardown warning", "runtest", "test_warning_recorded_hook.py::test_func"),
("setup warning", "runtest", "test_warning_captured_hook.py::test_func"),
("call warning", "runtest", "test_warning_captured_hook.py::test_func"),
("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"),
]
for index in range(len(expected)):
collected_result = collected[index]
@@ -518,7 +517,6 @@ class TestDeprecationWarningsByDefault:
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
@pytest.mark.skip("not relevant until pytest 8.0")
@pytest.mark.parametrize("change_default", [None, "ini", "cmdline"])
def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> None:
"""This ensures that PytestRemovedInXWarnings raised by pytest are turned into errors.
@@ -530,7 +528,7 @@ def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> No
"""
import warnings, pytest
def test():
warnings.warn(pytest.PytestRemovedIn8Warning("some warning"))
warnings.warn(pytest.PytestRemovedIn7Warning("some warning"))
"""
)
if change_default == "ini":
@@ -538,12 +536,12 @@ def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> No
"""
[pytest]
filterwarnings =
ignore::pytest.PytestRemovedIn8Warning
ignore::pytest.PytestRemovedIn7Warning
"""
)
args = (
("-Wignore::pytest.PytestRemovedIn8Warning",)
("-Wignore::pytest.PytestRemovedIn7Warning",)
if change_default == "cmdline"
else ()
)
@@ -775,57 +773,3 @@ class TestStackLevel:
"*Unknown pytest.mark.unknown*",
]
)
def test_resource_warning(pytester: Pytester, monkeypatch: pytest.MonkeyPatch) -> None:
# Some platforms (notably PyPy) don't have tracemalloc.
# We choose to explicitly not skip this in case tracemalloc is not
# available, using `importorskip("tracemalloc")` for example,
# because we want to ensure the same code path does not break in those platforms.
try:
import tracemalloc # noqa
has_tracemalloc = True
except ImportError:
has_tracemalloc = False
# Explicitly disable PYTHONTRACEMALLOC in case pytest's test suite is running
# with it enabled.
monkeypatch.delenv("PYTHONTRACEMALLOC", raising=False)
pytester.makepyfile(
"""
def open_file(p):
f = p.open("r")
assert p.read_text() == "hello"
def test_resource_warning(tmp_path):
p = tmp_path.joinpath("foo.txt")
p.write_text("hello")
open_file(p)
"""
)
result = pytester.run(sys.executable, "-Xdev", "-m", "pytest")
expected_extra = (
[
"*ResourceWarning* unclosed file*",
"*Enable tracemalloc to get traceback where the object was allocated*",
"*See https* for more info.",
]
if has_tracemalloc
else []
)
result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"])
monkeypatch.setenv("PYTHONTRACEMALLOC", "20")
result = pytester.run(sys.executable, "-Xdev", "-m", "pytest")
expected_extra = (
[
"*ResourceWarning* unclosed file*",
"*Object allocated at*",
]
if has_tracemalloc
else []
)
result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"])

View File

@@ -4,6 +4,7 @@ minversion = 3.20.0
distshare = {homedir}/.tox/distshare
envlist =
linting
py36
py37
py38
py39
@@ -100,9 +101,9 @@ commands =
[testenv:plugins]
# use latest versions of all plugins, including pre-releases
pip_pre=true
# use latest pip to get new dependency resolver (#7783)
# use latest pip and new dependency resolver (#7783)
download=true
install_command=python -m pip install {opts} {packages}
install_command=python -m pip --use-feature=2020-resolver install {opts} {packages}
changedir = testing/plugins_integration
deps = -rtesting/plugins_integration/requirements.txt
setenv =