Compare commits
206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2390610696 | ||
|
|
a0714aa007 | ||
|
|
44ad1c9811 | ||
|
|
5dc77253d4 | ||
|
|
a517827318 | ||
|
|
21fe071d79 | ||
|
|
f8bb8572fe | ||
|
|
1944dc06d3 | ||
|
|
946634c84c | ||
|
|
d849a3ed64 | ||
|
|
721a0881fb | ||
|
|
5341b9cd67 | ||
|
|
c39bdf6190 | ||
|
|
b0c4775a28 | ||
|
|
45f34dfb8d | ||
|
|
e4f022f0d8 | ||
|
|
63b0c6f75f | ||
|
|
884b911a9c | ||
|
|
6e49a74089 | ||
|
|
79c2012d40 | ||
|
|
de69883e3a | ||
|
|
1de00e9830 | ||
|
|
7f5d9b9df4 | ||
|
|
82eb86f707 | ||
|
|
0319a0d4fd | ||
|
|
7855a72d2c | ||
|
|
7a0a0e8b08 | ||
|
|
fbcfd3a52e | ||
|
|
b170081788 | ||
|
|
7a5f2feefb | ||
|
|
69140717d4 | ||
|
|
5c7c3f6329 | ||
|
|
ba40975bb7 | ||
|
|
e3fe7286f8 | ||
|
|
34c73944e1 | ||
|
|
350122abb2 | ||
|
|
06ff7ca13b | ||
|
|
6dfe498c77 | ||
|
|
a566b78730 | ||
|
|
511adf85be | ||
|
|
c71b5df734 | ||
|
|
d53951836d | ||
|
|
a4d7254d18 | ||
|
|
b6c55787fe | ||
|
|
fb03d1388b | ||
|
|
d9bf9dbec1 | ||
|
|
64319dbc01 | ||
|
|
1e8135df16 | ||
|
|
1e32a4b570 | ||
|
|
faa1f9d2ad | ||
|
|
14890329dc | ||
|
|
d97d44a97a | ||
|
|
f6b995e9d5 | ||
|
|
661b938fca | ||
|
|
7e510769b4 | ||
|
|
a704605cf1 | ||
|
|
797b924fc4 | ||
|
|
b5ec092525 | ||
|
|
5b35518389 | ||
|
|
8528052a95 | ||
|
|
1eb83706b6 | ||
|
|
fa09f6dcc6 | ||
|
|
b55c02c3c9 | ||
|
|
b7a142f4ba | ||
|
|
2d824329eb | ||
|
|
ecb23106d8 | ||
|
|
8174a30164 | ||
|
|
0142bb6687 | ||
|
|
52cf700f1b | ||
|
|
6b3ece5bdb | ||
|
|
4059000834 | ||
|
|
7d5207a736 | ||
|
|
3e65a461c7 | ||
|
|
fc3b5b2610 | ||
|
|
9335a0b445 | ||
|
|
0ded3297a9 | ||
|
|
fc9cbbd4c4 | ||
|
|
1a17539065 | ||
|
|
32de8e2893 | ||
|
|
5d4a342a7a | ||
|
|
1790f17228 | ||
|
|
ee8baa2676 | ||
|
|
ae38b076da | ||
|
|
85c5bd26b6 | ||
|
|
e1204e1e23 | ||
|
|
313b61471f | ||
|
|
1daa8129c6 | ||
|
|
b5ff089d2c | ||
|
|
fda8024622 | ||
|
|
4b823a42ce | ||
|
|
6c9b277ce4 | ||
|
|
3de43e5102 | ||
|
|
c76ae74bd7 | ||
|
|
24534cdd29 | ||
|
|
99c78aa93a | ||
|
|
3a6bdcd76b | ||
|
|
4a1bba25b9 | ||
|
|
9e1add75f7 | ||
|
|
4da9026766 | ||
|
|
7c231baa64 | ||
|
|
fc538c5766 | ||
|
|
fbfd4b5005 | ||
|
|
ec752537ea | ||
|
|
dd667336ce | ||
|
|
29d16d2939 | ||
|
|
5313d50e18 | ||
|
|
4f3f36c396 | ||
|
|
af124c7f21 | ||
|
|
4e6d53fef5 | ||
|
|
751d726d21 | ||
|
|
9e491f430e | ||
|
|
63f258f432 | ||
|
|
baaa67dfb9 | ||
|
|
519f351b4f | ||
|
|
5d53447a73 | ||
|
|
1716d3c9bf | ||
|
|
ac699e7b25 | ||
|
|
a5f37199a9 | ||
|
|
9fa82598a9 | ||
|
|
ba32a3bd87 | ||
|
|
c8641f879f | ||
|
|
6041511fb4 | ||
|
|
739408b958 | ||
|
|
1636322995 | ||
|
|
b8edacb8f1 | ||
|
|
612489e2bd | ||
|
|
383774db10 | ||
|
|
3b5b3cf50e | ||
|
|
23e343af60 | ||
|
|
f9a995b56d | ||
|
|
d9d78a8aef | ||
|
|
76d15231f5 | ||
|
|
4cc05e7bee | ||
|
|
2d57d5c32f | ||
|
|
faeb16146b | ||
|
|
b241c0b479 | ||
|
|
78403237cf | ||
|
|
f84fea0888 | ||
|
|
271bdf6c23 | ||
|
|
fd56968f2b | ||
|
|
5b75b0d03f | ||
|
|
aac5d5d08b | ||
|
|
b1460f3261 | ||
|
|
a88ae8289c | ||
|
|
62320e4ff7 | ||
|
|
6514041a35 | ||
|
|
7d548c38e2 | ||
|
|
07eeeb8dfc | ||
|
|
be774667c2 | ||
|
|
762bb61562 | ||
|
|
725de3a0d3 | ||
|
|
fcada1ea47 | ||
|
|
6f7f89f3c4 | ||
|
|
0a20452f78 | ||
|
|
cc23ec91d0 | ||
|
|
a15f544962 | ||
|
|
3823ce60dd | ||
|
|
e03f82c359 | ||
|
|
158f41fdf8 | ||
|
|
fd6a4507ac | ||
|
|
15156757b6 | ||
|
|
0860f4e916 | ||
|
|
11965d1c27 | ||
|
|
14be71b234 | ||
|
|
2d6206b89a | ||
|
|
41f57ef95d | ||
|
|
4eca6063c8 | ||
|
|
819f5abd73 | ||
|
|
adf891ab1d | ||
|
|
f08184ba20 | ||
|
|
28783e5d23 | ||
|
|
7834b3b07f | ||
|
|
ece756fcb4 | ||
|
|
d380771065 | ||
|
|
b893d2a0fe | ||
|
|
e3b1799766 | ||
|
|
5d1385320f | ||
|
|
eff54aece1 | ||
|
|
bc1fc3f0fc | ||
|
|
784ffa0fad | ||
|
|
90412827c3 | ||
|
|
424c3eebde | ||
|
|
9c2247ec1b | ||
|
|
61f7c27ec0 | ||
|
|
1b81d636e2 | ||
|
|
1b196fbeaf | ||
|
|
22524046cf | ||
|
|
6dcd652d4a | ||
|
|
be9faa68d8 | ||
|
|
e4e13dd913 | ||
|
|
5a399030c1 | ||
|
|
19c8ef63a4 | ||
|
|
22951bba67 | ||
|
|
ec8e23951d | ||
|
|
bf47357511 | ||
|
|
2d2dc4a2a8 | ||
|
|
f1c7585184 | ||
|
|
bb6155adfa | ||
|
|
5fefd7de96 | ||
|
|
750ce30392 | ||
|
|
de1f378b60 | ||
|
|
14d5b4ca6c | ||
|
|
307dbf15c4 | ||
|
|
a8697601ad | ||
|
|
0ed1b0ac12 | ||
|
|
26b0702b98 |
62
.github/workflows/deploy.yml
vendored
62
.github/workflows/deploy.yml
vendored
@@ -1,26 +1,23 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# These tags are protected, see:
|
||||
# https://github.com/pytest-dev/pytest/settings/tag_protection
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version'
|
||||
required: true
|
||||
default: '1.2.3'
|
||||
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -31,6 +28,18 @@ jobs:
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
needs: [package]
|
||||
runs-on: ubuntu-latest
|
||||
environment: deploy
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -38,14 +47,35 @@ jobs:
|
||||
path: dist
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
git config user.name "pytest bot"
|
||||
git config user.email "pytestbot@gmail.com"
|
||||
git tag --annotate --message=v${{ github.event.inputs.version }} v${{ github.event.inputs.version }} ${{ github.sha }}
|
||||
git push origin v${{ github.event.inputs.version }}
|
||||
|
||||
release-notes:
|
||||
|
||||
# todo: generate the content in the build job
|
||||
# the goal being of using a github action script to push the release data
|
||||
# after success instead of creating a complete python/tox env
|
||||
needs: [deploy]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
password: ${{ secrets.pypi_token }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.7"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
|
||||
23
.github/workflows/stale.yml
vendored
Normal file
23
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: close needs-information issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
debug-only: false
|
||||
days-before-issue-stale: 14
|
||||
days-before-issue-close: 7
|
||||
only-labels: "status: needs information"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
84
.github/workflows/test.yml
vendored
84
.github/workflows/test.yml
vendored
@@ -27,7 +27,19 @@ concurrency:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
@@ -38,27 +50,28 @@ jobs:
|
||||
matrix:
|
||||
name: [
|
||||
"windows-py37",
|
||||
"windows-py37-pluggy",
|
||||
"windows-py38",
|
||||
"windows-py38-pluggy",
|
||||
"windows-py39",
|
||||
"windows-py310",
|
||||
"windows-py311",
|
||||
"windows-py312",
|
||||
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
"ubuntu-py38",
|
||||
"ubuntu-py38-pluggy",
|
||||
"ubuntu-py39",
|
||||
"ubuntu-py310",
|
||||
"ubuntu-py311",
|
||||
"ubuntu-py312",
|
||||
"ubuntu-pypy3",
|
||||
|
||||
"macos-py37",
|
||||
"macos-py38",
|
||||
"macos-py39",
|
||||
"macos-py310",
|
||||
"macos-py312",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
"plugins",
|
||||
]
|
||||
@@ -68,15 +81,15 @@ jobs:
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-numpy"
|
||||
- name: "windows-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-pluggymain-pylib-xdist"
|
||||
- name: "windows-py38"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38-unittestextras"
|
||||
use_coverage: true
|
||||
- name: "windows-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "windows-py39"
|
||||
python: "3.9"
|
||||
os: windows-latest
|
||||
@@ -86,19 +99,19 @@ jobs:
|
||||
os: windows-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "windows-py311"
|
||||
python: "3.11-dev"
|
||||
python: "3.11"
|
||||
os: windows-latest
|
||||
tox_env: "py311"
|
||||
- name: "windows-py312"
|
||||
python: "3.12-dev"
|
||||
os: windows-latest
|
||||
tox_env: "py312"
|
||||
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-lsof-numpy-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py37-freeze"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -107,6 +120,10 @@ jobs:
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-xdist"
|
||||
- name: "ubuntu-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py39"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
@@ -116,10 +133,15 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "ubuntu-py311"
|
||||
python: "3.11-dev"
|
||||
python: "3.11"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py311"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py312"
|
||||
python: "3.12-dev"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py312"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-pypy3"
|
||||
python: "pypy-3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -129,29 +151,25 @@ jobs:
|
||||
python: "3.7"
|
||||
os: macos-latest
|
||||
tox_env: "py37-xdist"
|
||||
- name: "macos-py38"
|
||||
python: "3.8"
|
||||
os: macos-latest
|
||||
tox_env: "py38-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py39"
|
||||
python: "3.9"
|
||||
os: macos-latest
|
||||
tox_env: "py39-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py310"
|
||||
python: "3.10"
|
||||
os: macos-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "macos-py312"
|
||||
python: "3.12-dev"
|
||||
os: macos-latest
|
||||
tox_env: "py312-xdist"
|
||||
|
||||
- name: "plugins"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
tox_env: "plugins"
|
||||
|
||||
- name: "docs"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "docs"
|
||||
- name: "doctesting"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -164,10 +182,17 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -176,11 +201,13 @@ jobs:
|
||||
|
||||
- name: Test without coverage
|
||||
if: "! matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}"
|
||||
shell: bash
|
||||
run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
|
||||
|
||||
- name: Test with coverage
|
||||
if: "matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}-coverage"
|
||||
shell: bash
|
||||
run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
|
||||
|
||||
- name: Generate coverage report
|
||||
if: "matrix.use_coverage"
|
||||
@@ -194,10 +221,3 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage.xml
|
||||
verbose: true
|
||||
|
||||
check-package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
2
.github/workflows/update-plugin-list.yml
vendored
2
.github/workflows/update-plugin-list.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
default_language_version:
|
||||
python: "3.10"
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
@@ -7,7 +5,7 @@ repos:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: 1.13.0
|
||||
rev: 1.14.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==23.1.0]
|
||||
@@ -23,7 +21,7 @@ repos:
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.0.2
|
||||
rev: v2.1.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
@@ -38,27 +36,27 @@ repos:
|
||||
additional_dependencies:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v3.9.0
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.10.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v3.7.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.2.0
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.11", "--include-version-classifiers"]
|
||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.1.1
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
@@ -9,6 +9,10 @@ python:
|
||||
path: .
|
||||
- requirements: doc/en/requirements.txt
|
||||
|
||||
sphinx:
|
||||
configuration: doc/en/conf.py
|
||||
fail_on_warning: true
|
||||
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
|
||||
15
AUTHORS
15
AUTHORS
@@ -8,11 +8,14 @@ Abdeali JK
|
||||
Abdelrahman Elbehery
|
||||
Abhijeet Kasurde
|
||||
Adam Johnson
|
||||
Adam Stewart
|
||||
Adam Uhlir
|
||||
Ahn Ki-Wook
|
||||
Akiomi Kamakura
|
||||
Alan Velasco
|
||||
Alessio Izzo
|
||||
Alex Jones
|
||||
Alex Lambson
|
||||
Alexander Johnson
|
||||
Alexander King
|
||||
Alexei Kozlenok
|
||||
@@ -55,6 +58,7 @@ Benjamin Peterson
|
||||
Bernard Pratz
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Larsen
|
||||
Brian Maissy
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
@@ -68,6 +72,7 @@ Charles Cloud
|
||||
Charles Machalow
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Cheuk Ting Ho
|
||||
Chris Mahoney
|
||||
Chris Lamb
|
||||
Chris NeJame
|
||||
Chris Rose
|
||||
@@ -126,6 +131,7 @@ Eric Siegerman
|
||||
Erik Aronesty
|
||||
Erik M. Bray
|
||||
Evan Kepner
|
||||
Evgeny Seliverstov
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
Felix Hofstätter
|
||||
@@ -160,6 +166,8 @@ Ian Bicking
|
||||
Ian Lesperance
|
||||
Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Isaac Virshup
|
||||
Israel Fruchter
|
||||
Itxaso Aizpurua
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
@@ -192,6 +200,7 @@ Justice Ndou
|
||||
Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Kamran Ahmad
|
||||
Kenny Y
|
||||
Karl O. Pinc
|
||||
Karthikeyan Singaravelan
|
||||
Katarzyna Jachim
|
||||
@@ -222,6 +231,7 @@ Maho
|
||||
Maik Figura
|
||||
Mandeep Bhutani
|
||||
Manuel Krebber
|
||||
Marc Mueller
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
@@ -304,6 +314,7 @@ Rafal Semik
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Robert Holt
|
||||
Roberto Aldera
|
||||
Roberto Polli
|
||||
Roland Puntaier
|
||||
Romain Dorgueil
|
||||
@@ -326,11 +337,13 @@ Serhii Mozghovyi
|
||||
Seth Junot
|
||||
Shantanu Jain
|
||||
Shubham Adep
|
||||
Simon Blanchard
|
||||
Simon Gomizelj
|
||||
Simon Holesch
|
||||
Simon Kerr
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
Stefaan Lippens
|
||||
Stefan Farmbauer
|
||||
Stefan Scherfke
|
||||
Stefan Zimmermann
|
||||
@@ -363,12 +376,14 @@ Tony Narlock
|
||||
Tor Colvin
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tyler Smart
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Maryama
|
||||
Victor Rodriguez
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Vijay Arora
|
||||
Virgil Dupras
|
||||
Vitaly Lashmanov
|
||||
Vivaan Verma
|
||||
|
||||
@@ -50,7 +50,7 @@ Fix bugs
|
||||
--------
|
||||
|
||||
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
|
||||
See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_
|
||||
See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
|
||||
that are friendly to new contributors.
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
|
||||
|
||||
@@ -133,14 +133,12 @@ Releasing
|
||||
|
||||
Both automatic and manual processes described above follow the same steps from this point onward.
|
||||
|
||||
#. 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::
|
||||
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
|
||||
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
|
||||
as source.
|
||||
|
||||
git fetch upstream
|
||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
||||
git push upstream MAJOR.MINOR.PATCH
|
||||
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||
and tag the repository.
|
||||
|
||||
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||
@@ -1 +0,0 @@
|
||||
Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
|
||||
@@ -1,3 +0,0 @@
|
||||
Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
|
||||
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
|
||||
file.
|
||||
@@ -1 +0,0 @@
|
||||
pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).
|
||||
@@ -1 +0,0 @@
|
||||
Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
|
||||
@@ -1 +0,0 @@
|
||||
Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
|
||||
@@ -1 +0,0 @@
|
||||
The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
|
||||
@@ -1 +0,0 @@
|
||||
:confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
|
||||
@@ -1 +0,0 @@
|
||||
pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
|
||||
@@ -1 +0,0 @@
|
||||
Correctly handle ``__tracebackhide__`` for chained exceptions.
|
||||
@@ -1,2 +0,0 @@
|
||||
The full output of a test is no longer truncated if the truncation message would be longer than
|
||||
the hidden text. The line number shown has also been fixed.
|
||||
@@ -1 +0,0 @@
|
||||
``--log-disable`` CLI option added to disable individual loggers.
|
||||
@@ -1 +0,0 @@
|
||||
Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
||||
@@ -6,6 +6,13 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.4.3
|
||||
release-7.4.2
|
||||
release-7.4.1
|
||||
release-7.4.0
|
||||
release-7.3.2
|
||||
release-7.3.1
|
||||
release-7.3.0
|
||||
release-7.2.2
|
||||
release-7.2.1
|
||||
release-7.2.0
|
||||
|
||||
130
doc/en/announce/release-7.3.0.rst
Normal file
130
doc/en/announce/release-7.3.0.rst
Normal file
@@ -0,0 +1,130 @@
|
||||
pytest-7.3.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.3.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Aaron Berdy
|
||||
* Adam Turner
|
||||
* Albert Villanova del Moral
|
||||
* Alessio Izzo
|
||||
* Alex Hadley
|
||||
* Alice Purcell
|
||||
* Anthony Sottile
|
||||
* Anton Yakutovich
|
||||
* Ashish Kurmi
|
||||
* Babak Keyvani
|
||||
* Billy
|
||||
* Brandon Chinn
|
||||
* Bruno Oliveira
|
||||
* Cal Jacobson
|
||||
* Chanvin Xiao
|
||||
* Cheuk Ting Ho
|
||||
* Chris Wheeler
|
||||
* Daniel Garcia Moreno
|
||||
* Daniel Scheffler
|
||||
* Daniel Valenzuela
|
||||
* EmptyRabbit
|
||||
* Ezio Melotti
|
||||
* Felix Hofstätter
|
||||
* Florian Best
|
||||
* Florian Bruhin
|
||||
* Fredrik Berndtsson
|
||||
* Gabriel Landau
|
||||
* Garvit Shubham
|
||||
* Gergely Kalmár
|
||||
* HTRafal
|
||||
* Hugo van Kemenade
|
||||
* Ilya Konstantinov
|
||||
* Itxaso Aizpurua
|
||||
* James Gerity
|
||||
* Jay
|
||||
* John Litborn
|
||||
* Jon Parise
|
||||
* Jouke Witteveen
|
||||
* Kadino
|
||||
* Kevin C
|
||||
* Kian Eliasi
|
||||
* Klaus Rettinghaus
|
||||
* Kodi Arfer
|
||||
* Mahesh Vashishtha
|
||||
* Manuel Jacob
|
||||
* Marko Pacak
|
||||
* MatthewFlamm
|
||||
* Miro Hrončok
|
||||
* Nate Meyvis
|
||||
* Neil Girdhar
|
||||
* Nhieuvu1802
|
||||
* Nipunn Koorapati
|
||||
* Ofek Lev
|
||||
* Paul Kehrer
|
||||
* Paul Müller
|
||||
* Paul Reece
|
||||
* Pax
|
||||
* Pete Baughman
|
||||
* Peyman Salehi
|
||||
* Philipp A
|
||||
* Pierre Sassoulas
|
||||
* Prerak Patel
|
||||
* Ramsey
|
||||
* Ran Benita
|
||||
* Robert O'Shea
|
||||
* Ronny Pfannschmidt
|
||||
* Rowin
|
||||
* Ruth Comer
|
||||
* Samuel Colvin
|
||||
* Samuel Gaist
|
||||
* Sandro Tosi
|
||||
* Santiago Castro
|
||||
* Shantanu
|
||||
* Simon K
|
||||
* Stefanie Molin
|
||||
* Stephen Rosen
|
||||
* Sviatoslav Sydorenko
|
||||
* Tatiana Ovary
|
||||
* Teejay
|
||||
* Thierry Moisan
|
||||
* Thomas Grainger
|
||||
* Tim Hoffmann
|
||||
* Tobias Diez
|
||||
* Tony Narlock
|
||||
* Vivaan Verma
|
||||
* Wolfremium
|
||||
* Yannick PÉROUX
|
||||
* Yusuke Kadowaki
|
||||
* Zac Hatfield-Dodds
|
||||
* Zach OBrien
|
||||
* aizpurua23a
|
||||
* bitzge
|
||||
* bluthej
|
||||
* gresm
|
||||
* holesch
|
||||
* itxasos23
|
||||
* johnkangw
|
||||
* q0w
|
||||
* rdb
|
||||
* s-padmanaban
|
||||
* skhomuti
|
||||
* sommersoft
|
||||
* vin01
|
||||
* wim glenn
|
||||
* wodny
|
||||
* zx.qiu
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-7.3.1.rst
Normal file
18
doc/en/announce/release-7.3.1.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-7.3.1
|
||||
=======================================
|
||||
|
||||
pytest 7.3.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
21
doc/en/announce/release-7.3.2.rst
Normal file
21
doc/en/announce/release-7.3.2.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
pytest-7.3.2
|
||||
=======================================
|
||||
|
||||
pytest 7.3.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Adam J. Stewart
|
||||
* Alessio Izzo
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
49
doc/en/announce/release-7.4.0.rst
Normal file
49
doc/en/announce/release-7.4.0.rst
Normal file
@@ -0,0 +1,49 @@
|
||||
pytest-7.4.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.4.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Adam J. Stewart
|
||||
* Alessio Izzo
|
||||
* Alex
|
||||
* Alex Lambson
|
||||
* Brian Larsen
|
||||
* Bruno Oliveira
|
||||
* Bryan Ricker
|
||||
* Chris Mahoney
|
||||
* Facundo Batista
|
||||
* Florian Bruhin
|
||||
* Jarrett Keifer
|
||||
* Kenny Y
|
||||
* Miro Hrončok
|
||||
* Ran Benita
|
||||
* Roberto Aldera
|
||||
* Ronny Pfannschmidt
|
||||
* Sergey Kim
|
||||
* Stefanie Molin
|
||||
* Vijay Arora
|
||||
* Ville Skyttä
|
||||
* Zac Hatfield-Dodds
|
||||
* bzoracler
|
||||
* leeyueh
|
||||
* nondescryptid
|
||||
* theirix
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
20
doc/en/announce/release-7.4.1.rst
Normal file
20
doc/en/announce/release-7.4.1.rst
Normal file
@@ -0,0 +1,20 @@
|
||||
pytest-7.4.1
|
||||
=======================================
|
||||
|
||||
pytest 7.4.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-7.4.2.rst
Normal file
18
doc/en/announce/release-7.4.2.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-7.4.2
|
||||
=======================================
|
||||
|
||||
pytest 7.4.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
19
doc/en/announce/release-7.4.3.rst
Normal file
19
doc/en/announce/release-7.4.3.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
pytest-7.4.3
|
||||
=======================================
|
||||
|
||||
pytest 7.4.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Marc Mueller
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -92,3 +92,5 @@ pytest version min. Python version
|
||||
5.0 - 6.1 3.5+
|
||||
3.3 - 4.6 2.7, 3.4+
|
||||
============== ===================
|
||||
|
||||
`Status of Python Versions <https://devguide.python.org/versions/>`__.
|
||||
|
||||
@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
cachedir: .pytest_cache
|
||||
rootdir: /home/sweet/project
|
||||
collected 0 items
|
||||
cache -- .../_pytest/cacheprovider.py:510
|
||||
cache -- .../_pytest/cacheprovider.py:532
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
@@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:757
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:498
|
||||
caplog -- .../_pytest/logging.py:570
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
@@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||
* caplog.clear() -> clear captured records and formatted log output string
|
||||
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:29
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:30
|
||||
A convenient fixture for monkey-patching.
|
||||
|
||||
The fixture provides these methods to modify objects, dictionaries, or
|
||||
|
||||
@@ -28,6 +28,278 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.4.3 (2023-10-24)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base class markers are considered first -- this resolves a regression.
|
||||
|
||||
|
||||
- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases.
|
||||
|
||||
|
||||
- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
|
||||
|
||||
|
||||
pytest 7.4.2 (2023-09-07)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
|
||||
|
||||
|
||||
- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
|
||||
|
||||
|
||||
- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
|
||||
|
||||
|
||||
- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
|
||||
|
||||
|
||||
pytest 7.4.1 (2023-09-02)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
|
||||
child modules as attributes of the parent modules.
|
||||
|
||||
|
||||
- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
|
||||
|
||||
|
||||
- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
|
||||
to be imported more than once, causing problems with modules that have import side effects.
|
||||
|
||||
|
||||
pytest 7.4.0 (2023-06-23)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
|
||||
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
|
||||
|
||||
|
||||
- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
|
||||
|
||||
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
|
||||
|
||||
Previously, the last frame of the traceback was shown, even though it was hidden.
|
||||
|
||||
|
||||
- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
|
||||
|
||||
Added ``TerminalReporter.wrap_write()`` as a helper for that.
|
||||
|
||||
|
||||
- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
|
||||
|
||||
|
||||
- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
|
||||
|
||||
|
||||
- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
|
||||
|
||||
|
||||
- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`.
|
||||
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
|
||||
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
|
||||
|
||||
|
||||
- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
|
||||
|
||||
If after updating to this version you see that your `norecursedirs` setting is not being respected,
|
||||
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
|
||||
Most likely, your hook returns `False` for paths it does not want to ignore,
|
||||
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
|
||||
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
|
||||
|
||||
|
||||
- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
|
||||
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
|
||||
``logging.disable(LEVEL)``.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
|
||||
|
||||
|
||||
- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
|
||||
|
||||
|
||||
- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
|
||||
even when it was not utilized (e.g. when explicit paths were given on the command line).
|
||||
Now the ``testpaths`` are only considered when they are in use.
|
||||
|
||||
|
||||
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
|
||||
|
||||
|
||||
- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
|
||||
|
||||
|
||||
pytest 7.3.2 (2023-06-10)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
|
||||
|
||||
|
||||
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
|
||||
|
||||
|
||||
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
|
||||
|
||||
|
||||
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
|
||||
|
||||
|
||||
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
|
||||
|
||||
|
||||
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
|
||||
|
||||
|
||||
pytest 7.3.1 (2023-04-14)
|
||||
=========================
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
|
||||
|
||||
|
||||
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
|
||||
|
||||
|
||||
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
|
||||
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
|
||||
|
||||
|
||||
pytest 7.3.0 (2023-04-08)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#10525 <https://github.com/pytest-dev/pytest/issues/10525>`_: Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
|
||||
|
||||
|
||||
- `#10755 <https://github.com/pytest-dev/pytest/issues/10755>`_: :confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact.
|
||||
|
||||
|
||||
- `#7431 <https://github.com/pytest-dev/pytest/issues/7431>`_: ``--log-disable`` CLI option added to disable individual loggers.
|
||||
|
||||
|
||||
- `#8141 <https://github.com/pytest-dev/pytest/issues/8141>`_: Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10226 <https://github.com/pytest-dev/pytest/issues/10226>`_: If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||
|
||||
|
||||
- `#10658 <https://github.com/pytest-dev/pytest/issues/10658>`_: Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
|
||||
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
|
||||
file.
|
||||
|
||||
|
||||
- `#10710 <https://github.com/pytest-dev/pytest/issues/10710>`_: Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
|
||||
|
||||
|
||||
- `#10727 <https://github.com/pytest-dev/pytest/issues/10727>`_: Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
|
||||
|
||||
|
||||
- `#10840 <https://github.com/pytest-dev/pytest/issues/10840>`_: pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
|
||||
|
||||
|
||||
- `#6267 <https://github.com/pytest-dev/pytest/issues/6267>`_: The full output of a test is no longer truncated if the truncation message would be longer than
|
||||
the hidden text. The line number shown has also been fixed.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10743 <https://github.com/pytest-dev/pytest/issues/10743>`_: The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
|
||||
|
||||
|
||||
- `#10765 <https://github.com/pytest-dev/pytest/issues/10765>`_: Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
|
||||
|
||||
|
||||
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
|
||||
NOTE: This change was reverted in version 7.3.1.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#10782 <https://github.com/pytest-dev/pytest/issues/10782>`_: Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
|
||||
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
|
||||
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
|
||||
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
|
||||
|
||||
Nice diffs for ``attrs`` classes are still supported though.
|
||||
|
||||
|
||||
pytest 7.2.2 (2023-03-03)
|
||||
=========================
|
||||
|
||||
@@ -468,7 +740,7 @@ Breaking Changes
|
||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||
|
||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
|
||||
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
|
||||
|
||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||
@@ -3968,7 +4240,7 @@ Removals
|
||||
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
||||
|
||||
|
||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
|
||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
|
||||
|
||||
Use ``Node.get_closest_marker(name)`` as a replacement.
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||
# epub_scheme = ''
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# The unique identifier of the text. This can be an ISBN number
|
||||
# or the project homepage.
|
||||
# epub_identifier = ''
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class YamlFile(pytest.File):
|
||||
# We need a yaml parser, e.g. PyYAML.
|
||||
import yaml
|
||||
|
||||
raw = yaml.safe_load(self.path.open())
|
||||
raw = yaml.safe_load(self.path.open(encoding="utf-8"))
|
||||
for name, spec in sorted(raw.items()):
|
||||
yield YamlItem.from_parent(self, name=name, spec=spec)
|
||||
|
||||
@@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
|
||||
" no further details known at this point.",
|
||||
]
|
||||
)
|
||||
return super().repr_failure(excinfo)
|
||||
|
||||
def reportinfo(self):
|
||||
return self.path, 0, f"usecase: {self.name}"
|
||||
|
||||
@@ -502,8 +502,12 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
.. code-block:: pytest
|
||||
|
||||
. $ pytest -rs -q multipython.py
|
||||
........................... [100%]
|
||||
27 passed in 0.12s
|
||||
sssssssssssssssssssssssssss [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [9] multipython.py:69: 'python3.5' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.6' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||
27 skipped in 0.12s
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
|
||||
@@ -70,12 +70,12 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
> assert not f()
|
||||
E assert not 42
|
||||
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0002>()
|
||||
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0006>()
|
||||
|
||||
failure_demo.py:39: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0006>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
|
||||
|
||||
def test_eq_text(self):
|
||||
> assert "spam" == "eggs"
|
||||
@@ -86,7 +86,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:44: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
|
||||
|
||||
def test_eq_similar_text(self):
|
||||
> assert "foo 1 bar" == "foo 2 bar"
|
||||
@@ -99,7 +99,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:47: AssertionError
|
||||
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
|
||||
|
||||
def test_eq_multiline_text(self):
|
||||
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
|
||||
@@ -112,7 +112,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:50: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
|
||||
|
||||
def test_eq_long_text(self):
|
||||
a = "1" * 100 + "a" + "2" * 100
|
||||
@@ -129,7 +129,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:55: AssertionError
|
||||
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
|
||||
|
||||
def test_eq_long_text_multiline(self):
|
||||
a = "1\n" * 100 + "a" + "2\n" * 100
|
||||
@@ -149,7 +149,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:60: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
|
||||
|
||||
def test_eq_list(self):
|
||||
> assert [0, 1, 2] == [0, 1, 3]
|
||||
@@ -160,7 +160,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:63: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
|
||||
|
||||
def test_eq_list_long(self):
|
||||
a = [0] * 100 + [1] + [3] * 100
|
||||
@@ -173,7 +173,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:68: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
|
||||
|
||||
def test_eq_dict(self):
|
||||
> assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
|
||||
@@ -190,7 +190,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:71: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
|
||||
|
||||
def test_eq_set(self):
|
||||
> assert {0, 10, 11, 12} == {0, 20, 21}
|
||||
@@ -207,7 +207,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:74: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
|
||||
|
||||
def test_eq_longer_list(self):
|
||||
> assert [1, 2] == [1, 2, 3]
|
||||
@@ -218,7 +218,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:77: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
|
||||
|
||||
def test_in_list(self):
|
||||
> assert 1 in [0, 2, 3, 4, 5]
|
||||
@@ -227,7 +227,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:80: AssertionError
|
||||
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
|
||||
|
||||
def test_not_in_text_multiline(self):
|
||||
text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail"
|
||||
@@ -245,7 +245,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:84: AssertionError
|
||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
|
||||
|
||||
def test_not_in_text_single(self):
|
||||
text = "single foo line"
|
||||
@@ -258,7 +258,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:88: AssertionError
|
||||
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
|
||||
|
||||
def test_not_in_text_single_long(self):
|
||||
text = "head " * 50 + "foo " + "tail " * 20
|
||||
@@ -271,7 +271,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:92: AssertionError
|
||||
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
|
||||
|
||||
def test_not_in_text_single_long_term(self):
|
||||
text = "head " * 50 + "f" * 70 + "tail " * 20
|
||||
@@ -284,7 +284,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:96: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_dataclass _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
|
||||
|
||||
def test_eq_dataclass(self):
|
||||
from dataclasses import dataclass
|
||||
@@ -311,7 +311,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:108: AssertionError
|
||||
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0017>
|
||||
|
||||
def test_eq_attrs(self):
|
||||
import attr
|
||||
@@ -345,7 +345,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
i = Foo()
|
||||
> assert i.b == 2
|
||||
E assert 1 == 2
|
||||
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0017>.b
|
||||
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0018>.b
|
||||
|
||||
failure_demo.py:128: AssertionError
|
||||
_________________________ test_attribute_instance __________________________
|
||||
@@ -356,8 +356,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
> assert Foo().b == 2
|
||||
E AssertionError: assert 1 == 2
|
||||
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018>.b
|
||||
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019>.b
|
||||
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||
|
||||
failure_demo.py:135: AssertionError
|
||||
__________________________ test_attribute_failure __________________________
|
||||
@@ -375,7 +375,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:146:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef0019>
|
||||
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef001a>
|
||||
|
||||
def _get_b(self):
|
||||
> raise Exception("Failed to get attrib")
|
||||
@@ -393,15 +393,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
> assert Foo().b == Bar().b
|
||||
E AssertionError: assert 1 == 2
|
||||
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
||||
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
||||
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||
|
||||
failure_demo.py:156: AssertionError
|
||||
__________________________ TestRaises.test_raises __________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001c>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
|
||||
|
||||
def test_raises(self):
|
||||
s = "qwe"
|
||||
@@ -411,7 +411,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:166: ValueError
|
||||
______________________ TestRaises.test_raises_doesnt _______________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
|
||||
|
||||
def test_raises_doesnt(self):
|
||||
> raises(OSError, int, "3")
|
||||
@@ -420,7 +420,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:169: Failed
|
||||
__________________________ TestRaises.test_raise ___________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
|
||||
|
||||
def test_raise(self):
|
||||
> raise ValueError("demo error")
|
||||
@@ -429,7 +429,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:172: ValueError
|
||||
________________________ TestRaises.test_tupleerror ________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||
|
||||
def test_tupleerror(self):
|
||||
> a, b = [1] # NOQA
|
||||
@@ -438,7 +438,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:175: ValueError
|
||||
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
|
||||
|
||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||
items = [1, 2, 3]
|
||||
@@ -451,7 +451,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
items is [1, 2, 3]
|
||||
________________________ TestRaises.test_some_error ________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
|
||||
|
||||
def test_some_error(self):
|
||||
> if namenotexi: # NOQA
|
||||
@@ -482,7 +482,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
abc-123:2: AssertionError
|
||||
____________________ TestMoreErrors.test_complex_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0022>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
|
||||
|
||||
def test_complex_error(self):
|
||||
def f():
|
||||
@@ -508,7 +508,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:6: AssertionError
|
||||
___________________ TestMoreErrors.test_z1_unpack_error ____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
|
||||
|
||||
def test_z1_unpack_error(self):
|
||||
items = []
|
||||
@@ -518,7 +518,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:217: ValueError
|
||||
____________________ TestMoreErrors.test_z2_type_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
|
||||
|
||||
def test_z2_type_error(self):
|
||||
items = 3
|
||||
@@ -528,20 +528,20 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:221: TypeError
|
||||
______________________ TestMoreErrors.test_startswith ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0026>
|
||||
|
||||
def test_startswith(self):
|
||||
s = "123"
|
||||
g = "456"
|
||||
> assert s.startswith(g)
|
||||
E AssertionError: assert False
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
|
||||
|
||||
failure_demo.py:226: AssertionError
|
||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0027>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0028>
|
||||
|
||||
def test_startswith_nested(self):
|
||||
def f():
|
||||
@@ -552,15 +552,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
> assert f().startswith(g())
|
||||
E AssertionError: assert False
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0028>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
|
||||
|
||||
failure_demo.py:235: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||
|
||||
def test_global_func(self):
|
||||
> assert isinstance(globf(42), float)
|
||||
@@ -571,18 +571,18 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:238: AssertionError
|
||||
_______________________ TestMoreErrors.test_instance _______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||
|
||||
def test_instance(self):
|
||||
self.x = 6 * 7
|
||||
> assert self.x != 42
|
||||
E assert 42 != 42
|
||||
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>.x
|
||||
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>.x
|
||||
|
||||
failure_demo.py:242: AssertionError
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||
|
||||
def test_compare(self):
|
||||
> assert globf(10) < 5
|
||||
@@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:245: AssertionError
|
||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
|
||||
|
||||
def test_try_finally(self):
|
||||
x = 1
|
||||
@@ -603,7 +603,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:250: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||
|
||||
def test_single_line(self):
|
||||
class A:
|
||||
@@ -618,7 +618,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:261: AssertionError
|
||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||
|
||||
def test_multiline(self):
|
||||
class A:
|
||||
@@ -637,7 +637,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:268: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
|
||||
|
||||
def test_custom_repr(self):
|
||||
class JSON:
|
||||
|
||||
@@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture(scope="package")
|
||||
def db():
|
||||
return DB()
|
||||
|
||||
@@ -817,7 +817,7 @@ case we just write some information out to a ``failures`` file:
|
||||
# we only look at actual failing test calls, not setup/teardown
|
||||
if rep.when == "call" and rep.failed:
|
||||
mode = "a" if os.path.exists("failures") else "w"
|
||||
with open("failures", mode) as f:
|
||||
with open("failures", mode, encoding="utf-8") as f:
|
||||
# let's also access a fixture for the fun of it
|
||||
if "tmp_path" in item.fixturenames:
|
||||
extra = " ({})".format(item.funcargs["tmp_path"])
|
||||
|
||||
@@ -294,3 +294,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
|
||||
|
||||
setuptools intends to
|
||||
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
|
||||
|
||||
Checking with flake8-pytest-style
|
||||
---------------------------------
|
||||
|
||||
In order to ensure that pytest is being used correctly in your project,
|
||||
it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin.
|
||||
|
||||
flake8-pytest-style checks for common mistakes and coding style violations in pytest code,
|
||||
such as incorrect use of fixtures, test function names, and markers.
|
||||
By using this plugin, you can catch these errors early in the development process
|
||||
and ensure that your pytest code is consistent and easy to maintain.
|
||||
|
||||
A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_.
|
||||
|
||||
.. note::
|
||||
|
||||
flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style.
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.2.0.dev534+ga2c84caaa.d20230317
|
||||
pytest 7.4.3
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
|
||||
idiomatic python constructs without boilerplate code while not losing
|
||||
introspection information.
|
||||
|
||||
However, if you specify a message with the assertion like this:
|
||||
If a message is specified with the assertion like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
assert a % 2 == 0, "value was odd, should be even"
|
||||
|
||||
then no assertion introspection takes places at all and the message
|
||||
will be simply shown in the traceback.
|
||||
it is printed alongside the assertion introspection in the traceback.
|
||||
|
||||
See :ref:`assert-details` for more information on assertion introspection.
|
||||
|
||||
|
||||
@@ -176,14 +176,21 @@ with more recent files coming first.
|
||||
Behavior when no tests failed in the last run
|
||||
---------------------------------------------
|
||||
|
||||
When no tests failed in the last run, or when no cached ``lastfailed`` data was
|
||||
found, ``pytest`` can be configured either to run all of the tests or no tests,
|
||||
using the ``--last-failed-no-failures`` option, which takes one of the following values:
|
||||
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
|
||||
Determines whether to execute tests when there are no previously (known)
|
||||
failures or when no cached ``lastfailed`` data was found.
|
||||
|
||||
There are two options:
|
||||
|
||||
* ``all``: when there are no known test failures, runs all tests (the full test suite). This is the default.
|
||||
* ``none``: when there are no known test failures, just emits a message stating this and exit successfully.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # run no tests and exit
|
||||
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
|
||||
|
||||
The new config.cache object
|
||||
--------------------------------
|
||||
|
||||
@@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
|
||||
class TestDirectoryInit:
|
||||
def test_cwd_starts_empty(self):
|
||||
assert os.listdir(os.getcwd()) == []
|
||||
with open("myfile", "w") as f:
|
||||
with open("myfile", "w", encoding="utf-8") as f:
|
||||
f.write("hello")
|
||||
|
||||
def test_cwd_again_starts_empty(self):
|
||||
|
||||
@@ -24,8 +24,8 @@ created in the `base temporary directory`_.
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
assert 0
|
||||
|
||||
@@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
@@ -207,10 +207,10 @@ creation of a per-test temporary directory:
|
||||
@pytest.fixture(autouse=True)
|
||||
def initdir(self, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
|
||||
tmp_path.joinpath("samplefile.ini").write_text("# testdata")
|
||||
tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")
|
||||
|
||||
def test_method(self):
|
||||
with open("samplefile.ini") as f:
|
||||
with open("samplefile.ini", encoding="utf-8") as f:
|
||||
s = f.read()
|
||||
assert "testdata" in s
|
||||
|
||||
|
||||
@@ -35,11 +35,12 @@ Pytest supports several ways to run and select tests from the command-line.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest -k "MyClass and not method"
|
||||
pytest -k 'MyClass and not method'
|
||||
|
||||
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
||||
which can include Python operators that use filenames, class names and function names as variables.
|
||||
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
||||
Use ``""`` instead of ``''`` in expression when running this on Windows
|
||||
|
||||
.. _nodeids:
|
||||
|
||||
@@ -172,7 +173,8 @@ You can invoke ``pytest`` from Python code directly:
|
||||
|
||||
this acts as if you would call "pytest" from the command line.
|
||||
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
|
||||
You can pass in options and arguments:
|
||||
If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
|
||||
You can pass in options and arguments explicitly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
:orphan:
|
||||
|
||||
..
|
||||
.. sidebar:: Next Open Trainings
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `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
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
.. _features:
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
|
||||
setup.cfg
|
||||
~~~~~~~~~
|
||||
|
||||
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <python:distutils/configfile>`, and can also be used to hold pytest configuration
|
||||
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
|
||||
if they have a ``[tool:pytest]`` section.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,6 +82,8 @@ pytest.exit
|
||||
pytest.main
|
||||
~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`pytest.main-usage`
|
||||
|
||||
.. autofunction:: pytest.main
|
||||
|
||||
pytest.param
|
||||
@@ -783,18 +785,66 @@ reporting or interaction with exceptions:
|
||||
.. autofunction:: pytest_leave_pdb
|
||||
|
||||
|
||||
Objects
|
||||
-------
|
||||
Collection tree objects
|
||||
-----------------------
|
||||
|
||||
Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`.
|
||||
These are the collector and item classes (collectively called "nodes") which
|
||||
make up the collection tree.
|
||||
|
||||
Node
|
||||
~~~~
|
||||
|
||||
CallInfo
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.CallInfo()
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
Collector
|
||||
~~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Collector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.Item()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
File
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.File()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FSCollector
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Session
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Package
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Package()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Module
|
||||
~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Module()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Class
|
||||
~~~~~
|
||||
@@ -803,13 +853,34 @@ Class
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Collector
|
||||
~~~~~~~~~
|
||||
Function
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Collector()
|
||||
.. autoclass:: pytest.Function()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Objects
|
||||
-------
|
||||
|
||||
Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`
|
||||
or importable from ``pytest``.
|
||||
|
||||
|
||||
CallInfo
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.CallInfo()
|
||||
:members:
|
||||
|
||||
CollectReport
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -837,13 +908,6 @@ ExitCode
|
||||
.. autoclass:: pytest.ExitCode
|
||||
:members:
|
||||
|
||||
File
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.File()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
FixtureDef
|
||||
~~~~~~~~~~
|
||||
@@ -852,34 +916,6 @@ FixtureDef
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FSCollector
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Function
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Function()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.Item()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
MarkDecorator
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -907,19 +943,6 @@ Metafunc
|
||||
.. autoclass:: pytest.Metafunc()
|
||||
:members:
|
||||
|
||||
Module
|
||||
~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Module()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Node
|
||||
~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
Parser
|
||||
~~~~~~
|
||||
|
||||
@@ -941,13 +964,6 @@ PytestPluginManager
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
Session
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
TestReport
|
||||
~~~~~~~~~~
|
||||
|
||||
@@ -956,10 +972,16 @@ TestReport
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
_Result
|
||||
TestShortLogReport
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.TestShortLogReport()
|
||||
:members:
|
||||
|
||||
Result
|
||||
~~~~~~~
|
||||
|
||||
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information.
|
||||
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.
|
||||
|
||||
Stash
|
||||
~~~~~
|
||||
@@ -1049,11 +1071,11 @@ Environment variables that can be used to change pytest's behavior.
|
||||
|
||||
.. envvar:: CI
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
|
||||
|
||||
.. envvar:: BUILD_NUMBER
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
|
||||
|
||||
.. envvar:: PYTEST_ADDOPTS
|
||||
|
||||
@@ -1697,6 +1719,11 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
pythonpath = src1 src2
|
||||
|
||||
.. note::
|
||||
|
||||
``pythonpath`` does not affect some imports that happen very early,
|
||||
most notably plugins loaded using the ``-p`` command line option.
|
||||
|
||||
|
||||
.. confval:: required_plugins
|
||||
|
||||
@@ -1713,13 +1740,12 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
.. confval:: testpaths
|
||||
|
||||
|
||||
|
||||
Sets list of directories that should be searched for tests when
|
||||
no specific directories, files or test ids are given in the command line when
|
||||
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
||||
File system paths may use shell-style wildcards, including the recursive
|
||||
``**`` pattern.
|
||||
|
||||
Useful when all project tests are in a known location to speed up
|
||||
test collection and to avoid picking up undesired tests by accident.
|
||||
|
||||
@@ -1728,8 +1754,17 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
testpaths = testing doc
|
||||
|
||||
This tells pytest to only look for tests in ``testing`` and ``doc``
|
||||
directories when executing from the root directory.
|
||||
This configuration means that executing:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pytest
|
||||
|
||||
has the same practical effects as executing:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pytest testing doc
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_count
|
||||
@@ -1744,7 +1779,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
tmp_path_retention_count = 3
|
||||
|
||||
Default: 3
|
||||
Default: ``3``
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_policy
|
||||
@@ -1763,7 +1798,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
tmp_path_retention_policy = "all"
|
||||
|
||||
Default: all
|
||||
Default: ``all``
|
||||
|
||||
|
||||
.. confval:: usefixtures
|
||||
@@ -1852,8 +1887,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
tests. Optional argument: glob (default: '*').
|
||||
--cache-clear Remove all cache contents at start of test run
|
||||
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
||||
Which tests to run with no previously (known)
|
||||
failures
|
||||
With ``--lf``, determines whether to execute tests
|
||||
when there are no previously (known) failures or
|
||||
when no cached ``lastfailed`` data was found.
|
||||
``all`` (the default) runs the full test suite
|
||||
again. ``none`` just emits a message about no known
|
||||
failures and exits successfully.
|
||||
--sw, --stepwise Exit on test failure and continue from last failing
|
||||
test next time
|
||||
--sw-skip, --stepwise-skip
|
||||
@@ -1904,8 +1943,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--strict-markers Markers not registered in the `markers` section of
|
||||
the configuration file raise errors
|
||||
--strict (Deprecated) alias to --strict-markers
|
||||
-c file Load configuration from `file` instead of trying to
|
||||
locate one of the implicit configuration files
|
||||
-c FILE, --config-file=FILE
|
||||
Load configuration from `FILE` instead of trying to
|
||||
locate one of the implicit configuration files.
|
||||
--continue-on-collection-errors
|
||||
Force test execution even if collection errors occur
|
||||
--rootdir=ROOTDIR Define root directory for tests. Can be relative
|
||||
@@ -1996,7 +2036,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
Auto-indent multiline messages passed to the logging
|
||||
module. Accepts true|on, false|off or an integer.
|
||||
--log-disable=LOGGER_DISABLE
|
||||
Disable a logger by name. Can be passed multipe
|
||||
Disable a logger by name. Can be passed multiple
|
||||
times.
|
||||
|
||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||
|
||||
@@ -31,10 +31,16 @@ class InvalidFeatureRelease(Exception):
|
||||
SLUG = "pytest-dev/pytest"
|
||||
|
||||
PR_BODY = """\
|
||||
Created automatically from manual trigger.
|
||||
Created by the [prepare release pr](https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml)
|
||||
workflow.
|
||||
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
Once all builds pass and it has been **approved** by one or more maintainers,
|
||||
start the [deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
|
||||
|
||||
* `Use workflow from`: `release-{version}`.
|
||||
* `Release version`: `{version}`.
|
||||
|
||||
After the `deploy` workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ def main():
|
||||
Platform agnostic wrapper script for towncrier.
|
||||
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
|
||||
"""
|
||||
with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file:
|
||||
with open(
|
||||
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"
|
||||
) as draft_file:
|
||||
return call(("towncrier", "--draft"), stdout=draft_file)
|
||||
|
||||
|
||||
|
||||
@@ -13,11 +13,25 @@ from tqdm import tqdm
|
||||
FILE_HEAD = r"""
|
||||
.. _plugin-list:
|
||||
|
||||
Plugin List
|
||||
===========
|
||||
Pytest Plugin List
|
||||
==================
|
||||
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
Packages classified as inactive are excluded.
|
||||
|
||||
For detailed insights into how this list is generated,
|
||||
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
|
||||
.. warning::
|
||||
|
||||
Please be aware that this list is not a curated collection of projects
|
||||
and does not undergo a systematic review process.
|
||||
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
|
||||
|
||||
Do not presume any endorsement from the ``pytest`` project or its developers,
|
||||
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically. Packages classified as inactive are excluded.
|
||||
|
||||
.. The following conditional uses a different format for this list when
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
@@ -33,6 +47,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||
"Development Status :: 6 - Mature",
|
||||
"Development Status :: 7 - Inactive",
|
||||
)
|
||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||
"logassert",
|
||||
}
|
||||
|
||||
|
||||
def escape_rst(text: str) -> str:
|
||||
@@ -52,18 +69,18 @@ def iter_plugins():
|
||||
regex = r">([\d\w-]*)</a>"
|
||||
response = requests.get("https://pypi.org/simple")
|
||||
|
||||
matches = list(
|
||||
match
|
||||
for match in re.finditer(regex, response.text)
|
||||
if match.groups()[0].startswith("pytest-")
|
||||
)
|
||||
match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
|
||||
plugin_names = [
|
||||
name
|
||||
for name in match_names
|
||||
if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
]
|
||||
|
||||
for match in tqdm(matches, smoothing=0):
|
||||
name = match.groups()[0]
|
||||
for name in tqdm(plugin_names, smoothing=0):
|
||||
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
||||
if response.status_code == 404:
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
|
||||
# return 404 on the JSON API. Skip.
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
|
||||
# but return 404 on the JSON API. Skip.
|
||||
continue
|
||||
response.raise_for_status()
|
||||
info = response.json()["info"]
|
||||
|
||||
@@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
|
||||
url = https://docs.pytest.org/en/latest/
|
||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
license_files = LICENSE
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
classifiers =
|
||||
Development Status :: 6 - Mature
|
||||
@@ -22,6 +22,7 @@ classifiers =
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Utilities
|
||||
@@ -73,6 +74,7 @@ testing =
|
||||
nose
|
||||
pygments>=2.7.2
|
||||
requests
|
||||
setuptools
|
||||
xmlschema
|
||||
|
||||
[options.package_data]
|
||||
|
||||
@@ -31,7 +31,6 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
from weakref import ref
|
||||
|
||||
import pluggy
|
||||
|
||||
@@ -50,9 +49,9 @@ from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
from typing_extensions import Literal
|
||||
from typing_extensions import SupportsIndex
|
||||
from weakref import ReferenceType
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
|
||||
@@ -194,25 +193,25 @@ class Frame:
|
||||
class TracebackEntry:
|
||||
"""A single entry in a Traceback."""
|
||||
|
||||
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
|
||||
__slots__ = ("_rawentry", "_repr_style")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rawentry: TracebackType,
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
repr_style: Optional['Literal["short", "long"]'] = None,
|
||||
) -> None:
|
||||
self._rawentry = rawentry
|
||||
self._excinfo = excinfo
|
||||
self._repr_style: Optional['Literal["short", "long"]'] = None
|
||||
self._rawentry: "Final" = rawentry
|
||||
self._repr_style: "Final" = repr_style
|
||||
|
||||
def with_repr_style(
|
||||
self, repr_style: Optional['Literal["short", "long"]']
|
||||
) -> "TracebackEntry":
|
||||
return TracebackEntry(self._rawentry, repr_style)
|
||||
|
||||
@property
|
||||
def lineno(self) -> int:
|
||||
return self._rawentry.tb_lineno - 1
|
||||
|
||||
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
|
||||
assert mode in ("short", "long")
|
||||
self._repr_style = mode
|
||||
|
||||
@property
|
||||
def frame(self) -> Frame:
|
||||
return Frame(self._rawentry.tb_frame)
|
||||
@@ -272,7 +271,7 @@ class TracebackEntry:
|
||||
|
||||
source = property(getsource)
|
||||
|
||||
def ishidden(self) -> bool:
|
||||
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
|
||||
"""Return True if the current frame has a var __tracebackhide__
|
||||
resolving to True.
|
||||
|
||||
@@ -296,7 +295,7 @@ class TracebackEntry:
|
||||
else:
|
||||
break
|
||||
if tbh and callable(tbh):
|
||||
return tbh(None if self._excinfo is None else self._excinfo())
|
||||
return tbh(excinfo)
|
||||
return tbh
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -329,16 +328,14 @@ class Traceback(List[TracebackEntry]):
|
||||
def __init__(
|
||||
self,
|
||||
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
) -> None:
|
||||
"""Initialize from given python traceback object and ExceptionInfo."""
|
||||
self._excinfo = excinfo
|
||||
if isinstance(tb, TracebackType):
|
||||
|
||||
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
||||
cur_: Optional[TracebackType] = cur
|
||||
while cur_ is not None:
|
||||
yield TracebackEntry(cur_, excinfo=excinfo)
|
||||
yield TracebackEntry(cur_)
|
||||
cur_ = cur_.tb_next
|
||||
|
||||
super().__init__(f(tb))
|
||||
@@ -378,7 +375,7 @@ class Traceback(List[TracebackEntry]):
|
||||
continue
|
||||
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
|
||||
continue
|
||||
return Traceback(x._rawentry, self._excinfo)
|
||||
return Traceback(x._rawentry)
|
||||
return self
|
||||
|
||||
@overload
|
||||
@@ -398,26 +395,27 @@ class Traceback(List[TracebackEntry]):
|
||||
return super().__getitem__(key)
|
||||
|
||||
def filter(
|
||||
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
|
||||
self,
|
||||
# TODO(py38): change to positional only.
|
||||
_excinfo_or_fn: Union[
|
||||
"ExceptionInfo[BaseException]",
|
||||
Callable[[TracebackEntry], bool],
|
||||
],
|
||||
) -> "Traceback":
|
||||
"""Return a Traceback instance with certain items removed
|
||||
"""Return a Traceback instance with certain items removed.
|
||||
|
||||
fn is a function that gets a single argument, a TracebackEntry
|
||||
instance, and should return True when the item should be added
|
||||
to the Traceback, False when not.
|
||||
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
|
||||
which are hidden (see ishidden() above).
|
||||
|
||||
By default this removes all the TracebackEntries which are hidden
|
||||
(see ishidden() above).
|
||||
Otherwise, the filter is a function that gets a single argument, a
|
||||
``TracebackEntry`` instance, and should return True when the item should
|
||||
be added to the ``Traceback``, False when not.
|
||||
"""
|
||||
return Traceback(filter(fn, self), self._excinfo)
|
||||
|
||||
def getcrashentry(self) -> Optional[TracebackEntry]:
|
||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
||||
for i in range(-1, -len(self) - 1, -1):
|
||||
entry = self[i]
|
||||
if not entry.ishidden():
|
||||
return entry
|
||||
return None
|
||||
if isinstance(_excinfo_or_fn, ExceptionInfo):
|
||||
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
|
||||
else:
|
||||
fn = _excinfo_or_fn
|
||||
return Traceback(filter(fn, self))
|
||||
|
||||
def recursionindex(self) -> Optional[int]:
|
||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||
@@ -469,22 +467,41 @@ class ExceptionInfo(Generic[E]):
|
||||
self._traceback = traceback
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
def from_exception(
|
||||
cls,
|
||||
exc_info: Tuple[Type[E], E, TracebackType],
|
||||
# Ignoring error: "Cannot use a covariant type variable as a parameter".
|
||||
# This is OK to ignore because this class is (conceptually) readonly.
|
||||
# See https://github.com/python/mypy/issues/7049.
|
||||
exception: E, # type: ignore[misc]
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[E]":
|
||||
"""Return an ExceptionInfo for an existing exc_info tuple.
|
||||
"""Return an ExceptionInfo for an existing exception.
|
||||
|
||||
.. warning::
|
||||
|
||||
Experimental API
|
||||
The exception must have a non-``None`` ``__traceback__`` attribute,
|
||||
otherwise this function fails with an assertion error. This means that
|
||||
the exception must have been raised, or added a traceback with the
|
||||
:py:meth:`~BaseException.with_traceback()` method.
|
||||
|
||||
:param exprinfo:
|
||||
A text string helping to determine if we should strip
|
||||
``AssertionError`` from the output. Defaults to the exception
|
||||
message/``__str__()``.
|
||||
|
||||
.. versionadded:: 7.4
|
||||
"""
|
||||
assert (
|
||||
exception.__traceback__
|
||||
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
||||
exc_info = (type(exception), exception, exception.__traceback__)
|
||||
return cls.from_exc_info(exc_info, exprinfo)
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
cls,
|
||||
exc_info: Tuple[Type[E], E, TracebackType],
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[E]":
|
||||
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
|
||||
_striptext = ""
|
||||
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
||||
exprinfo = getattr(exc_info[1], "msg", None)
|
||||
@@ -563,7 +580,7 @@ class ExceptionInfo(Generic[E]):
|
||||
def traceback(self) -> Traceback:
|
||||
"""The traceback."""
|
||||
if self._traceback is None:
|
||||
self._traceback = Traceback(self.tb, excinfo=ref(self))
|
||||
self._traceback = Traceback(self.tb)
|
||||
return self._traceback
|
||||
|
||||
@traceback.setter
|
||||
@@ -603,11 +620,14 @@ class ExceptionInfo(Generic[E]):
|
||||
return isinstance(self.value, exc)
|
||||
|
||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||
exconly = self.exconly(tryshort=True)
|
||||
entry = self.traceback.getcrashentry()
|
||||
if entry:
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
# Find last non-hidden traceback entry that led to the exception of the
|
||||
# traceback, or None if all hidden.
|
||||
for i in range(-1, -len(self.traceback) - 1, -1):
|
||||
entry = self.traceback[i]
|
||||
if not entry.ishidden(self):
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
exconly = self.exconly(tryshort=True)
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
return None
|
||||
|
||||
def getrepr(
|
||||
@@ -615,7 +635,9 @@ class ExceptionInfo(Generic[E]):
|
||||
showlocals: bool = False,
|
||||
style: "_TracebackStyle" = "long",
|
||||
abspath: bool = False,
|
||||
tbfilter: bool = True,
|
||||
tbfilter: Union[
|
||||
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||
] = True,
|
||||
funcargs: bool = False,
|
||||
truncate_locals: bool = True,
|
||||
chain: bool = True,
|
||||
@@ -627,14 +649,20 @@ class ExceptionInfo(Generic[E]):
|
||||
Ignored if ``style=="native"``.
|
||||
|
||||
:param str style:
|
||||
long|short|no|native|value traceback style.
|
||||
long|short|line|no|native|value traceback style.
|
||||
|
||||
:param bool abspath:
|
||||
If paths should be changed to absolute or left unchanged.
|
||||
|
||||
:param bool tbfilter:
|
||||
Hide entries that contain a local variable ``__tracebackhide__==True``.
|
||||
Ignored if ``style=="native"``.
|
||||
:param tbfilter:
|
||||
A filter for traceback entries.
|
||||
|
||||
* If false, don't hide any entries.
|
||||
* If true, hide internal entries and entries that contain a local
|
||||
variable ``__tracebackhide__ = True``.
|
||||
* If a callable, delegates the filtering to the callable.
|
||||
|
||||
Ignored if ``style`` is ``"native"``.
|
||||
|
||||
:param bool funcargs:
|
||||
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
||||
@@ -653,7 +681,9 @@ class ExceptionInfo(Generic[E]):
|
||||
return ReprExceptionInfo(
|
||||
reprtraceback=ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
self.type, self.value, self.traceback[0]._rawentry
|
||||
self.type,
|
||||
self.value,
|
||||
self.traceback[0]._rawentry if self.traceback else None,
|
||||
)
|
||||
),
|
||||
reprcrash=self._getreprcrash(),
|
||||
@@ -697,7 +727,7 @@ class FormattedExcinfo:
|
||||
showlocals: bool = False
|
||||
style: "_TracebackStyle" = "long"
|
||||
abspath: bool = True
|
||||
tbfilter: bool = True
|
||||
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||
funcargs: bool = False
|
||||
truncate_locals: bool = True
|
||||
chain: bool = True
|
||||
@@ -809,12 +839,16 @@ class FormattedExcinfo:
|
||||
|
||||
def repr_traceback_entry(
|
||||
self,
|
||||
entry: TracebackEntry,
|
||||
entry: Optional[TracebackEntry],
|
||||
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
||||
) -> "ReprEntry":
|
||||
lines: List[str] = []
|
||||
style = entry._repr_style if entry._repr_style is not None else self.style
|
||||
if style in ("short", "long"):
|
||||
style = (
|
||||
entry._repr_style
|
||||
if entry is not None and entry._repr_style is not None
|
||||
else self.style
|
||||
)
|
||||
if style in ("short", "long") and entry is not None:
|
||||
source = self._getentrysource(entry)
|
||||
if source is None:
|
||||
source = Source("???")
|
||||
@@ -855,25 +889,31 @@ class FormattedExcinfo:
|
||||
|
||||
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
|
||||
traceback = excinfo.traceback
|
||||
if self.tbfilter:
|
||||
traceback = traceback.filter()
|
||||
if callable(self.tbfilter):
|
||||
traceback = self.tbfilter(excinfo)
|
||||
elif self.tbfilter:
|
||||
traceback = traceback.filter(excinfo)
|
||||
|
||||
if isinstance(excinfo.value, RecursionError):
|
||||
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
||||
else:
|
||||
extraline = None
|
||||
|
||||
if not traceback:
|
||||
if extraline is None:
|
||||
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
|
||||
entries = [self.repr_traceback_entry(None, excinfo)]
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
last = traceback[-1]
|
||||
entries = []
|
||||
if self.style == "value":
|
||||
reprentry = self.repr_traceback_entry(last, excinfo)
|
||||
entries.append(reprentry)
|
||||
entries = [self.repr_traceback_entry(last, excinfo)]
|
||||
return ReprTraceback(entries, None, style=self.style)
|
||||
|
||||
for index, entry in enumerate(traceback):
|
||||
einfo = (last == entry) and excinfo or None
|
||||
reprentry = self.repr_traceback_entry(entry, einfo)
|
||||
entries.append(reprentry)
|
||||
entries = [
|
||||
self.repr_traceback_entry(entry, excinfo if last == entry else None)
|
||||
for entry in traceback
|
||||
]
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
def _truncate_recursive_traceback(
|
||||
@@ -930,6 +970,7 @@ class FormattedExcinfo:
|
||||
seen: Set[int] = set()
|
||||
while e is not None and id(e) not in seen:
|
||||
seen.add(id(e))
|
||||
|
||||
if excinfo_:
|
||||
# Fall back to native traceback as a temporary workaround until
|
||||
# full support for exception groups added to ExceptionInfo.
|
||||
@@ -946,14 +987,7 @@ class FormattedExcinfo:
|
||||
)
|
||||
else:
|
||||
reprtraceback = self.repr_traceback(excinfo_)
|
||||
|
||||
# will be None if all traceback entries are hidden
|
||||
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
|
||||
if reprcrash:
|
||||
if self.style == "value":
|
||||
repr_chain += [(reprtraceback, None, descr)]
|
||||
else:
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
reprcrash = excinfo_._getreprcrash()
|
||||
else:
|
||||
# Fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work.
|
||||
@@ -961,25 +995,17 @@ class FormattedExcinfo:
|
||||
traceback.format_exception(type(e), e, None)
|
||||
)
|
||||
reprcrash = None
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
|
||||
if e.__cause__ is not None and self.chain:
|
||||
e = e.__cause__
|
||||
excinfo_ = (
|
||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||
descr = "The above exception was the direct cause of the following exception:"
|
||||
elif (
|
||||
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
||||
):
|
||||
e = e.__context__
|
||||
excinfo_ = (
|
||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||
descr = "During handling of the above exception, another exception occurred:"
|
||||
else:
|
||||
e = None
|
||||
@@ -1158,8 +1184,8 @@ class ReprEntry(TerminalRepr):
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
if self.style == "short":
|
||||
assert self.reprfileloc is not None
|
||||
self.reprfileloc.toterminal(tw)
|
||||
if self.reprfileloc:
|
||||
self.reprfileloc.toterminal(tw)
|
||||
self._write_entry_lines(tw)
|
||||
if self.reprlocals:
|
||||
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||
|
||||
@@ -953,7 +953,7 @@ class LocalPath:
|
||||
else:
|
||||
p.dirpath()._ensuredirs()
|
||||
if not p.check(file=1):
|
||||
p.open("w").close()
|
||||
p.open("wb").close()
|
||||
return p
|
||||
|
||||
@overload
|
||||
|
||||
@@ -13,6 +13,7 @@ import struct
|
||||
import sys
|
||||
import tokenize
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
@@ -46,8 +47,18 @@ if TYPE_CHECKING:
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
namedExpr = ast.NamedExpr
|
||||
astNameConstant = ast.Constant
|
||||
astStr = ast.Constant
|
||||
astNum = ast.Constant
|
||||
else:
|
||||
namedExpr = ast.Expr
|
||||
astNameConstant = ast.NameConstant
|
||||
astStr = ast.Str
|
||||
astNum = ast.Num
|
||||
|
||||
|
||||
class Sentinel:
|
||||
pass
|
||||
|
||||
|
||||
assertstate_key = StashKey["AssertionState"]()
|
||||
@@ -57,6 +68,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
# Special marker that denotes we have just left a scope definition
|
||||
_SCOPE_END_MARKER = Sentinel()
|
||||
|
||||
|
||||
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
||||
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||
@@ -639,6 +653,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
.push_format_context() and .pop_format_context() which allows
|
||||
to build another %-formatted string while already building one.
|
||||
|
||||
:scope: A tuple containing the current scope used for variables_overwrite.
|
||||
|
||||
:variables_overwrite: A dict filled with references to variables
|
||||
that change value within an assert. This happens when a variable is
|
||||
reassigned with the walrus operator
|
||||
@@ -660,7 +676,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
else:
|
||||
self.enable_assertion_pass_hook = False
|
||||
self.source = source
|
||||
self.variables_overwrite: Dict[str, str] = {}
|
||||
self.scope: tuple[ast.AST, ...] = ()
|
||||
self.variables_overwrite: defaultdict[
|
||||
tuple[ast.AST, ...], Dict[str, str]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def run(self, mod: ast.Module) -> None:
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
@@ -680,9 +699,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if (
|
||||
expect_docstring
|
||||
and isinstance(item, ast.Expr)
|
||||
and isinstance(item.value, ast.Str)
|
||||
and isinstance(item.value, astStr)
|
||||
):
|
||||
doc = item.value.s
|
||||
if sys.version_info >= (3, 8):
|
||||
doc = item.value.value
|
||||
else:
|
||||
doc = item.value.s
|
||||
if self.is_rewrite_disabled(doc):
|
||||
return
|
||||
expect_docstring = False
|
||||
@@ -723,9 +745,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
mod.body[pos:pos] = imports
|
||||
|
||||
# Collect asserts.
|
||||
nodes: List[ast.AST] = [mod]
|
||||
self.scope = (mod,)
|
||||
nodes: List[Union[ast.AST, Sentinel]] = [mod]
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
self.scope = tuple((*self.scope, node))
|
||||
nodes.append(_SCOPE_END_MARKER)
|
||||
if node == _SCOPE_END_MARKER:
|
||||
self.scope = self.scope[:-1]
|
||||
continue
|
||||
assert isinstance(node, ast.AST)
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new: List[ast.AST] = []
|
||||
@@ -814,7 +844,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
keys = [ast.Str(key) for key in current.keys()]
|
||||
keys = [astStr(key) for key in current.keys()]
|
||||
format_dict = ast.Dict(keys, list(current.values()))
|
||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||
name = "@py_format" + str(next(self.variable_counter))
|
||||
@@ -868,16 +898,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
|
||||
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||
msg = self.pop_format_context(ast.Str(explanation))
|
||||
msg = self.pop_format_context(astStr(explanation))
|
||||
|
||||
# Failed
|
||||
if assert_.msg:
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
gluestr = "\n>assert "
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
assertmsg = astStr("")
|
||||
gluestr = "assert "
|
||||
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
|
||||
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
|
||||
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
fmt = self.helper("_format_explanation", err_msg)
|
||||
@@ -893,8 +923,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
hook_call_pass = ast.Expr(
|
||||
self.helper(
|
||||
"_call_assertion_pass",
|
||||
ast.Num(assert_.lineno),
|
||||
ast.Str(orig),
|
||||
astNum(assert_.lineno),
|
||||
astStr(orig),
|
||||
fmt_pass,
|
||||
)
|
||||
)
|
||||
@@ -913,7 +943,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
variables = [
|
||||
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||
]
|
||||
clear_format = ast.Assign(variables, ast.NameConstant(None))
|
||||
clear_format = ast.Assign(variables, astNameConstant(None))
|
||||
self.statements.append(clear_format)
|
||||
|
||||
else: # Original assertion rewriting
|
||||
@@ -924,9 +954,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
explanation = "\n>assert " + explanation
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
assertmsg = astStr("")
|
||||
explanation = "assert " + explanation
|
||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
|
||||
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
|
||||
msg = self.pop_format_context(template)
|
||||
fmt = self.helper("_format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
@@ -938,7 +968,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# Clear temporary variables by setting them to None.
|
||||
if self.variables:
|
||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||
clear = ast.Assign(variables, ast.NameConstant(None))
|
||||
clear = ast.Assign(variables, astNameConstant(None))
|
||||
self.statements.append(clear)
|
||||
# Fix locations (line numbers/column offsets).
|
||||
for stmt in self.statements:
|
||||
@@ -952,20 +982,20 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
target_id = name.target.id # type: ignore[attr-defined]
|
||||
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
|
||||
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(target_id))
|
||||
expr = ast.IfExp(test, self.display(name), astStr(target_id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
||||
# Display the repr of the name if it's a local variable or
|
||||
# _should_repr_global_name() thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
|
||||
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
||||
expr = ast.IfExp(test, self.display(name), astStr(name.id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
||||
@@ -996,12 +1026,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
]
|
||||
):
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[v.left.target.id] = pytest_temp
|
||||
self.variables_overwrite[self.scope][
|
||||
v.left.target.id
|
||||
] = v.left # type:ignore[assignment]
|
||||
v.left.target.id = pytest_temp
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||
expl_format = self.pop_format_context(ast.Str(expl))
|
||||
expl_format = self.pop_format_context(astStr(expl))
|
||||
call = ast.Call(app, [expl_format], [])
|
||||
self.expl_stmts.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
@@ -1013,7 +1045,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.statements = body = inner
|
||||
self.statements = save
|
||||
self.expl_stmts = fail_save
|
||||
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
|
||||
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
|
||||
expl = self.pop_format_context(expl_template)
|
||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||
|
||||
@@ -1037,10 +1069,22 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
for arg in call.args:
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
arg = self.variables_overwrite[self.scope][
|
||||
arg.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
for keyword in call.keywords:
|
||||
if isinstance(
|
||||
keyword.value, ast.Name
|
||||
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||
keyword.value = self.variables_overwrite[self.scope][
|
||||
keyword.value.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
@@ -1074,8 +1118,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
||||
self.push_format_context()
|
||||
# We first check if we have overwritten a variable in the previous assert
|
||||
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
||||
comp.left.id = self.variables_overwrite[comp.left.id]
|
||||
if isinstance(
|
||||
comp.left, ast.Name
|
||||
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
|
||||
comp.left = self.variables_overwrite[self.scope][
|
||||
comp.left.id
|
||||
] # type:ignore[assignment]
|
||||
if isinstance(comp.left, namedExpr):
|
||||
self.variables_overwrite[self.scope][
|
||||
comp.left.target.id
|
||||
] = comp.left # type:ignore[assignment]
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = f"({left_expl})"
|
||||
@@ -1093,15 +1145,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
and next_operand.target.id == left_res.id
|
||||
):
|
||||
next_operand.target.id = self.variable()
|
||||
self.variables_overwrite[left_res.id] = next_operand.target.id
|
||||
self.variables_overwrite[self.scope][
|
||||
left_res.id
|
||||
] = next_operand # type:ignore[assignment]
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = f"({next_expl})"
|
||||
results.append(next_res)
|
||||
sym = BINOP_MAP[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
syms.append(astStr(sym))
|
||||
expl = f"{left_expl} {sym} {next_expl}"
|
||||
expls.append(ast.Str(expl))
|
||||
expls.append(astStr(expl))
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
|
||||
@@ -27,7 +27,7 @@ from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import Module
|
||||
from _pytest.nodes import File
|
||||
from _pytest.python import Package
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
@@ -179,16 +179,22 @@ class Cache:
|
||||
else:
|
||||
cache_dir_exists_already = self._cachedir.exists()
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
except OSError:
|
||||
self.warn("could not create cache path {path}", path=path, _ispytest=True)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"could not create cache path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
return
|
||||
if not cache_dir_exists_already:
|
||||
self._ensure_supporting_files()
|
||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||
try:
|
||||
f = path.open("w", encoding="UTF-8")
|
||||
except OSError:
|
||||
self.warn("cache could not write path {path}", path=path, _ispytest=True)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"cache could not write path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
else:
|
||||
with f:
|
||||
f.write(data)
|
||||
@@ -213,22 +219,30 @@ class LFPluginCollWrapper:
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||
if isinstance(collector, Session):
|
||||
if isinstance(collector, (Session, Package)):
|
||||
out = yield
|
||||
res: CollectReport = out.get_result()
|
||||
|
||||
# Sort any lf-paths to the beginning.
|
||||
lf_paths = self.lfplugin._last_failed_paths
|
||||
|
||||
# Use stable sort to priorize last failed.
|
||||
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
|
||||
# Package.path is the __init__.py file, we need the directory.
|
||||
if isinstance(node, Package):
|
||||
path = node.path.parent
|
||||
else:
|
||||
path = node.path
|
||||
return path in lf_paths
|
||||
|
||||
res.result = sorted(
|
||||
res.result,
|
||||
# use stable sort to priorize last failed
|
||||
key=lambda x: x.path in lf_paths,
|
||||
key=sort_key,
|
||||
reverse=True,
|
||||
)
|
||||
return
|
||||
|
||||
elif isinstance(collector, Module):
|
||||
elif isinstance(collector, File):
|
||||
if collector.path in self.lfplugin._last_failed_paths:
|
||||
out = yield
|
||||
res = out.get_result()
|
||||
@@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
|
||||
def pytest_make_collect_report(
|
||||
self, collector: nodes.Collector
|
||||
) -> Optional[CollectReport]:
|
||||
# Packages are Modules, but _last_failed_paths only contains
|
||||
# test-bearing paths and doesn't try to include the paths of their
|
||||
# packages, so don't filter them.
|
||||
if isinstance(collector, Module) and not isinstance(collector, Package):
|
||||
# Packages are Files, but we only want to skip test-bearing Files,
|
||||
# so don't filter Packages.
|
||||
if isinstance(collector, File) and not isinstance(collector, Package):
|
||||
if collector.path not in self.lfplugin._last_failed_paths:
|
||||
self.lfplugin._skipped_files += 1
|
||||
|
||||
@@ -299,9 +312,14 @@ class LFPlugin:
|
||||
)
|
||||
|
||||
def get_last_failed_paths(self) -> Set[Path]:
|
||||
"""Return a set with all Paths()s of the previously failed nodeids."""
|
||||
"""Return a set with all Paths of the previously failed nodeids and
|
||||
their parents."""
|
||||
rootpath = self.config.rootpath
|
||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
||||
result = set()
|
||||
for nodeid in self.lastfailed:
|
||||
path = rootpath / nodeid.split("::")[0]
|
||||
result.add(path)
|
||||
result.update(path.parents)
|
||||
return {x for x in result if x.exists()}
|
||||
|
||||
def pytest_report_collectionfinish(self) -> Optional[str]:
|
||||
@@ -487,7 +505,11 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
dest="last_failed_no_failures",
|
||||
choices=("all", "none"),
|
||||
default="all",
|
||||
help="Which tests to run with no previously (known) failures",
|
||||
help="With ``--lf``, determines whether to execute tests when there "
|
||||
"are no previously (known) failures or when no "
|
||||
"cached ``lastfailed`` data was found. "
|
||||
"``all`` (the default) runs the full test suite again. "
|
||||
"``none`` just emits a message about no known failures and exits successfully.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ class DontReadFromInput(TextIO):
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||
|
||||
def truncate(self, size: Optional[int] = None) -> int:
|
||||
raise UnsupportedOperation("cannont truncate stdin")
|
||||
raise UnsupportedOperation("cannot truncate stdin")
|
||||
|
||||
def write(self, data: str) -> int:
|
||||
raise UnsupportedOperation("cannot write to stdin")
|
||||
|
||||
@@ -380,15 +380,24 @@ else:
|
||||
|
||||
|
||||
def get_user_id() -> int | None:
|
||||
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
|
||||
# win32 does not have a getuid() function.
|
||||
# On Emscripten, getuid() is a stub that always returns 0.
|
||||
if sys.platform in ("win32", "emscripten"):
|
||||
"""Return the current process's real user id or None if it could not be
|
||||
determined.
|
||||
|
||||
:return: The user id or None if it could not be determined.
|
||||
"""
|
||||
# mypy follows the version and platform checking expectation of PEP 484:
|
||||
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||
if sys.platform == "win32" or sys.platform == "emscripten":
|
||||
# win32 does not have a getuid() function.
|
||||
# Emscripten has a return 0 stub.
|
||||
return None
|
||||
# getuid shouldn't fail, but cpython defines such a case.
|
||||
# Let's hope for the best.
|
||||
uid = os.getuid()
|
||||
return uid if uid != -1 else None
|
||||
else:
|
||||
# On other platforms, a return value of -1 is assumed to indicate that
|
||||
# the current process's real user id could not be determined.
|
||||
ERROR = -1
|
||||
uid = os.getuid()
|
||||
return uid if uid != ERROR else None
|
||||
|
||||
|
||||
# Perform exhaustiveness checking.
|
||||
|
||||
@@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import importlib_metadata
|
||||
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import absolutepath
|
||||
@@ -57,6 +57,7 @@ from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import resolve_package_path
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import warn_explicit_for
|
||||
@@ -137,7 +138,9 @@ def main(
|
||||
) -> Union[int, ExitCode]:
|
||||
"""Perform an in-process test run.
|
||||
|
||||
:param args: List of command line arguments.
|
||||
:param args:
|
||||
List of command line arguments. If `None` or not given, defaults to reading
|
||||
arguments directly from the process command line (:data:`sys.argv`).
|
||||
:param plugins: List of plugin objects to be auto-registered during initialization.
|
||||
|
||||
:returns: An exit code.
|
||||
@@ -442,10 +445,10 @@ class PytestPluginManager(PluginManager):
|
||||
# so we avoid accessing possibly non-readable attributes
|
||||
# (see issue #1073).
|
||||
if not name.startswith("pytest_"):
|
||||
return
|
||||
return None
|
||||
# Ignore names which can not be hooks.
|
||||
if name == "pytest_plugins":
|
||||
return
|
||||
return None
|
||||
|
||||
opts = super().parse_hookimpl_opts(plugin, name)
|
||||
if opts is not None:
|
||||
@@ -454,9 +457,9 @@ class PytestPluginManager(PluginManager):
|
||||
method = getattr(plugin, name)
|
||||
# Consider only actual functions for hooks (#3775).
|
||||
if not inspect.isroutine(method):
|
||||
return
|
||||
return None
|
||||
# Collect unmarked hooks as long as they have the `pytest_' prefix.
|
||||
return _get_legacy_hook_marks(
|
||||
return _get_legacy_hook_marks( # type: ignore[return-value]
|
||||
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
|
||||
)
|
||||
|
||||
@@ -465,7 +468,7 @@ class PytestPluginManager(PluginManager):
|
||||
if opts is None:
|
||||
method = getattr(module_or_class, name)
|
||||
if name.startswith("pytest_"):
|
||||
opts = _get_legacy_hook_marks(
|
||||
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
||||
method,
|
||||
"spec",
|
||||
("firstresult", "historic"),
|
||||
@@ -526,7 +529,13 @@ class PytestPluginManager(PluginManager):
|
||||
# Internal API for local conftest plugin handling.
|
||||
#
|
||||
def _set_initial_conftests(
|
||||
self, namespace: argparse.Namespace, rootpath: Path
|
||||
self,
|
||||
args: Sequence[Union[str, Path]],
|
||||
pyargs: bool,
|
||||
noconftest: bool,
|
||||
rootpath: Path,
|
||||
confcutdir: Optional[Path],
|
||||
importmode: Union[ImportMode, str],
|
||||
) -> None:
|
||||
"""Load initial conftest files given a preparsed "namespace".
|
||||
|
||||
@@ -536,27 +545,25 @@ class PytestPluginManager(PluginManager):
|
||||
common options will not confuse our logic here.
|
||||
"""
|
||||
current = Path.cwd()
|
||||
self._confcutdir = (
|
||||
absolutepath(current / namespace.confcutdir)
|
||||
if namespace.confcutdir
|
||||
else None
|
||||
)
|
||||
self._noconftest = namespace.noconftest
|
||||
self._using_pyargs = namespace.pyargs
|
||||
testpaths = namespace.file_or_dir
|
||||
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
|
||||
self._noconftest = noconftest
|
||||
self._using_pyargs = pyargs
|
||||
foundanchor = False
|
||||
for testpath in testpaths:
|
||||
path = str(testpath)
|
||||
for intitial_path in args:
|
||||
path = str(intitial_path)
|
||||
# remove node-id syntax
|
||||
i = path.find("::")
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
anchor = absolutepath(current / path)
|
||||
if anchor.exists(): # we found some file object
|
||||
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
||||
|
||||
# Ensure we do not break if what appears to be an anchor
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
if safe_exists(anchor):
|
||||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(current, namespace.importmode, rootpath)
|
||||
self._try_load_conftest(current, importmode, rootpath)
|
||||
|
||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||
"""Whether a path is within the confcutdir.
|
||||
@@ -1055,9 +1062,10 @@ class Config:
|
||||
fin()
|
||||
|
||||
def get_terminal_writer(self) -> TerminalWriter:
|
||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
||||
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
|
||||
"terminalreporter"
|
||||
)
|
||||
assert terminalreporter is not None
|
||||
return terminalreporter._tw
|
||||
|
||||
def pytest_cmdline_parse(
|
||||
@@ -1130,8 +1138,25 @@ class Config:
|
||||
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
||||
# We haven't fully parsed the command line arguments yet, so
|
||||
# early_config.args it not set yet. But we need it for
|
||||
# discovering the initial conftests. So "pre-run" the logic here.
|
||||
# It will be done for real in `parse()`.
|
||||
args, args_source = early_config._decide_args(
|
||||
args=early_config.known_args_namespace.file_or_dir,
|
||||
pyargs=early_config.known_args_namespace.pyargs,
|
||||
testpaths=early_config.getini("testpaths"),
|
||||
invocation_dir=early_config.invocation_params.dir,
|
||||
rootpath=early_config.rootpath,
|
||||
warn=False,
|
||||
)
|
||||
self.pluginmanager._set_initial_conftests(
|
||||
early_config.known_args_namespace, rootpath=early_config.rootpath
|
||||
args=args,
|
||||
pyargs=early_config.known_args_namespace.pyargs,
|
||||
noconftest=early_config.known_args_namespace.noconftest,
|
||||
rootpath=early_config.rootpath,
|
||||
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||
importmode=early_config.known_args_namespace.importmode,
|
||||
)
|
||||
|
||||
def _initini(self, args: Sequence[str]) -> None:
|
||||
@@ -1211,6 +1236,49 @@ class Config:
|
||||
|
||||
return args
|
||||
|
||||
def _decide_args(
|
||||
self,
|
||||
*,
|
||||
args: List[str],
|
||||
pyargs: List[str],
|
||||
testpaths: List[str],
|
||||
invocation_dir: Path,
|
||||
rootpath: Path,
|
||||
warn: bool,
|
||||
) -> Tuple[List[str], ArgsSource]:
|
||||
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
||||
|
||||
:param warn: Whether can issue warnings.
|
||||
"""
|
||||
if args:
|
||||
source = Config.ArgsSource.ARGS
|
||||
result = args
|
||||
else:
|
||||
if invocation_dir == rootpath:
|
||||
source = Config.ArgsSource.TESTPATHS
|
||||
if pyargs:
|
||||
result = testpaths
|
||||
else:
|
||||
result = []
|
||||
for path in testpaths:
|
||||
result.extend(sorted(glob.iglob(path, recursive=True)))
|
||||
if testpaths and not result:
|
||||
if warn:
|
||||
warning_text = (
|
||||
"No files were found in testpaths; "
|
||||
"consider removing or adjusting your testpaths configuration. "
|
||||
"Searching recursively from the current directory instead."
|
||||
)
|
||||
self.issue_config_time_warning(
|
||||
PytestConfigWarning(warning_text), stacklevel=3
|
||||
)
|
||||
else:
|
||||
result = []
|
||||
if not result:
|
||||
source = Config.ArgsSource.INCOVATION_DIR
|
||||
result = [str(invocation_dir)]
|
||||
return result, source
|
||||
|
||||
def _preparse(self, args: List[str], addopts: bool = True) -> None:
|
||||
if addopts:
|
||||
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
||||
@@ -1249,8 +1317,11 @@ class Config:
|
||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||
)
|
||||
|
||||
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
if self.known_args_namespace.confcutdir is None:
|
||||
if self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
else:
|
||||
confcutdir = str(self.rootpath)
|
||||
self.known_args_namespace.confcutdir = confcutdir
|
||||
try:
|
||||
self.hook.pytest_load_initial_conftests(
|
||||
@@ -1356,25 +1427,17 @@ class Config:
|
||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||
self._parser.after_preparse = True # type: ignore
|
||||
try:
|
||||
source = Config.ArgsSource.ARGS
|
||||
args = self._parser.parse_setoption(
|
||||
args, self.option, namespace=self.option
|
||||
)
|
||||
if not args:
|
||||
if self.invocation_params.dir == self.rootpath:
|
||||
source = Config.ArgsSource.TESTPATHS
|
||||
testpaths: List[str] = self.getini("testpaths")
|
||||
if self.known_args_namespace.pyargs:
|
||||
args = testpaths
|
||||
else:
|
||||
args = []
|
||||
for path in testpaths:
|
||||
args.extend(sorted(glob.iglob(path, recursive=True)))
|
||||
if not args:
|
||||
source = Config.ArgsSource.INCOVATION_DIR
|
||||
args = [str(self.invocation_params.dir)]
|
||||
self.args = args
|
||||
self.args_source = source
|
||||
self.args, self.args_source = self._decide_args(
|
||||
args=args,
|
||||
pyargs=self.known_args_namespace.pyargs,
|
||||
testpaths=self.getini("testpaths"),
|
||||
invocation_dir=self.invocation_params.dir,
|
||||
rootpath=self.rootpath,
|
||||
warn=True,
|
||||
)
|
||||
except PrintHelp:
|
||||
pass
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from .exceptions import UsageError
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import commonpath
|
||||
from _pytest.pathlib import safe_exists
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Config
|
||||
@@ -151,14 +152,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
|
||||
return path
|
||||
return path.parent
|
||||
|
||||
def safe_exists(path: Path) -> bool:
|
||||
# This can throw on paths that contain characters unrepresentable at the OS level,
|
||||
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
|
||||
try:
|
||||
return path.exists()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# These look like paths but may not exist
|
||||
possible_paths = (
|
||||
absolutepath(get_file_part_from_node_id(arg))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Discover and run doctests in modules and test files."""
|
||||
import bdb
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
@@ -536,6 +537,25 @@ class DoctestModule(Module):
|
||||
tests, obj, name, module, source_lines, globs, seen
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
|
||||
def _from_module(self, module, object):
|
||||
"""`cached_property` objects are never considered a part
|
||||
of the 'current module'. As such they are skipped by doctest.
|
||||
Here we override `_from_module` to check the underlying
|
||||
function instead. https://github.com/python/cpython/issues/107995
|
||||
"""
|
||||
if hasattr(functools, "cached_property") and isinstance(
|
||||
object, functools.cached_property
|
||||
):
|
||||
object = object.func
|
||||
|
||||
# Type ignored because this is a private function.
|
||||
return super()._from_module(module, object) # type: ignore[misc]
|
||||
|
||||
else: # pragma: no cover
|
||||
pass
|
||||
|
||||
if self.path.name == "conftest.py":
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from typing import Generator
|
||||
from typing import TextIO
|
||||
|
||||
import pytest
|
||||
from _pytest.config import Config
|
||||
@@ -11,7 +9,7 @@ from _pytest.nodes import Item
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
fault_handler_stderr_key = StashKey[TextIO]()
|
||||
fault_handler_stderr_fd_key = StashKey[int]()
|
||||
fault_handler_originally_enabled_key = StashKey[bool]()
|
||||
|
||||
|
||||
@@ -26,10 +24,9 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
def pytest_configure(config: Config) -> None:
|
||||
import faulthandler
|
||||
|
||||
stderr_fd_copy = os.dup(get_stderr_fileno())
|
||||
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
||||
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
||||
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
|
||||
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
||||
|
||||
|
||||
def pytest_unconfigure(config: Config) -> None:
|
||||
@@ -37,9 +34,9 @@ def pytest_unconfigure(config: Config) -> None:
|
||||
|
||||
faulthandler.disable()
|
||||
# Close the dup file installed during pytest_configure.
|
||||
if fault_handler_stderr_key in config.stash:
|
||||
config.stash[fault_handler_stderr_key].close()
|
||||
del config.stash[fault_handler_stderr_key]
|
||||
if fault_handler_stderr_fd_key in config.stash:
|
||||
os.close(config.stash[fault_handler_stderr_fd_key])
|
||||
del config.stash[fault_handler_stderr_fd_key]
|
||||
if config.stash.get(fault_handler_originally_enabled_key, False):
|
||||
# Re-enable the faulthandler if it was originally enabled.
|
||||
faulthandler.enable(file=get_stderr_fileno())
|
||||
@@ -53,7 +50,7 @@ def get_stderr_fileno() -> int:
|
||||
if fileno == -1:
|
||||
raise AttributeError()
|
||||
return fileno
|
||||
except (AttributeError, io.UnsupportedOperation):
|
||||
except (AttributeError, ValueError):
|
||||
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||
# This is potentially dangerous, but the best we can do.
|
||||
@@ -67,10 +64,10 @@ def get_timeout_config_value(config: Config) -> float:
|
||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
timeout = get_timeout_config_value(item.config)
|
||||
stderr = item.config.stash[fault_handler_stderr_key]
|
||||
if timeout > 0 and stderr is not None:
|
||||
if timeout > 0:
|
||||
import faulthandler
|
||||
|
||||
stderr = item.config.stash[fault_handler_stderr_fd_key]
|
||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||
try:
|
||||
yield
|
||||
|
||||
@@ -46,6 +46,7 @@ from _pytest.compat import getimfunc
|
||||
from _pytest.compat import getlocation
|
||||
from _pytest.compat import is_generator
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import NotSetType
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.config import _PluggyPlugin
|
||||
@@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
|
||||
session._fixturemanager = FixtureManager(session)
|
||||
|
||||
|
||||
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
|
||||
import pytest
|
||||
def get_scope_package(
|
||||
node: nodes.Item,
|
||||
fixturedef: "FixtureDef[object]",
|
||||
) -> Optional[Union[nodes.Item, nodes.Collector]]:
|
||||
from _pytest.python import Package
|
||||
|
||||
cls = pytest.Package
|
||||
current = node
|
||||
current: Optional[Union[nodes.Item, nodes.Collector]] = node
|
||||
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
|
||||
while current and (
|
||||
type(current) is not cls or fixture_package_name != current.nodeid
|
||||
not isinstance(current, Package) or fixture_package_name != current.nodeid
|
||||
):
|
||||
current = current.parent
|
||||
current = current.parent # type: ignore[assignment]
|
||||
if current is None:
|
||||
return node.session
|
||||
return current
|
||||
@@ -434,7 +437,23 @@ class FixtureRequest:
|
||||
@property
|
||||
def node(self):
|
||||
"""Underlying collection node (depends on current request scope)."""
|
||||
return self._getscopeitem(self._scope)
|
||||
scope = self._scope
|
||||
if scope is Scope.Function:
|
||||
# This might also be a non-function Item despite its attribute name.
|
||||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||
elif scope is Scope.Package:
|
||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||
# but on FixtureRequest (a subclass).
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
||||
else:
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
if node is None and scope is Scope.Class:
|
||||
# Fallback to function item itself.
|
||||
node = self._pyfuncitem
|
||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
||||
scope, self._pyfuncitem
|
||||
)
|
||||
return node
|
||||
|
||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||
@@ -518,11 +537,7 @@ class FixtureRequest:
|
||||
"""Add finalizer/teardown function to be called without arguments after
|
||||
the last test within the requesting test context finished execution."""
|
||||
# XXX usually this method is shadowed by fixturedef specific ones.
|
||||
self._addfinalizer(finalizer, scope=self.scope)
|
||||
|
||||
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
|
||||
node = self._getscopeitem(scope)
|
||||
node.addfinalizer(finalizer)
|
||||
self.node.addfinalizer(finalizer)
|
||||
|
||||
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
||||
"""Apply a marker to a single test function invocation.
|
||||
@@ -717,28 +732,6 @@ class FixtureRequest:
|
||||
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
|
||||
return lines
|
||||
|
||||
def _getscopeitem(
|
||||
self, scope: Union[Scope, "_ScopeName"]
|
||||
) -> Union[nodes.Item, nodes.Collector]:
|
||||
if isinstance(scope, str):
|
||||
scope = Scope(scope)
|
||||
if scope is Scope.Function:
|
||||
# This might also be a non-function Item despite its attribute name.
|
||||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||
elif scope is Scope.Package:
|
||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||
# but on FixtureRequest (a subclass).
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
||||
else:
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
if node is None and scope is Scope.Class:
|
||||
# Fallback to function item itself.
|
||||
node = self._pyfuncitem
|
||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
||||
scope, self._pyfuncitem
|
||||
)
|
||||
return node
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<FixtureRequest for %r>" % (self.node)
|
||||
|
||||
@@ -1593,13 +1586,52 @@ class FixtureManager:
|
||||
# Separate parametrized setups.
|
||||
items[:] = reorder_items(items)
|
||||
|
||||
@overload
|
||||
def parsefactories(
|
||||
self, node_or_obj, nodeid=NOTSET, unittest: bool = False
|
||||
self,
|
||||
node_or_obj: nodes.Node,
|
||||
*,
|
||||
unittest: bool = ...,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@overload
|
||||
def parsefactories( # noqa: F811
|
||||
self,
|
||||
node_or_obj: object,
|
||||
nodeid: Optional[str],
|
||||
*,
|
||||
unittest: bool = ...,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def parsefactories( # noqa: F811
|
||||
self,
|
||||
node_or_obj: Union[nodes.Node, object],
|
||||
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||
*,
|
||||
unittest: bool = False,
|
||||
) -> None:
|
||||
"""Collect fixtures from a collection node or object.
|
||||
|
||||
Found fixtures are parsed into `FixtureDef`s and saved.
|
||||
|
||||
If `node_or_object` is a collection node (with an underlying Python
|
||||
object), the node's object is traversed and the node's nodeid is used to
|
||||
determine the fixtures' visibilty. `nodeid` must not be specified in
|
||||
this case.
|
||||
|
||||
If `node_or_object` is an object (e.g. a plugin), the object is
|
||||
traversed and the given `nodeid` is used to determine the fixtures'
|
||||
visibility. `nodeid` must be specified in this case; None and "" mean
|
||||
total visibility.
|
||||
"""
|
||||
if nodeid is not NOTSET:
|
||||
holderobj = node_or_obj
|
||||
else:
|
||||
holderobj = node_or_obj.obj
|
||||
assert isinstance(node_or_obj, nodes.Node)
|
||||
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
|
||||
assert isinstance(node_or_obj.nodeid, str)
|
||||
nodeid = node_or_obj.nodeid
|
||||
if holderobj in self._holderobjseen:
|
||||
return
|
||||
|
||||
@@ -11,6 +11,7 @@ from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import PrintHelp
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
class HelpAction(Action):
|
||||
@@ -105,7 +106,7 @@ def pytest_cmdline_parse():
|
||||
if config.option.debug:
|
||||
# --debug | --debug <file.log> was provided.
|
||||
path = config.option.debug
|
||||
debugfile = open(path, "w")
|
||||
debugfile = open(path, "w", encoding="utf-8")
|
||||
debugfile.write(
|
||||
"versions pytest-%s, "
|
||||
"python-%s\ncwd=%s\nargs=%s\n\n"
|
||||
@@ -159,7 +160,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
def showhelp(config: Config) -> None:
|
||||
import textwrap
|
||||
|
||||
reporter = config.pluginmanager.get_plugin("terminalreporter")
|
||||
reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
|
||||
"terminalreporter"
|
||||
)
|
||||
assert reporter is not None
|
||||
tw = reporter._tw
|
||||
tw.write(config._parser.optparser.format_help())
|
||||
tw.line()
|
||||
|
||||
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
from _pytest.code import ExceptionInfo
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import PytestPluginManager
|
||||
@@ -41,6 +41,7 @@ if TYPE_CHECKING:
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.terminal import TerminalReporter
|
||||
from _pytest.terminal import TestShortLogReport
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
|
||||
|
||||
@@ -806,7 +807,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_report_teststatus( # type:ignore[empty-body]
|
||||
report: Union["CollectReport", "TestReport"], config: "Config"
|
||||
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
|
||||
) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
|
||||
"""Return result-category, shortletter and verbose word for status
|
||||
reporting.
|
||||
|
||||
|
||||
@@ -502,6 +502,10 @@ class LogXML:
|
||||
# Local hack to handle xdist report order.
|
||||
workernode = getattr(report, "node", None)
|
||||
reporter = self.node_reporters.pop((nodeid, workernode))
|
||||
|
||||
for propname, propvalue in report.user_properties:
|
||||
reporter.add_property(propname, str(propvalue))
|
||||
|
||||
if reporter is not None:
|
||||
reporter.finalize()
|
||||
|
||||
@@ -599,9 +603,6 @@ class LogXML:
|
||||
reporter = self._opentestcase(report)
|
||||
reporter.write_captured_output(report)
|
||||
|
||||
for propname, propvalue in report.user_properties:
|
||||
reporter.add_property(propname, str(propvalue))
|
||||
|
||||
self.finalize(report)
|
||||
report_wid = getattr(report, "worker_id", None)
|
||||
report_ii = getattr(report, "item_index", None)
|
||||
|
||||
@@ -5,7 +5,11 @@ import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from contextlib import nullcontext
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from io import StringIO
|
||||
from logging import LogRecord
|
||||
from pathlib import Path
|
||||
from typing import AbstractSet
|
||||
from typing import Dict
|
||||
@@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str:
|
||||
return _ANSI_ESCAPE_SEQ.sub("", text)
|
||||
|
||||
|
||||
class ColoredLevelFormatter(logging.Formatter):
|
||||
class DatetimeFormatter(logging.Formatter):
|
||||
"""A logging formatter which formats record with
|
||||
:func:`datetime.datetime.strftime` formatter instead of
|
||||
:func:`time.strftime` in case of microseconds in format string.
|
||||
"""
|
||||
|
||||
def formatTime(self, record: LogRecord, datefmt=None) -> str:
|
||||
if datefmt and "%f" in datefmt:
|
||||
ct = self.converter(record.created)
|
||||
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
||||
# Construct `datetime.datetime` object from `struct_time`
|
||||
# and msecs information from `record`
|
||||
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
|
||||
return dt.strftime(datefmt)
|
||||
# Use `logging.Formatter` for non-microsecond formats
|
||||
return super().formatTime(record, datefmt)
|
||||
|
||||
|
||||
class ColoredLevelFormatter(DatetimeFormatter):
|
||||
"""A logging formatter which colorizes the %(levelname)..s part of the
|
||||
log format passed to __init__."""
|
||||
|
||||
@@ -302,7 +324,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
action="append",
|
||||
default=[],
|
||||
dest="logger_disable",
|
||||
help="Disable a logger by name. Can be passed multipe times.",
|
||||
help="Disable a logger by name. Can be passed multiple times.",
|
||||
)
|
||||
|
||||
|
||||
@@ -376,11 +398,12 @@ class LogCaptureFixture:
|
||||
self._initial_handler_level: Optional[int] = None
|
||||
# Dict of log name -> log level.
|
||||
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
||||
self._initial_disabled_logging_level: Optional[int] = None
|
||||
|
||||
def _finalize(self) -> None:
|
||||
"""Finalize the fixture.
|
||||
|
||||
This restores the log levels changed by :meth:`set_level`.
|
||||
This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
|
||||
"""
|
||||
# Restore log levels.
|
||||
if self._initial_handler_level is not None:
|
||||
@@ -388,6 +411,10 @@ class LogCaptureFixture:
|
||||
for logger_name, level in self._initial_logger_levels.items():
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(level)
|
||||
# Disable logging at the original disabled logging level.
|
||||
if self._initial_disabled_logging_level is not None:
|
||||
logging.disable(self._initial_disabled_logging_level)
|
||||
self._initial_disabled_logging_level = None
|
||||
|
||||
@property
|
||||
def handler(self) -> LogCaptureHandler:
|
||||
@@ -453,13 +480,51 @@ class LogCaptureFixture:
|
||||
"""Reset the list of log records and the captured log text."""
|
||||
self.handler.clear()
|
||||
|
||||
def _force_enable_logging(
|
||||
self, level: Union[int, str], logger_obj: logging.Logger
|
||||
) -> int:
|
||||
"""Enable the desired logging level if the global level was disabled via ``logging.disabled``.
|
||||
|
||||
Only enables logging levels greater than or equal to the requested ``level``.
|
||||
|
||||
Does nothing if the desired ``level`` wasn't disabled.
|
||||
|
||||
:param level:
|
||||
The logger level caplog should capture.
|
||||
All logging is enabled if a non-standard logging level string is supplied.
|
||||
Valid level strings are in :data:`logging._nameToLevel`.
|
||||
:param logger_obj: The logger object to check.
|
||||
|
||||
:return: The original disabled logging level.
|
||||
"""
|
||||
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
|
||||
|
||||
if isinstance(level, str):
|
||||
# Try to translate the level string to an int for `logging.disable()`
|
||||
level = logging.getLevelName(level)
|
||||
|
||||
if not isinstance(level, int):
|
||||
# The level provided was not valid, so just un-disable all logging.
|
||||
logging.disable(logging.NOTSET)
|
||||
elif not logger_obj.isEnabledFor(level):
|
||||
# Each level is `10` away from other levels.
|
||||
# https://docs.python.org/3/library/logging.html#logging-levels
|
||||
disable_level = max(level - 10, logging.NOTSET)
|
||||
logging.disable(disable_level)
|
||||
|
||||
return original_disable_level
|
||||
|
||||
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
|
||||
"""Set the level of a logger for the duration of a test.
|
||||
"""Set the threshold level of a logger for the duration of a test.
|
||||
|
||||
Logging messages which are less severe than this level will not be captured.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
The levels of the loggers changed by this function will be
|
||||
restored to their initial values at the end of the test.
|
||||
|
||||
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||
|
||||
:param level: The level.
|
||||
:param logger: The logger to update. If not given, the root logger.
|
||||
"""
|
||||
@@ -470,6 +535,9 @@ class LogCaptureFixture:
|
||||
if self._initial_handler_level is None:
|
||||
self._initial_handler_level = self.handler.level
|
||||
self.handler.setLevel(level)
|
||||
initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
|
||||
if self._initial_disabled_logging_level is None:
|
||||
self._initial_disabled_logging_level = initial_disabled_logging_level
|
||||
|
||||
@contextmanager
|
||||
def at_level(
|
||||
@@ -479,6 +547,8 @@ class LogCaptureFixture:
|
||||
the end of the 'with' statement the level is restored to its original
|
||||
value.
|
||||
|
||||
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||
|
||||
:param level: The level.
|
||||
:param logger: The logger to update. If not given, the root logger.
|
||||
"""
|
||||
@@ -487,11 +557,13 @@ class LogCaptureFixture:
|
||||
logger_obj.setLevel(level)
|
||||
handler_orig_level = self.handler.level
|
||||
self.handler.setLevel(level)
|
||||
original_disable_level = self._force_enable_logging(level, logger_obj)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logger_obj.setLevel(orig_level)
|
||||
self.handler.setLevel(handler_orig_level)
|
||||
logging.disable(original_disable_level)
|
||||
|
||||
|
||||
@fixture
|
||||
@@ -577,7 +649,7 @@ class LoggingPlugin:
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
)
|
||||
|
||||
log_file_formatter = logging.Formatter(
|
||||
log_file_formatter = DatetimeFormatter(
|
||||
log_file_format, datefmt=log_file_date_format
|
||||
)
|
||||
self.log_file_handler.setFormatter(log_file_formatter)
|
||||
@@ -588,6 +660,8 @@ class LoggingPlugin:
|
||||
)
|
||||
if self._log_cli_enabled():
|
||||
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
|
||||
# Guaranteed by `_log_cli_enabled()`.
|
||||
assert terminal_reporter is not None
|
||||
capture_manager = config.pluginmanager.get_plugin("capturemanager")
|
||||
# if capturemanager plugin is disabled, live logging still works.
|
||||
self.log_cli_handler: Union[
|
||||
@@ -621,7 +695,7 @@ class LoggingPlugin:
|
||||
create_terminal_writer(self._config), log_format, log_date_format
|
||||
)
|
||||
else:
|
||||
formatter = logging.Formatter(log_format, log_date_format)
|
||||
formatter = DatetimeFormatter(log_format, log_date_format)
|
||||
|
||||
formatter._style = PercentStyleMultiline(
|
||||
formatter._style._fmt, auto_indent=auto_indent
|
||||
|
||||
@@ -36,6 +36,7 @@ from _pytest.outcomes import exit
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.pathlib import visit
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
@@ -122,11 +123,12 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
)
|
||||
group._addoption(
|
||||
"-c",
|
||||
metavar="file",
|
||||
"--config-file",
|
||||
metavar="FILE",
|
||||
type=str,
|
||||
dest="inifilename",
|
||||
help="Load configuration from `file` instead of trying to locate one of the "
|
||||
"implicit configuration files",
|
||||
help="Load configuration from `FILE` instead of trying to locate one of the "
|
||||
"implicit configuration files.",
|
||||
)
|
||||
group._addoption(
|
||||
"--continue-on-collection-errors",
|
||||
@@ -399,6 +401,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
|
||||
allow_in_venv = config.getoption("collect_in_virtualenv")
|
||||
if not allow_in_venv and _in_venv(collection_path):
|
||||
return True
|
||||
|
||||
if collection_path.is_dir():
|
||||
norecursepatterns = config.getini("norecursedirs")
|
||||
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
|
||||
return True
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -455,6 +463,11 @@ class _bestrelpath_cache(Dict[Path, str]):
|
||||
|
||||
@final
|
||||
class Session(nodes.FSCollector):
|
||||
"""The root of the collection tree.
|
||||
|
||||
``Session`` collects the initial paths given as arguments to pytest.
|
||||
"""
|
||||
|
||||
Interrupted = Interrupted
|
||||
Failed = Failed
|
||||
# Set on the session by runner.pytest_sessionstart.
|
||||
@@ -562,9 +575,6 @@ class Session(nodes.FSCollector):
|
||||
ihook = self.gethookproxy(fspath.parent)
|
||||
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
||||
return False
|
||||
norecursepatterns = self.config.getini("norecursedirs")
|
||||
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _collectfile(
|
||||
@@ -685,8 +695,8 @@ class Session(nodes.FSCollector):
|
||||
# are not collected more than once.
|
||||
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
|
||||
|
||||
# Dirnames of pkgs with dunder-init files.
|
||||
pkg_roots: Dict[str, Package] = {}
|
||||
# Directories of pkgs with dunder-init files.
|
||||
pkg_roots: Dict[Path, Package] = {}
|
||||
|
||||
for argpath, names in self._initial_parts:
|
||||
self.trace("processing argument", (argpath, names))
|
||||
@@ -707,7 +717,7 @@ class Session(nodes.FSCollector):
|
||||
col = self._collectfile(pkginit, handle_dupes=False)
|
||||
if col:
|
||||
if isinstance(col[0], Package):
|
||||
pkg_roots[str(parent)] = col[0]
|
||||
pkg_roots[parent] = col[0]
|
||||
node_cache1[col[0].path] = [col[0]]
|
||||
|
||||
# If it's a directory argument, recurse and look for any Subpackages.
|
||||
@@ -716,7 +726,7 @@ class Session(nodes.FSCollector):
|
||||
assert not names, f"invalid arg {(argpath, names)!r}"
|
||||
|
||||
seen_dirs: Set[Path] = set()
|
||||
for direntry in visit(str(argpath), self._recurse):
|
||||
for direntry in visit(argpath, self._recurse):
|
||||
if not direntry.is_file():
|
||||
continue
|
||||
|
||||
@@ -731,8 +741,8 @@ class Session(nodes.FSCollector):
|
||||
for x in self._collectfile(pkginit):
|
||||
yield x
|
||||
if isinstance(x, Package):
|
||||
pkg_roots[str(dirpath)] = x
|
||||
if str(dirpath) in pkg_roots:
|
||||
pkg_roots[dirpath] = x
|
||||
if dirpath in pkg_roots:
|
||||
# Do not collect packages here.
|
||||
continue
|
||||
|
||||
@@ -749,7 +759,7 @@ class Session(nodes.FSCollector):
|
||||
if argpath in node_cache1:
|
||||
col = node_cache1[argpath]
|
||||
else:
|
||||
collect_root = pkg_roots.get(str(argpath.parent), self)
|
||||
collect_root = pkg_roots.get(argpath.parent, self)
|
||||
col = collect_root._collectfile(argpath, handle_dupes=False)
|
||||
if col:
|
||||
node_cache1[argpath] = col
|
||||
@@ -886,7 +896,7 @@ def resolve_collection_argument(
|
||||
strpath = search_pypath(strpath)
|
||||
fspath = invocation_path / strpath
|
||||
fspath = absolutepath(fspath)
|
||||
if not fspath.exists():
|
||||
if not safe_exists(fspath):
|
||||
msg = (
|
||||
"module or package not found: {arg} (missing __init__.py?)"
|
||||
if as_pypath
|
||||
|
||||
@@ -18,6 +18,7 @@ import ast
|
||||
import dataclasses
|
||||
import enum
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
from typing import Callable
|
||||
from typing import Iterator
|
||||
@@ -26,6 +27,11 @@ from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
astNameConstant = ast.Constant
|
||||
else:
|
||||
astNameConstant = ast.NameConstant
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Expression",
|
||||
@@ -132,7 +138,7 @@ IDENT_PREFIX = "$"
|
||||
|
||||
def expression(s: Scanner) -> ast.Expression:
|
||||
if s.accept(TokenType.EOF):
|
||||
ret: ast.expr = ast.NameConstant(False)
|
||||
ret: ast.expr = astNameConstant(False)
|
||||
else:
|
||||
ret = expr(s)
|
||||
s.accept(TokenType.EOF, reject=True)
|
||||
|
||||
@@ -373,7 +373,9 @@ def get_unpacked_marks(
|
||||
if not consider_mro:
|
||||
mark_lists = [obj.__dict__.get("pytestmark", [])]
|
||||
else:
|
||||
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
|
||||
mark_lists = [
|
||||
x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
|
||||
]
|
||||
mark_list = []
|
||||
for item in mark_lists:
|
||||
if isinstance(item, list):
|
||||
|
||||
@@ -7,6 +7,7 @@ from contextlib import contextmanager
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import MutableMapping
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
@@ -129,7 +130,7 @@ class MonkeyPatch:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._setattr: List[Tuple[object, str, object]] = []
|
||||
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
|
||||
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
|
||||
self._cwd: Optional[str] = None
|
||||
self._savesyspath: Optional[List[str]] = None
|
||||
|
||||
@@ -290,12 +291,13 @@ class MonkeyPatch:
|
||||
self._setattr.append((target, name, oldval))
|
||||
delattr(target, name)
|
||||
|
||||
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
|
||||
def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
|
||||
"""Set dictionary entry ``name`` to value."""
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
dic[name] = value
|
||||
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||
dic[name] = value # type: ignore[index]
|
||||
|
||||
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
|
||||
def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
|
||||
"""Delete ``name`` from dict.
|
||||
|
||||
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
||||
@@ -306,7 +308,8 @@ class MonkeyPatch:
|
||||
raise KeyError(name)
|
||||
else:
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
del dic[name]
|
||||
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||
del dic[name] # type: ignore[attr-defined]
|
||||
|
||||
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
||||
"""Set environment variable ``name`` to ``value``.
|
||||
@@ -401,11 +404,13 @@ class MonkeyPatch:
|
||||
for dictionary, key, value in reversed(self._setitem):
|
||||
if value is notset:
|
||||
try:
|
||||
del dictionary[key]
|
||||
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||
del dictionary[key] # type: ignore[attr-defined]
|
||||
except KeyError:
|
||||
pass # Was already deleted, so we have the desired state.
|
||||
else:
|
||||
dictionary[key] = value
|
||||
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||
dictionary[key] = value # type: ignore[index]
|
||||
self._setitem[:] = []
|
||||
if self._savesyspath is not None:
|
||||
sys.path[:] = self._savesyspath
|
||||
|
||||
@@ -22,6 +22,7 @@ import _pytest._code
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.code import Traceback
|
||||
from _pytest.compat import cached_property
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.config import Config
|
||||
@@ -156,10 +157,11 @@ class NodeMeta(type):
|
||||
|
||||
|
||||
class Node(metaclass=NodeMeta):
|
||||
"""Base class for Collector and Item, the components of the test
|
||||
collection tree.
|
||||
r"""Base class of :class:`Collector` and :class:`Item`, the components of
|
||||
the test collection tree.
|
||||
|
||||
Collector subclasses have children; Items are leaf nodes.
|
||||
``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
|
||||
leaf nodes.
|
||||
"""
|
||||
|
||||
# Implemented in the legacypath plugin.
|
||||
@@ -432,8 +434,8 @@ class Node(metaclass=NodeMeta):
|
||||
assert current is None or isinstance(current, cls)
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
pass
|
||||
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||
return excinfo.traceback
|
||||
|
||||
def _repr_failure_py(
|
||||
self,
|
||||
@@ -449,13 +451,13 @@ class Node(metaclass=NodeMeta):
|
||||
style = "value"
|
||||
if isinstance(excinfo.value, FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
|
||||
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
|
||||
if self.config.getoption("fulltrace", False):
|
||||
style = "long"
|
||||
tbfilter = False
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = self._traceback_filter
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
@@ -486,7 +488,7 @@ class Node(metaclass=NodeMeta):
|
||||
abspath=abspath,
|
||||
showlocals=self.config.getoption("showlocals", False),
|
||||
style=style,
|
||||
tbfilter=False, # pruned already, or in --fulltrace mode.
|
||||
tbfilter=tbfilter,
|
||||
truncate_locals=truncate_locals,
|
||||
)
|
||||
|
||||
@@ -524,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
"""Collector instances create children through collect() and thus
|
||||
iteratively build a tree."""
|
||||
"""Base class of all collectors.
|
||||
|
||||
Collector create children through `collect()` and thus iteratively build
|
||||
the collection tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
"""An error during collection, contains a custom message."""
|
||||
|
||||
def collect(self) -> Iterable[Union["Item", "Collector"]]:
|
||||
"""Return a list of children (items and collectors) for this
|
||||
collection node."""
|
||||
"""Collect children (items and collectors) for this collector."""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
# TODO: This omits the style= parameter which breaks Liskov Substitution.
|
||||
@@ -557,13 +561,14 @@ class Collector(Node):
|
||||
|
||||
return self._repr_failure_py(excinfo, style=tbstyle)
|
||||
|
||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||
if hasattr(self, "path"):
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.path)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
return excinfo.traceback.filter(excinfo)
|
||||
return excinfo.traceback
|
||||
|
||||
|
||||
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
|
||||
@@ -575,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
|
||||
|
||||
|
||||
class FSCollector(Collector):
|
||||
"""Base class for filesystem collectors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fspath: Optional[LEGACY_PATH] = None,
|
||||
@@ -658,7 +665,7 @@ class File(FSCollector):
|
||||
|
||||
|
||||
class Item(Node):
|
||||
"""A basic test invocation item.
|
||||
"""Base class of all test invocation items.
|
||||
|
||||
Note that for a single function there might be multiple test invocation items.
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,7 @@ import itertools
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import types
|
||||
import uuid
|
||||
import warnings
|
||||
from enum import Enum
|
||||
@@ -26,8 +27,11 @@ from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -63,21 +67,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||
return path.joinpath(".lock")
|
||||
|
||||
|
||||
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||
def on_rm_rf_error(
|
||||
func,
|
||||
path: str,
|
||||
excinfo: Union[
|
||||
BaseException,
|
||||
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
|
||||
],
|
||||
*,
|
||||
start_path: Path,
|
||||
) -> bool:
|
||||
"""Handle known read-only errors during rmtree.
|
||||
|
||||
The returned value is used only by our own tests.
|
||||
"""
|
||||
exctype, excvalue = exc[:2]
|
||||
if isinstance(excinfo, BaseException):
|
||||
exc = excinfo
|
||||
else:
|
||||
exc = excinfo[1]
|
||||
|
||||
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
||||
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
||||
if isinstance(excvalue, FileNotFoundError):
|
||||
if isinstance(exc, FileNotFoundError):
|
||||
return False
|
||||
|
||||
if not isinstance(excvalue, PermissionError):
|
||||
if not isinstance(exc, PermissionError):
|
||||
warnings.warn(
|
||||
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
|
||||
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -86,7 +102,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||
warnings.warn(
|
||||
PytestWarning(
|
||||
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
||||
func, path, exctype, excvalue
|
||||
func, path, type(exc), exc
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -149,7 +165,10 @@ def rm_rf(path: Path) -> None:
|
||||
are read-only."""
|
||||
path = ensure_extended_length_path(path)
|
||||
onerror = partial(on_rm_rf_error, start_path=path)
|
||||
shutil.rmtree(str(path), onerror=onerror)
|
||||
if sys.version_info >= (3, 12):
|
||||
shutil.rmtree(str(path), onexc=onerror)
|
||||
else:
|
||||
shutil.rmtree(str(path), onerror=onerror)
|
||||
|
||||
|
||||
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||
@@ -335,7 +354,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||
yield path
|
||||
|
||||
|
||||
def cleanup_dead_symlink(root: Path):
|
||||
def cleanup_dead_symlinks(root: Path):
|
||||
for left_dir in root.iterdir():
|
||||
if left_dir.is_symlink():
|
||||
if not left_dir.resolve().exists():
|
||||
@@ -353,7 +372,7 @@ def cleanup_numbered_dir(
|
||||
for path in root.glob("garbage-*"):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
|
||||
cleanup_dead_symlink(root)
|
||||
cleanup_dead_symlinks(root)
|
||||
|
||||
|
||||
def make_numbered_dir_with_cleanup(
|
||||
@@ -504,6 +523,8 @@ def import_path(
|
||||
|
||||
if mode is ImportMode.importlib:
|
||||
module_name = module_name_from_path(path, root)
|
||||
with contextlib.suppress(KeyError):
|
||||
return sys.modules[module_name]
|
||||
|
||||
for meta_importer in sys.meta_path:
|
||||
spec = meta_importer.find_spec(module_name, [str(path.parent)])
|
||||
@@ -602,6 +623,11 @@ def module_name_from_path(path: Path, root: Path) -> str:
|
||||
# Use the parts for the relative path to the root path.
|
||||
path_parts = relative_path.parts
|
||||
|
||||
# Module name for packages do not contain the __init__ file, unless
|
||||
# the `__init__.py` file is at the root.
|
||||
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
|
||||
path_parts = path_parts[:-1]
|
||||
|
||||
return ".".join(path_parts)
|
||||
|
||||
|
||||
@@ -614,6 +640,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
|
||||
otherwise "src.tests.test_foo" is not importable by ``__import__``.
|
||||
"""
|
||||
module_parts = module_name.split(".")
|
||||
child_module: Union[ModuleType, None] = None
|
||||
module: Union[ModuleType, None] = None
|
||||
child_name: str = ""
|
||||
while module_name:
|
||||
if module_name not in modules:
|
||||
try:
|
||||
@@ -623,13 +652,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
|
||||
# ourselves to fall back to creating a dummy module.
|
||||
if not sys.meta_path:
|
||||
raise ModuleNotFoundError
|
||||
importlib.import_module(module_name)
|
||||
module = importlib.import_module(module_name)
|
||||
except ModuleNotFoundError:
|
||||
module = ModuleType(
|
||||
module_name,
|
||||
doc="Empty module created by pytest's importmode=importlib.",
|
||||
)
|
||||
else:
|
||||
module = modules[module_name]
|
||||
if child_module:
|
||||
# Add child attribute to the parent that can reference the child
|
||||
# modules.
|
||||
if not hasattr(module, child_name):
|
||||
setattr(module, child_name, child_module)
|
||||
modules[module_name] = module
|
||||
# Keep track of the child module while moving up the tree.
|
||||
child_module, child_name = module, module_name.rpartition(".")[-1]
|
||||
module_parts.pop(-1)
|
||||
module_name = ".".join(module_parts)
|
||||
|
||||
@@ -651,30 +689,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
|
||||
return result
|
||||
|
||||
|
||||
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
|
||||
"""Scan a directory recursively, in breadth-first order.
|
||||
|
||||
The returned entries are sorted.
|
||||
"""
|
||||
entries = []
|
||||
with os.scandir(path) as s:
|
||||
# Skip entries with symlink loops and other brokenness, so the caller
|
||||
# doesn't have to deal with it.
|
||||
for entry in s:
|
||||
try:
|
||||
entry.is_file()
|
||||
except OSError as err:
|
||||
if _ignore_error(err):
|
||||
continue
|
||||
raise
|
||||
entries.append(entry)
|
||||
entries.sort(key=lambda entry: entry.name)
|
||||
return entries
|
||||
|
||||
|
||||
def visit(
|
||||
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
|
||||
) -> Iterator["os.DirEntry[str]"]:
|
||||
"""Walk a directory recursively, in breadth-first order.
|
||||
|
||||
The `recurse` predicate determines whether a directory is recursed.
|
||||
|
||||
Entries at each directory level are sorted.
|
||||
"""
|
||||
|
||||
# Skip entries with symlink loops and other brokenness, so the caller doesn't
|
||||
# have to deal with it.
|
||||
entries = []
|
||||
for entry in os.scandir(path):
|
||||
try:
|
||||
entry.is_file()
|
||||
except OSError as err:
|
||||
if _ignore_error(err):
|
||||
continue
|
||||
raise
|
||||
entries.append(entry)
|
||||
|
||||
entries.sort(key=lambda entry: entry.name)
|
||||
|
||||
entries = scandir(path)
|
||||
yield from entries
|
||||
|
||||
for entry in entries:
|
||||
if entry.is_dir() and recurse(entry):
|
||||
yield from visit(entry.path, recurse)
|
||||
@@ -746,3 +792,13 @@ def copytree(source: Path, target: Path) -> None:
|
||||
shutil.copyfile(x, newx)
|
||||
elif x.is_dir():
|
||||
newx.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def safe_exists(p: Path) -> bool:
|
||||
"""Like Path.exists(), but account for input arguments that might be too long (#11394)."""
|
||||
try:
|
||||
return p.exists()
|
||||
except (ValueError, OSError):
|
||||
# ValueError: stat: path too long for Windows
|
||||
# OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
|
||||
return False
|
||||
|
||||
@@ -6,6 +6,7 @@ import collections.abc
|
||||
import contextlib
|
||||
import gc
|
||||
import importlib
|
||||
import locale
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
@@ -129,6 +130,7 @@ class LsofFdLeakChecker:
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True,
|
||||
text=True,
|
||||
encoding=locale.getpreferredencoding(False),
|
||||
).stdout
|
||||
|
||||
def isopen(line: str) -> bool:
|
||||
@@ -750,7 +752,7 @@ class Pytester:
|
||||
|
||||
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
|
||||
"""Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
|
||||
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True)
|
||||
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
|
||||
self._request.addfinalizer(reprec.finish_recording)
|
||||
return reprec
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ from _pytest._code import filter_traceback
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.code import Traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ascii_escaped
|
||||
@@ -56,7 +57,6 @@ from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
|
||||
from _pytest.deprecated import INSTANCE_COLLECTOR
|
||||
from _pytest.deprecated import NOSE_SUPPORT_METHOD
|
||||
from _pytest.fixtures import FuncFixtureInfo
|
||||
@@ -522,7 +522,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
|
||||
|
||||
class Module(nodes.File, PyCollector):
|
||||
"""Collector for test classes and functions."""
|
||||
"""Collector for test classes and functions in a Python module."""
|
||||
|
||||
def _getobj(self):
|
||||
return self._importtestmodule()
|
||||
@@ -659,6 +659,9 @@ class Module(nodes.File, PyCollector):
|
||||
|
||||
|
||||
class Package(Module):
|
||||
"""Collector for files and directories in a Python packages -- directories
|
||||
with an `__init__.py` file."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fspath: Optional[LEGACY_PATH],
|
||||
@@ -667,7 +670,7 @@ class Package(Module):
|
||||
config=None,
|
||||
session=None,
|
||||
nodeid=None,
|
||||
path=Optional[Path],
|
||||
path: Optional[Path] = None,
|
||||
) -> None:
|
||||
# NOTE: Could be just the following, but kept as-is for compat.
|
||||
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
||||
@@ -699,14 +702,6 @@ class Package(Module):
|
||||
func = partial(_call_with_optional_argument, teardown_module, self.obj)
|
||||
self.addfinalizer(func)
|
||||
|
||||
def gethookproxy(self, fspath: "os.PathLike[str]"):
|
||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
||||
return self.session.gethookproxy(fspath)
|
||||
|
||||
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
|
||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
||||
return self.session.isinitpath(path)
|
||||
|
||||
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
|
||||
if direntry.name == "__pycache__":
|
||||
return False
|
||||
@@ -714,9 +709,6 @@ class Package(Module):
|
||||
ihook = self.session.gethookproxy(fspath.parent)
|
||||
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
||||
return False
|
||||
norecursepatterns = self.config.getini("norecursedirs")
|
||||
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _collectfile(
|
||||
@@ -745,11 +737,11 @@ class Package(Module):
|
||||
|
||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||
this_path = self.path.parent
|
||||
init_module = this_path / "__init__.py"
|
||||
if init_module.is_file() and path_matches_patterns(
|
||||
init_module, self.config.getini("python_files")
|
||||
):
|
||||
yield Module.from_parent(self, path=init_module)
|
||||
|
||||
# Always collect the __init__ first.
|
||||
if path_matches_patterns(self.path, self.config.getini("python_files")):
|
||||
yield Module.from_parent(self, path=self.path)
|
||||
|
||||
pkg_prefixes: Set[Path] = set()
|
||||
for direntry in visit(str(this_path), recurse=self._recurse):
|
||||
path = Path(direntry.path)
|
||||
@@ -799,7 +791,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o
|
||||
|
||||
|
||||
class Class(PyCollector):
|
||||
"""Collector for test methods."""
|
||||
"""Collector for test methods (and nested classes) in a Python class."""
|
||||
|
||||
@classmethod
|
||||
def from_parent(cls, parent, *, name, obj=None, **kw):
|
||||
@@ -1160,7 +1152,7 @@ class CallSpec2:
|
||||
arg2scope = self._arg2scope.copy()
|
||||
for arg, val in zip(argnames, valset):
|
||||
if arg in params or arg in funcargs:
|
||||
raise ValueError(f"duplicate {arg!r}")
|
||||
raise ValueError(f"duplicate parametrization of {arg!r}")
|
||||
valtype_for_arg = valtypes[arg]
|
||||
if valtype_for_arg == "params":
|
||||
params[arg] = val
|
||||
@@ -1251,8 +1243,9 @@ class Metafunc:
|
||||
during the collection phase. If you need to setup expensive resources
|
||||
see about setting indirect to do it rather than at test setup time.
|
||||
|
||||
Can be called multiple times, in which case each call parametrizes all
|
||||
previous parametrizations, e.g.
|
||||
Can be called multiple times per test function (but only on different
|
||||
argument names), in which case each call parametrizes all previous
|
||||
parametrizations, e.g.
|
||||
|
||||
::
|
||||
|
||||
@@ -1684,7 +1677,7 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
||||
|
||||
|
||||
class Function(PyobjMixin, nodes.Item):
|
||||
"""An Item responsible for setting up and executing a Python test function.
|
||||
"""Item responsible for setting up and executing a Python test function.
|
||||
|
||||
:param name:
|
||||
The full function name, including any decorations like those
|
||||
@@ -1801,7 +1794,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||
def setup(self) -> None:
|
||||
self._request._fillfixtures()
|
||||
|
||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
||||
code = _pytest._code.Code.from_function(get_real_func(self.obj))
|
||||
path, firstlineno = code.path, code.firstlineno
|
||||
@@ -1813,14 +1806,21 @@ class Function(PyobjMixin, nodes.Item):
|
||||
ntraceback = ntraceback.filter(filter_traceback)
|
||||
if not ntraceback:
|
||||
ntraceback = traceback
|
||||
ntraceback = ntraceback.filter(excinfo)
|
||||
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
# issue364: mark all but first and last frames to
|
||||
# only show a single-line message for each frame.
|
||||
if self.config.getoption("tbstyle", "auto") == "auto":
|
||||
if len(excinfo.traceback) > 2:
|
||||
for entry in excinfo.traceback[1:-1]:
|
||||
entry.set_repr_style("short")
|
||||
if len(ntraceback) > 2:
|
||||
ntraceback = Traceback(
|
||||
entry
|
||||
if i == 0 or i == len(ntraceback) - 1
|
||||
else entry.with_repr_style("short")
|
||||
for i, entry in enumerate(ntraceback)
|
||||
)
|
||||
|
||||
return ntraceback
|
||||
return excinfo.traceback
|
||||
|
||||
# TODO: Type ignored -- breaks Liskov Substitution.
|
||||
def repr_failure( # type: ignore[override]
|
||||
@@ -1834,10 +1834,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||
|
||||
|
||||
class FunctionDefinition(Function):
|
||||
"""
|
||||
This class is a step gap solution until we evolve to have actual function definition nodes
|
||||
and manage to get rid of ``metafunc``.
|
||||
"""
|
||||
"""This class is a stop gap solution until we evolve to have actual function
|
||||
definition nodes and manage to get rid of ``metafunc``."""
|
||||
|
||||
def runtest(self) -> None:
|
||||
raise RuntimeError("function definitions are not supposed to be run as tests")
|
||||
|
||||
@@ -266,19 +266,20 @@ class ApproxMapping(ApproxBase):
|
||||
approx_side_as_map.items(), other_side.values()
|
||||
):
|
||||
if approx_value != other_value:
|
||||
max_abs_diff = max(
|
||||
max_abs_diff, abs(approx_value.expected - other_value)
|
||||
)
|
||||
if approx_value.expected == 0.0:
|
||||
max_rel_diff = math.inf
|
||||
else:
|
||||
max_rel_diff = max(
|
||||
max_rel_diff,
|
||||
abs(
|
||||
(approx_value.expected - other_value)
|
||||
/ approx_value.expected
|
||||
),
|
||||
if approx_value.expected is not None and other_value is not None:
|
||||
max_abs_diff = max(
|
||||
max_abs_diff, abs(approx_value.expected - other_value)
|
||||
)
|
||||
if approx_value.expected == 0.0:
|
||||
max_rel_diff = math.inf
|
||||
else:
|
||||
max_rel_diff = max(
|
||||
max_rel_diff,
|
||||
abs(
|
||||
(approx_value.expected - other_value)
|
||||
/ approx_value.expected
|
||||
),
|
||||
)
|
||||
different_ids.append(approx_key)
|
||||
|
||||
message_data = [
|
||||
@@ -950,11 +951,7 @@ def raises( # noqa: F811
|
||||
try:
|
||||
func(*args[1:], **kwargs)
|
||||
except expected_exception as e:
|
||||
# We just caught the exception - there is a traceback.
|
||||
assert e.__traceback__ is not None
|
||||
return _pytest._code.ExceptionInfo.from_exc_info(
|
||||
(type(e), e, e.__traceback__)
|
||||
)
|
||||
return _pytest._code.ExceptionInfo.from_exception(e)
|
||||
fail(message)
|
||||
|
||||
|
||||
|
||||
@@ -347,10 +347,9 @@ class TestReport(BaseReport):
|
||||
elif isinstance(excinfo.value, skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
if r is None:
|
||||
raise ValueError(
|
||||
"There should always be a traceback entry for skipping a test."
|
||||
)
|
||||
assert (
|
||||
r is not None
|
||||
), "There should always be a traceback entry for skipping a test."
|
||||
if excinfo.value._use_item_location:
|
||||
path, line = item.reportinfo()[:2]
|
||||
assert line is not None
|
||||
|
||||
@@ -8,6 +8,7 @@ import datetime
|
||||
import inspect
|
||||
import platform
|
||||
import sys
|
||||
import textwrap
|
||||
import warnings
|
||||
from collections import Counter
|
||||
from functools import partial
|
||||
@@ -20,6 +21,7 @@ from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
@@ -111,6 +113,26 @@ class MoreQuietAction(argparse.Action):
|
||||
namespace.quiet = getattr(namespace, "quiet", 0) + 1
|
||||
|
||||
|
||||
class TestShortLogReport(NamedTuple):
|
||||
"""Used to store the test status result category, shortletter and verbose word.
|
||||
For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
|
||||
|
||||
:ivar category:
|
||||
The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string.
|
||||
|
||||
:ivar letter:
|
||||
The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
|
||||
|
||||
:ivar word:
|
||||
Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
|
||||
``"ERROR"``, or the empty string.
|
||||
"""
|
||||
|
||||
category: str
|
||||
letter: str
|
||||
word: Union[str, Tuple[str, Mapping[str, bool]]]
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("terminal reporting", "Reporting", after="general")
|
||||
group._addoption(
|
||||
@@ -426,6 +448,28 @@ class TerminalReporter:
|
||||
self._tw.line()
|
||||
self.currentfspath = None
|
||||
|
||||
def wrap_write(
|
||||
self,
|
||||
content: str,
|
||||
*,
|
||||
flush: bool = False,
|
||||
margin: int = 8,
|
||||
line_sep: str = "\n",
|
||||
**markup: bool,
|
||||
) -> None:
|
||||
"""Wrap message with margin for progress info."""
|
||||
width_of_current_line = self._tw.width_of_current_line
|
||||
wrapped = line_sep.join(
|
||||
textwrap.wrap(
|
||||
" " * width_of_current_line + content,
|
||||
width=self._screen_width - margin,
|
||||
drop_whitespace=True,
|
||||
replace_whitespace=False,
|
||||
),
|
||||
)
|
||||
wrapped = wrapped[width_of_current_line:]
|
||||
self._tw.write(wrapped, flush=flush, **markup)
|
||||
|
||||
def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
|
||||
self._tw.write(content, flush=flush, **markup)
|
||||
|
||||
@@ -525,10 +569,11 @@ class TerminalReporter:
|
||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||
self._tests_ran = True
|
||||
rep = report
|
||||
res: Tuple[
|
||||
str, str, Union[str, Tuple[str, Mapping[str, bool]]]
|
||||
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||
category, letter, word = res
|
||||
|
||||
res = TestShortLogReport(
|
||||
*self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||
)
|
||||
category, letter, word = res.category, res.letter, res.word
|
||||
if not isinstance(word, tuple):
|
||||
markup = None
|
||||
else:
|
||||
@@ -572,7 +617,7 @@ class TerminalReporter:
|
||||
formatted_reason = f" ({reason})"
|
||||
|
||||
if reason and formatted_reason is not None:
|
||||
self._tw.write(formatted_reason)
|
||||
self.wrap_write(formatted_reason)
|
||||
if self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
|
||||
@@ -28,7 +28,7 @@ from .pathlib import LOCK_TIMEOUT
|
||||
from .pathlib import make_numbered_dir
|
||||
from .pathlib import make_numbered_dir_with_cleanup
|
||||
from .pathlib import rm_rf
|
||||
from .pathlib import cleanup_dead_symlink
|
||||
from .pathlib import cleanup_dead_symlinks
|
||||
from _pytest.compat import final, get_user_id
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
@@ -100,7 +100,7 @@ class TempPathFactory:
|
||||
policy = config.getini("tmp_path_retention_policy")
|
||||
if policy not in ("all", "failed", "none"):
|
||||
raise ValueError(
|
||||
f"tmp_path_retention_policy must be either all, failed, none. Current intput: {policy}."
|
||||
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
|
||||
)
|
||||
|
||||
return cls(
|
||||
@@ -289,31 +289,30 @@ def tmp_path(
|
||||
|
||||
del request.node.stash[tmppath_result_key]
|
||||
|
||||
# remove dead symlink
|
||||
basetemp = tmp_path_factory._basetemp
|
||||
if basetemp is None:
|
||||
return
|
||||
cleanup_dead_symlink(basetemp)
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
||||
"""After each session, remove base directory if all the tests passed,
|
||||
the policy is "failed", and the basetemp is not specified by a user.
|
||||
"""
|
||||
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
||||
if tmp_path_factory._basetemp is None:
|
||||
basetemp = tmp_path_factory._basetemp
|
||||
if basetemp is None:
|
||||
return
|
||||
|
||||
policy = tmp_path_factory._retention_policy
|
||||
if (
|
||||
exitstatus == 0
|
||||
and policy == "failed"
|
||||
and tmp_path_factory._given_basetemp is None
|
||||
):
|
||||
passed_dir = tmp_path_factory._basetemp
|
||||
if passed_dir.exists():
|
||||
if basetemp.is_dir():
|
||||
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||
# permissions, etc, in which case we ignore it.
|
||||
rmtree(passed_dir, ignore_errors=True)
|
||||
rmtree(basetemp, ignore_errors=True)
|
||||
|
||||
# Remove dead symlinks.
|
||||
if basetemp.is_dir():
|
||||
cleanup_dead_symlinks(basetemp)
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||
|
||||
@@ -298,6 +298,9 @@ class TestCaseFunction(Function):
|
||||
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
||||
pass
|
||||
|
||||
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
|
||||
pass
|
||||
|
||||
def runtest(self) -> None:
|
||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
||||
|
||||
@@ -331,15 +334,16 @@ class TestCaseFunction(Function):
|
||||
finally:
|
||||
delattr(self._testcase, self.name)
|
||||
|
||||
def _prunetraceback(
|
||||
def _traceback_filter(
|
||||
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
||||
) -> None:
|
||||
super()._prunetraceback(excinfo)
|
||||
traceback = excinfo.traceback.filter(
|
||||
lambda x: not x.frame.f_globals.get("__unittest")
|
||||
) -> _pytest._code.Traceback:
|
||||
traceback = super()._traceback_filter(excinfo)
|
||||
ntraceback = traceback.filter(
|
||||
lambda x: not x.frame.f_globals.get("__unittest"),
|
||||
)
|
||||
if traceback:
|
||||
excinfo.traceback = traceback
|
||||
if not ntraceback:
|
||||
ntraceback = traceback
|
||||
return ntraceback
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
|
||||
@@ -149,7 +149,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
|
||||
"""
|
||||
Issue the warning :param:`message` for the definition of the given :param:`method`
|
||||
|
||||
this helps to log warnigns for functions defined prior to finding an issue with them
|
||||
this helps to log warnings for functions defined prior to finding an issue with them
|
||||
(like hook wrappers being marked in a legacy mechanism)
|
||||
"""
|
||||
lineno = method.__code__.co_firstlineno
|
||||
|
||||
@@ -62,6 +62,7 @@ from _pytest.reports import TestReport
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TestShortLogReport
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||
from _pytest.warning_types import PytestCacheWarning
|
||||
@@ -152,6 +153,7 @@ __all__ = [
|
||||
"TempPathFactory",
|
||||
"Testdir",
|
||||
"TestReport",
|
||||
"TestShortLogReport",
|
||||
"UsageError",
|
||||
"WarningsRecorder",
|
||||
"warns",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import contextlib
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
@@ -9,6 +11,14 @@ from py import error
|
||||
from py.path import local
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ignore_encoding_warning():
|
||||
with warnings.catch_warnings():
|
||||
with contextlib.suppress(NameError): # new in 3.10
|
||||
warnings.simplefilter("ignore", EncodingWarning)
|
||||
yield
|
||||
|
||||
|
||||
class CommonFSTests:
|
||||
def test_constructor_equality(self, path1):
|
||||
p = path1.__class__(path1)
|
||||
@@ -223,7 +233,8 @@ class CommonFSTests:
|
||||
assert not (path1 < path1)
|
||||
|
||||
def test_simple_read(self, path1):
|
||||
x = path1.join("samplefile").read("r")
|
||||
with ignore_encoding_warning():
|
||||
x = path1.join("samplefile").read("r")
|
||||
assert x == "samplefile\n"
|
||||
|
||||
def test_join_div_operator(self, path1):
|
||||
@@ -265,12 +276,14 @@ class CommonFSTests:
|
||||
|
||||
def test_readlines(self, path1):
|
||||
fn = path1.join("samplefile")
|
||||
contents = fn.readlines()
|
||||
with ignore_encoding_warning():
|
||||
contents = fn.readlines()
|
||||
assert contents == ["samplefile\n"]
|
||||
|
||||
def test_readlines_nocr(self, path1):
|
||||
fn = path1.join("samplefile")
|
||||
contents = fn.readlines(cr=0)
|
||||
with ignore_encoding_warning():
|
||||
contents = fn.readlines(cr=0)
|
||||
assert contents == ["samplefile", ""]
|
||||
|
||||
def test_file(self, path1):
|
||||
@@ -362,8 +375,8 @@ class CommonFSTests:
|
||||
initpy.copy(copied)
|
||||
try:
|
||||
assert copied.check()
|
||||
s1 = initpy.read()
|
||||
s2 = copied.read()
|
||||
s1 = initpy.read_text(encoding="utf-8")
|
||||
s2 = copied.read_text(encoding="utf-8")
|
||||
assert s1 == s2
|
||||
finally:
|
||||
if copied.check():
|
||||
@@ -376,8 +389,8 @@ class CommonFSTests:
|
||||
otherdir.copy(copied)
|
||||
assert copied.check(dir=1)
|
||||
assert copied.join("__init__.py").check(file=1)
|
||||
s1 = otherdir.join("__init__.py").read()
|
||||
s2 = copied.join("__init__.py").read()
|
||||
s1 = otherdir.join("__init__.py").read_text(encoding="utf-8")
|
||||
s2 = copied.join("__init__.py").read_text(encoding="utf-8")
|
||||
assert s1 == s2
|
||||
finally:
|
||||
if copied.check(dir=1):
|
||||
@@ -463,13 +476,13 @@ def setuptestfs(path):
|
||||
return
|
||||
# print "setting up test fs for", repr(path)
|
||||
samplefile = path.ensure("samplefile")
|
||||
samplefile.write("samplefile\n")
|
||||
samplefile.write_text("samplefile\n", encoding="utf-8")
|
||||
|
||||
execfile = path.ensure("execfile")
|
||||
execfile.write("x=42")
|
||||
execfile.write_text("x=42", encoding="utf-8")
|
||||
|
||||
execfilepy = path.ensure("execfile.py")
|
||||
execfilepy.write("x=42")
|
||||
execfilepy.write_text("x=42", encoding="utf-8")
|
||||
|
||||
d = {1: 2, "hello": "world", "answer": 42}
|
||||
path.ensure("samplepickle").dump(d)
|
||||
@@ -481,22 +494,24 @@ def setuptestfs(path):
|
||||
otherdir.ensure("__init__.py")
|
||||
|
||||
module_a = otherdir.ensure("a.py")
|
||||
module_a.write("from .b import stuff as result\n")
|
||||
module_a.write_text("from .b import stuff as result\n", encoding="utf-8")
|
||||
module_b = otherdir.ensure("b.py")
|
||||
module_b.write('stuff="got it"\n')
|
||||
module_b.write_text('stuff="got it"\n', encoding="utf-8")
|
||||
module_c = otherdir.ensure("c.py")
|
||||
module_c.write(
|
||||
module_c.write_text(
|
||||
"""import py;
|
||||
import otherdir.a
|
||||
value = otherdir.a.result
|
||||
"""
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
module_d = otherdir.ensure("d.py")
|
||||
module_d.write(
|
||||
module_d.write_text(
|
||||
"""import py;
|
||||
from otherdir import a
|
||||
value2 = a.result
|
||||
"""
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
@@ -534,9 +549,11 @@ def batch_make_numbered_dirs(rootdir, repeats):
|
||||
for i in range(repeats):
|
||||
dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir)
|
||||
file_ = dir_.join("foo")
|
||||
file_.write("%s" % i)
|
||||
actual = int(file_.read())
|
||||
assert actual == i, f"int(file_.read()) is {actual} instead of {i}"
|
||||
file_.write_text("%s" % i, encoding="utf-8")
|
||||
actual = int(file_.read_text(encoding="utf-8"))
|
||||
assert (
|
||||
actual == i
|
||||
), f"int(file_.read_text(encoding='utf-8')) is {actual} instead of {i}"
|
||||
dir_.join(".lock").remove(ignore_errors=True)
|
||||
return True
|
||||
|
||||
@@ -692,14 +709,14 @@ class TestLocalPath(CommonFSTests):
|
||||
|
||||
def test_open_and_ensure(self, path1):
|
||||
p = path1.join("sub1", "sub2", "file")
|
||||
with p.open("w", ensure=1) as f:
|
||||
with p.open("w", ensure=1, encoding="utf-8") as f:
|
||||
f.write("hello")
|
||||
assert p.read() == "hello"
|
||||
assert p.read_text(encoding="utf-8") == "hello"
|
||||
|
||||
def test_write_and_ensure(self, path1):
|
||||
p = path1.join("sub1", "sub2", "file")
|
||||
p.write("hello", ensure=1)
|
||||
assert p.read() == "hello"
|
||||
p.write_text("hello", ensure=1, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == "hello"
|
||||
|
||||
@pytest.mark.parametrize("bin", (False, True))
|
||||
def test_dump(self, tmpdir, bin):
|
||||
@@ -770,9 +787,9 @@ class TestLocalPath(CommonFSTests):
|
||||
newfile = tmpdir.join("test1", "test")
|
||||
newfile.ensure()
|
||||
assert newfile.check(file=1)
|
||||
newfile.write("42")
|
||||
newfile.write_text("42", encoding="utf-8")
|
||||
newfile.ensure()
|
||||
s = newfile.read()
|
||||
s = newfile.read_text(encoding="utf-8")
|
||||
assert s == "42"
|
||||
|
||||
def test_ensure_filepath_withoutdir(self, tmpdir):
|
||||
@@ -806,9 +823,9 @@ class TestLocalPath(CommonFSTests):
|
||||
newfilename = "/test" * 60 # type:ignore[unreachable]
|
||||
l1 = tmpdir.join(newfilename)
|
||||
l1.ensure(file=True)
|
||||
l1.write("foo")
|
||||
l1.write_text("foo", encoding="utf-8")
|
||||
l2 = tmpdir.join(newfilename)
|
||||
assert l2.read() == "foo"
|
||||
assert l2.read_text(encoding="utf-8") == "foo"
|
||||
|
||||
def test_visit_depth_first(self, tmpdir):
|
||||
tmpdir.ensure("a", "1")
|
||||
@@ -1278,14 +1295,14 @@ class TestPOSIXLocalPath:
|
||||
def test_hardlink(self, tmpdir):
|
||||
linkpath = tmpdir.join("test")
|
||||
filepath = tmpdir.join("file")
|
||||
filepath.write("Hello")
|
||||
filepath.write_text("Hello", encoding="utf-8")
|
||||
nlink = filepath.stat().nlink
|
||||
linkpath.mklinkto(filepath)
|
||||
assert filepath.stat().nlink == nlink + 1
|
||||
|
||||
def test_symlink_are_identical(self, tmpdir):
|
||||
filepath = tmpdir.join("file")
|
||||
filepath.write("Hello")
|
||||
filepath.write_text("Hello", encoding="utf-8")
|
||||
linkpath = tmpdir.join("test")
|
||||
linkpath.mksymlinkto(filepath)
|
||||
assert linkpath.readlink() == str(filepath)
|
||||
@@ -1293,7 +1310,7 @@ class TestPOSIXLocalPath:
|
||||
def test_symlink_isfile(self, tmpdir):
|
||||
linkpath = tmpdir.join("test")
|
||||
filepath = tmpdir.join("file")
|
||||
filepath.write("")
|
||||
filepath.write_text("", encoding="utf-8")
|
||||
linkpath.mksymlinkto(filepath)
|
||||
assert linkpath.check(file=1)
|
||||
assert not linkpath.check(link=0, file=1)
|
||||
@@ -1302,10 +1319,12 @@ class TestPOSIXLocalPath:
|
||||
def test_symlink_relative(self, tmpdir):
|
||||
linkpath = tmpdir.join("test")
|
||||
filepath = tmpdir.join("file")
|
||||
filepath.write("Hello")
|
||||
filepath.write_text("Hello", encoding="utf-8")
|
||||
linkpath.mksymlinkto(filepath, absolute=False)
|
||||
assert linkpath.readlink() == "file"
|
||||
assert filepath.read() == linkpath.read()
|
||||
assert filepath.read_text(encoding="utf-8") == linkpath.read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
def test_symlink_not_existing(self, tmpdir):
|
||||
linkpath = tmpdir.join("testnotexisting")
|
||||
@@ -1338,7 +1357,7 @@ class TestPOSIXLocalPath:
|
||||
def test_realpath_file(self, tmpdir):
|
||||
linkpath = tmpdir.join("test")
|
||||
filepath = tmpdir.join("file")
|
||||
filepath.write("")
|
||||
filepath.write_text("", encoding="utf-8")
|
||||
linkpath.mksymlinkto(filepath)
|
||||
realpath = linkpath.realpath()
|
||||
assert realpath.basename == "file"
|
||||
@@ -1383,7 +1402,7 @@ class TestPOSIXLocalPath:
|
||||
atime1 = path.atime()
|
||||
# we could wait here but timer resolution is very
|
||||
# system dependent
|
||||
path.read()
|
||||
path.read_binary()
|
||||
time.sleep(ATIME_RESOLUTION)
|
||||
atime2 = path.atime()
|
||||
time.sleep(ATIME_RESOLUTION)
|
||||
@@ -1467,7 +1486,7 @@ class TestPOSIXLocalPath:
|
||||
test_files = ["a", "b", "c"]
|
||||
src = tmpdir.join("src")
|
||||
for f in test_files:
|
||||
src.join(f).write(f, ensure=True)
|
||||
src.join(f).write_text(f, ensure=True, encoding="utf-8")
|
||||
dst = tmpdir.join("dst")
|
||||
# a small delay before the copy
|
||||
time.sleep(ATIME_RESOLUTION)
|
||||
@@ -1521,10 +1540,11 @@ class TestUnicodePy2Py3:
|
||||
def test_read_write(self, tmpdir):
|
||||
x = tmpdir.join("hello")
|
||||
part = "hällo"
|
||||
x.write(part)
|
||||
assert x.read() == part
|
||||
x.write(part.encode(sys.getdefaultencoding()))
|
||||
assert x.read() == part.encode(sys.getdefaultencoding())
|
||||
with ignore_encoding_warning():
|
||||
x.write(part)
|
||||
assert x.read() == part
|
||||
x.write(part.encode(sys.getdefaultencoding()))
|
||||
assert x.read() == part.encode(sys.getdefaultencoding())
|
||||
|
||||
|
||||
class TestBinaryAndTextMethods:
|
||||
|
||||
@@ -267,7 +267,7 @@ class TestGeneralUsage:
|
||||
def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None:
|
||||
sub1 = pytester.mkdir("sub1")
|
||||
sub2 = pytester.mkdir("sub2")
|
||||
sub1.joinpath("conftest.py").write_text("assert 0")
|
||||
sub1.joinpath("conftest.py").write_text("assert 0", encoding="utf-8")
|
||||
result = pytester.runpytest(sub2)
|
||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||
sub2.joinpath("__init__.py").touch()
|
||||
@@ -467,7 +467,7 @@ class TestGeneralUsage:
|
||||
assert "invalid" in str(excinfo.value)
|
||||
|
||||
p = pytester.path.joinpath("test_test_plugins_given_as_strings.py")
|
||||
p.write_text("def test_foo(): pass")
|
||||
p.write_text("def test_foo(): pass", encoding="utf-8")
|
||||
mod = types.ModuleType("myplugin")
|
||||
monkeypatch.setitem(sys.modules, "myplugin", mod)
|
||||
assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0
|
||||
@@ -587,7 +587,7 @@ class TestInvocationVariants:
|
||||
def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None:
|
||||
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
|
||||
path = pytester.mkpydir("tpkg")
|
||||
path.joinpath("test_hello.py").write_text("raise ImportError")
|
||||
path.joinpath("test_hello.py").write_text("raise ImportError", encoding="utf-8")
|
||||
|
||||
result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
|
||||
assert result.ret != 0
|
||||
@@ -597,10 +597,10 @@ class TestInvocationVariants:
|
||||
def test_pyargs_only_imported_once(self, pytester: Pytester) -> None:
|
||||
pkg = pytester.mkpydir("foo")
|
||||
pkg.joinpath("test_foo.py").write_text(
|
||||
"print('hello from test_foo')\ndef test(): pass"
|
||||
"print('hello from test_foo')\ndef test(): pass", encoding="utf-8"
|
||||
)
|
||||
pkg.joinpath("conftest.py").write_text(
|
||||
"def pytest_configure(config): print('configuring')"
|
||||
"def pytest_configure(config): print('configuring')", encoding="utf-8"
|
||||
)
|
||||
|
||||
result = pytester.runpytest(
|
||||
@@ -613,7 +613,7 @@ class TestInvocationVariants:
|
||||
|
||||
def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None:
|
||||
pytester.path.joinpath("conftest.py").touch()
|
||||
pytester.path.joinpath("t.py").write_text("def test(): pass")
|
||||
pytester.path.joinpath("t.py").write_text("def test(): pass", encoding="utf-8")
|
||||
result = pytester.runpytest("--pyargs", "t.py")
|
||||
assert result.ret == ExitCode.OK
|
||||
|
||||
@@ -622,8 +622,12 @@ class TestInvocationVariants:
|
||||
|
||||
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
|
||||
path = pytester.mkpydir("tpkg")
|
||||
path.joinpath("test_hello.py").write_text("def test_hello(): pass")
|
||||
path.joinpath("test_world.py").write_text("def test_world(): pass")
|
||||
path.joinpath("test_hello.py").write_text(
|
||||
"def test_hello(): pass", encoding="utf-8"
|
||||
)
|
||||
path.joinpath("test_world.py").write_text(
|
||||
"def test_world(): pass", encoding="utf-8"
|
||||
)
|
||||
result = pytester.runpytest("--pyargs", "tpkg")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*2 passed*"])
|
||||
@@ -662,13 +666,15 @@ class TestInvocationVariants:
|
||||
ns = d.joinpath("ns_pkg")
|
||||
ns.mkdir()
|
||||
ns.joinpath("__init__.py").write_text(
|
||||
"__import__('pkg_resources').declare_namespace(__name__)"
|
||||
"__import__('pkg_resources').declare_namespace(__name__)",
|
||||
encoding="utf-8",
|
||||
)
|
||||
lib = ns.joinpath(dirname)
|
||||
lib.mkdir()
|
||||
lib.joinpath("__init__.py").touch()
|
||||
lib.joinpath(f"test_{dirname}.py").write_text(
|
||||
f"def test_{dirname}(): pass\ndef test_other():pass"
|
||||
f"def test_{dirname}(): pass\ndef test_other():pass",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# The structure of the test directory is now:
|
||||
@@ -695,11 +701,15 @@ class TestInvocationVariants:
|
||||
monkeypatch.chdir("world")
|
||||
|
||||
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
|
||||
# pgk_resources has been deprecated entirely.
|
||||
# While we could change the test to use implicit namespace packages, seems better
|
||||
# to still ensure the old declaration via declare_namespace still works.
|
||||
ignore_w = r"-Wignore:Deprecated call to `pkg_resources.declare_namespace"
|
||||
ignore_w = (
|
||||
r"-Wignore:Deprecated call to `pkg_resources.declare_namespace",
|
||||
r"-Wignore:pkg_resources is deprecated",
|
||||
)
|
||||
result = pytester.runpytest(
|
||||
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
|
||||
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", *ignore_w
|
||||
)
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
@@ -750,10 +760,10 @@ class TestInvocationVariants:
|
||||
lib.mkdir()
|
||||
lib.joinpath("__init__.py").touch()
|
||||
lib.joinpath("test_bar.py").write_text(
|
||||
"def test_bar(): pass\ndef test_other(a_fixture):pass"
|
||||
"def test_bar(): pass\ndef test_other(a_fixture):pass", encoding="utf-8"
|
||||
)
|
||||
lib.joinpath("conftest.py").write_text(
|
||||
"import pytest\n@pytest.fixture\ndef a_fixture():pass"
|
||||
"import pytest\n@pytest.fixture\ndef a_fixture():pass", encoding="utf-8"
|
||||
)
|
||||
|
||||
d_local = pytester.mkdir("symlink_root")
|
||||
@@ -1272,8 +1282,7 @@ def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None:
|
||||
result.stderr.fnmatch_lines(["*@this is stderr@*"])
|
||||
|
||||
# now ensure the output is in the junitxml
|
||||
with open(pytester.path.joinpath("output.xml")) as f:
|
||||
fullXml = f.read()
|
||||
fullXml = pytester.path.joinpath("output.xml").read_text(encoding="utf-8")
|
||||
assert "@this is stdout@\n" in fullXml
|
||||
assert "@this is stderr@\n" in fullXml
|
||||
|
||||
@@ -1299,12 +1308,47 @@ def test_no_brokenpipeerror_message(pytester: Pytester) -> None:
|
||||
popen.stderr.close()
|
||||
|
||||
|
||||
def test_function_return_non_none_warning(testdir) -> None:
|
||||
testdir.makepyfile(
|
||||
def test_function_return_non_none_warning(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_stuff():
|
||||
return "something"
|
||||
"""
|
||||
)
|
||||
res = testdir.runpytest()
|
||||
res = pytester.runpytest()
|
||||
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
|
||||
|
||||
|
||||
def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
|
||||
"""
|
||||
Regression test for #10811: previously import_path with ImportMode.importlib would
|
||||
not return a module if already in sys.modules, resulting in modules being imported
|
||||
multiple times, which causes problems with modules that have import side effects.
|
||||
"""
|
||||
# Uses the exact reproducer form #10811, given it is very minimal
|
||||
# and illustrates the problem well.
|
||||
pytester.makepyfile(
|
||||
**{
|
||||
"pmxbot/commands.py": "from . import logging",
|
||||
"pmxbot/logging.py": "",
|
||||
"tests/__init__.py": "",
|
||||
"tests/test_commands.py": """
|
||||
import importlib
|
||||
from pmxbot import logging
|
||||
|
||||
class TestCommands:
|
||||
def test_boo(self):
|
||||
assert importlib.import_module('pmxbot.logging') is logging
|
||||
""",
|
||||
}
|
||||
)
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
addopts=
|
||||
--doctest-modules
|
||||
--import-mode importlib
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest_subprocess()
|
||||
result.stdout.fnmatch_lines("*1 passed*")
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import _pytest
|
||||
import _pytest._code
|
||||
import pytest
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
@@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None:
|
||||
assert info.type == ValueError
|
||||
|
||||
|
||||
def test_excinfo_from_exception_simple() -> None:
|
||||
try:
|
||||
raise ValueError
|
||||
except ValueError as e:
|
||||
assert e.__traceback__ is not None
|
||||
info = _pytest._code.ExceptionInfo.from_exception(e)
|
||||
assert info.type == ValueError
|
||||
|
||||
|
||||
def test_excinfo_from_exception_missing_traceback_assertion() -> None:
|
||||
with pytest.raises(AssertionError, match=r"must have.*__traceback__"):
|
||||
_pytest._code.ExceptionInfo.from_exception(ValueError())
|
||||
|
||||
|
||||
def test_excinfo_getstatement():
|
||||
def g():
|
||||
raise ValueError
|
||||
@@ -172,7 +186,7 @@ class TestTraceback_f_g_h:
|
||||
|
||||
def test_traceback_filter(self):
|
||||
traceback = self.excinfo.traceback
|
||||
ntraceback = traceback.filter()
|
||||
ntraceback = traceback.filter(self.excinfo)
|
||||
assert len(ntraceback) == len(traceback) - 1
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -203,7 +217,7 @@ class TestTraceback_f_g_h:
|
||||
|
||||
excinfo = pytest.raises(ValueError, h)
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.filter()
|
||||
ntraceback = traceback.filter(excinfo)
|
||||
print(f"old: {traceback!r}")
|
||||
print(f"new: {ntraceback!r}")
|
||||
|
||||
@@ -276,7 +290,7 @@ class TestTraceback_f_g_h:
|
||||
excinfo = pytest.raises(ValueError, fail)
|
||||
assert excinfo.traceback.recursionindex() is None
|
||||
|
||||
def test_traceback_getcrashentry(self):
|
||||
def test_getreprcrash(self):
|
||||
def i():
|
||||
__tracebackhide__ = True
|
||||
raise ValueError
|
||||
@@ -292,15 +306,13 @@ class TestTraceback_f_g_h:
|
||||
g()
|
||||
|
||||
excinfo = pytest.raises(ValueError, f)
|
||||
tb = excinfo.traceback
|
||||
entry = tb.getcrashentry()
|
||||
assert entry is not None
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
assert reprcrash is not None
|
||||
co = _pytest._code.Code.from_function(h)
|
||||
assert entry.frame.code.path == co.path
|
||||
assert entry.lineno == co.firstlineno + 1
|
||||
assert entry.frame.code.name == "h"
|
||||
assert reprcrash.path == str(co.path)
|
||||
assert reprcrash.lineno == co.firstlineno + 1 + 1
|
||||
|
||||
def test_traceback_getcrashentry_empty(self):
|
||||
def test_getreprcrash_empty(self):
|
||||
def g():
|
||||
__tracebackhide__ = True
|
||||
raise ValueError
|
||||
@@ -310,9 +322,7 @@ class TestTraceback_f_g_h:
|
||||
g()
|
||||
|
||||
excinfo = pytest.raises(ValueError, f)
|
||||
tb = excinfo.traceback
|
||||
entry = tb.getcrashentry()
|
||||
assert entry is None
|
||||
assert excinfo._getreprcrash() is None
|
||||
|
||||
|
||||
def test_excinfo_exconly():
|
||||
@@ -364,7 +374,7 @@ def test_excinfo_no_sourcecode():
|
||||
|
||||
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
|
||||
# XXX: simplified locally testable version
|
||||
tmp_path.joinpath("test.txt").write_text("{{ h()}}:")
|
||||
tmp_path.joinpath("test.txt").write_text("{{ h()}}:", encoding="utf-8")
|
||||
|
||||
jinja2 = pytest.importorskip("jinja2")
|
||||
loader = jinja2.FileSystemLoader(str(tmp_path))
|
||||
@@ -441,7 +451,7 @@ class TestFormattedExcinfo:
|
||||
source = textwrap.dedent(source)
|
||||
modpath = tmp_path.joinpath("mod.py")
|
||||
tmp_path.joinpath("__init__.py").touch()
|
||||
modpath.write_text(source)
|
||||
modpath.write_text(source, encoding="utf-8")
|
||||
importlib.invalidate_caches()
|
||||
return import_path(modpath, root=tmp_path)
|
||||
|
||||
@@ -614,7 +624,7 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.func1)
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
p = FormattedExcinfo()
|
||||
reprtb = p.repr_traceback_entry(excinfo.traceback[-1])
|
||||
|
||||
@@ -647,7 +657,7 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120)
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
entry = excinfo.traceback[-1]
|
||||
p = FormattedExcinfo(funcargs=True)
|
||||
reprfuncargs = p.repr_args(entry)
|
||||
@@ -674,7 +684,7 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d")
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
entry = excinfo.traceback[-1]
|
||||
p = FormattedExcinfo(funcargs=True)
|
||||
reprfuncargs = p.repr_args(entry)
|
||||
@@ -948,7 +958,7 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.f)
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
repr = excinfo.getrepr()
|
||||
repr.toterminal(tw_mock)
|
||||
assert tw_mock.lines[0] == ""
|
||||
@@ -982,7 +992,7 @@ raise ValueError()
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.f)
|
||||
tmp_path.joinpath("mod.py").unlink()
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
repr = excinfo.getrepr()
|
||||
repr.toterminal(tw_mock)
|
||||
assert tw_mock.lines[0] == ""
|
||||
@@ -1013,8 +1023,8 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.f)
|
||||
tmp_path.joinpath("mod.py").write_text("asdf")
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
tmp_path.joinpath("mod.py").write_text("asdf", encoding="utf-8")
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
repr = excinfo.getrepr()
|
||||
repr.toterminal(tw_mock)
|
||||
assert tw_mock.lines[0] == ""
|
||||
@@ -1111,9 +1121,11 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.f)
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback[1].set_repr_style("short")
|
||||
excinfo.traceback[2].set_repr_style("short")
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
excinfo.traceback = _pytest._code.Traceback(
|
||||
entry if i not in (1, 2) else entry.with_repr_style("short")
|
||||
for i, entry in enumerate(excinfo.traceback)
|
||||
)
|
||||
r = excinfo.getrepr(style="long")
|
||||
r.toterminal(tw_mock)
|
||||
for line in tw_mock.lines:
|
||||
@@ -1379,7 +1391,7 @@ raise ValueError()
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
mod.f()
|
||||
# previously crashed with `AttributeError: list has no attribute get`
|
||||
excinfo.traceback.filter()
|
||||
excinfo.traceback.filter(excinfo)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("style", ["short", "long"])
|
||||
@@ -1573,3 +1585,66 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
|
||||
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
|
||||
pytest.importorskip("exceptiongroup")
|
||||
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native"))
|
||||
def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
|
||||
"""Regression test for #10903."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test():
|
||||
__tracebackhide__ = True
|
||||
1 / 0
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("--tb", tbstyle)
|
||||
assert result.ret == 1
|
||||
if tbstyle != "line":
|
||||
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
|
||||
if tbstyle not in ("line", "native"):
|
||||
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])
|
||||
|
||||
|
||||
def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None:
|
||||
"""Hidden entries of chained exceptions are not shown (#1904)."""
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
def g1():
|
||||
__tracebackhide__ = True
|
||||
str.does_not_exist
|
||||
|
||||
def f3():
|
||||
__tracebackhide__ = True
|
||||
1 / 0
|
||||
|
||||
def f2():
|
||||
try:
|
||||
f3()
|
||||
except Exception:
|
||||
g1()
|
||||
|
||||
def f1():
|
||||
__tracebackhide__ = True
|
||||
f2()
|
||||
|
||||
def test():
|
||||
f1()
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(str(p), "--tb=short")
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*.py:11: in f2",
|
||||
" f3()",
|
||||
"E ZeroDivisionError: division by zero",
|
||||
"",
|
||||
"During handling of the above exception, another exception occurred:",
|
||||
"*.py:20: in test",
|
||||
" f1()",
|
||||
"*.py:13: in f2",
|
||||
" g1()",
|
||||
"E AttributeError:*'does_not_exist'",
|
||||
],
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
@@ -294,7 +294,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) -
|
||||
"""
|
||||
)
|
||||
path = tmp_path.joinpath("a.py")
|
||||
path.write_text(str(source))
|
||||
path.write_text(str(source), encoding="utf-8")
|
||||
mod: Any = import_path(path, root=tmp_path)
|
||||
s2 = Source(mod.A)
|
||||
assert str(source).strip() == str(s2).strip()
|
||||
|
||||
@@ -21,6 +21,15 @@ if sys.gettrace():
|
||||
sys.settrace(orig_trace)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Force terminal width to 80: some tests check the formatting of --help, which is sensible
|
||||
to terminal width.
|
||||
"""
|
||||
monkeypatch.setenv("COLUMNS", "80")
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
"""Prefer faster tests.
|
||||
@@ -105,7 +114,7 @@ def tw_mock():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dummy_yaml_custom_test(pytester: Pytester):
|
||||
def dummy_yaml_custom_test(pytester: Pytester) -> None:
|
||||
"""Writes a conftest file that collects and executes a dummy yaml test.
|
||||
|
||||
Taken from the docs, but stripped down to the bare minimum, useful for
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# mypy: disable-error-code="attr-defined"
|
||||
# mypy: disallow-untyped-defs
|
||||
import logging
|
||||
from typing import Iterator
|
||||
|
||||
import pytest
|
||||
from _pytest.logging import caplog_records_key
|
||||
@@ -8,12 +11,25 @@ logger = logging.getLogger(__name__)
|
||||
sublogger = logging.getLogger(__name__ + ".baz")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_disabled_logging() -> Iterator[None]:
|
||||
"""Simple fixture that ensures that a test doesn't disable logging.
|
||||
|
||||
This is necessary because ``logging.disable()`` is global, so a test disabling logging
|
||||
and not cleaning up after will break every test that runs after it.
|
||||
|
||||
This behavior was moved to a fixture so that logging will be un-disabled even if the test fails an assertion.
|
||||
"""
|
||||
yield
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
|
||||
def test_fixture_help(pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("--fixtures")
|
||||
result.stdout.fnmatch_lines(["*caplog*"])
|
||||
|
||||
|
||||
def test_change_level(caplog):
|
||||
def test_change_level(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.debug("handler DEBUG level")
|
||||
logger.info("handler INFO level")
|
||||
@@ -28,10 +44,27 @@ def test_change_level(caplog):
|
||||
assert "CRITICAL" in caplog.text
|
||||
|
||||
|
||||
def test_change_level_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
|
||||
logging.disable(logging.CRITICAL)
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
caplog.set_level(logging.WARNING)
|
||||
logger.info("handler INFO level")
|
||||
logger.warning("handler WARNING level")
|
||||
|
||||
caplog.set_level(logging.CRITICAL, logger=sublogger.name)
|
||||
sublogger.warning("logger SUB_WARNING level")
|
||||
sublogger.critical("logger SUB_CRITICAL level")
|
||||
|
||||
assert "INFO" not in caplog.text
|
||||
assert "WARNING" in caplog.text
|
||||
assert "SUB_WARNING" not in caplog.text
|
||||
assert "SUB_CRITICAL" in caplog.text
|
||||
|
||||
|
||||
def test_change_level_undo(pytester: Pytester) -> None:
|
||||
"""Ensure that 'set_level' is undone after the end of the test.
|
||||
|
||||
Tests the logging output themselves (affacted both by logger and handler levels).
|
||||
Tests the logging output themselves (affected both by logger and handler levels).
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
@@ -54,6 +87,35 @@ def test_change_level_undo(pytester: Pytester) -> None:
|
||||
result.stdout.no_fnmatch_line("*log from test2*")
|
||||
|
||||
|
||||
def test_change_disabled_level_undo(pytester: Pytester) -> None:
|
||||
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
|
||||
|
||||
Tests the logging output themselves (affected by disabled logging level).
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
def test1(caplog):
|
||||
logging.disable(logging.CRITICAL)
|
||||
caplog.set_level(logging.INFO)
|
||||
# using + operator here so fnmatch_lines doesn't match the code in the traceback
|
||||
logging.info('log from ' + 'test1')
|
||||
assert 0
|
||||
|
||||
def test2(caplog):
|
||||
# using + operator here so fnmatch_lines doesn't match the code in the traceback
|
||||
# use logging.warning because we need a level that will show up if logging.disabled
|
||||
# isn't reset to ``CRITICAL`` after test1.
|
||||
logging.warning('log from ' + 'test2')
|
||||
assert 0
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"])
|
||||
result.stdout.no_fnmatch_line("*log from test2*")
|
||||
|
||||
|
||||
def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
||||
"""Ensure that 'set_level' is undone after the end of the test (handler).
|
||||
|
||||
@@ -82,7 +144,7 @@ def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
||||
result.assert_outcomes(passed=3)
|
||||
|
||||
|
||||
def test_with_statement(caplog):
|
||||
def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level(logging.INFO):
|
||||
logger.debug("handler DEBUG level")
|
||||
logger.info("handler INFO level")
|
||||
@@ -97,7 +159,66 @@ def test_with_statement(caplog):
|
||||
assert "CRITICAL" in caplog.text
|
||||
|
||||
|
||||
def test_log_access(caplog):
|
||||
def test_with_statement_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
|
||||
logging.disable(logging.CRITICAL)
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
with caplog.at_level(logging.WARNING):
|
||||
logger.debug("handler DEBUG level")
|
||||
logger.info("handler INFO level")
|
||||
logger.warning("handler WARNING level")
|
||||
logger.error("handler ERROR level")
|
||||
logger.critical("handler CRITICAL level")
|
||||
|
||||
assert logging.root.manager.disable == logging.INFO
|
||||
|
||||
with caplog.at_level(logging.CRITICAL, logger=sublogger.name):
|
||||
sublogger.warning("logger SUB_WARNING level")
|
||||
sublogger.critical("logger SUB_CRITICAL level")
|
||||
|
||||
assert "DEBUG" not in caplog.text
|
||||
assert "INFO" not in caplog.text
|
||||
assert "WARNING" in caplog.text
|
||||
assert "ERROR" in caplog.text
|
||||
assert " CRITICAL" in caplog.text
|
||||
assert "SUB_WARNING" not in caplog.text
|
||||
assert "SUB_CRITICAL" in caplog.text
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"level_str,expected_disable_level",
|
||||
[
|
||||
("CRITICAL", logging.ERROR),
|
||||
("ERROR", logging.WARNING),
|
||||
("WARNING", logging.INFO),
|
||||
("INFO", logging.DEBUG),
|
||||
("DEBUG", logging.NOTSET),
|
||||
("NOTSET", logging.NOTSET),
|
||||
("NOTVALIDLEVEL", logging.NOTSET),
|
||||
],
|
||||
)
|
||||
def test_force_enable_logging_level_string(
|
||||
caplog: pytest.LogCaptureFixture, level_str: str, expected_disable_level: int
|
||||
) -> None:
|
||||
"""Test _force_enable_logging using a level string.
|
||||
|
||||
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
|
||||
always needs to be *at least* one level lower than the level that caplog is trying to capture.
|
||||
"""
|
||||
test_logger = logging.getLogger("test_str_level_force_enable")
|
||||
# Emulate a testing environment where all logging is disabled.
|
||||
logging.disable(logging.CRITICAL)
|
||||
# Make sure all logging is disabled.
|
||||
assert not test_logger.isEnabledFor(logging.CRITICAL)
|
||||
# Un-disable logging for `level_str`.
|
||||
caplog._force_enable_logging(level_str, test_logger)
|
||||
# Make sure that the disabled level is now one below the requested logging level.
|
||||
# We don't use `isEnabledFor` here because that also checks the level set by
|
||||
# `logging.setLevel()` which is irrelevant to `logging.disable()`.
|
||||
assert test_logger.manager.disable == expected_disable_level
|
||||
|
||||
|
||||
def test_log_access(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
assert caplog.records[0].levelname == "INFO"
|
||||
@@ -105,7 +226,7 @@ def test_log_access(caplog):
|
||||
assert "boo arg" in caplog.text
|
||||
|
||||
|
||||
def test_messages(caplog):
|
||||
def test_messages(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
logger.info("bar %s\nbaz %s", "arg1", "arg2")
|
||||
@@ -126,14 +247,14 @@ def test_messages(caplog):
|
||||
assert "Exception" not in caplog.messages[-1]
|
||||
|
||||
|
||||
def test_record_tuples(caplog):
|
||||
def test_record_tuples(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
|
||||
assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")]
|
||||
|
||||
|
||||
def test_unicode(caplog):
|
||||
def test_unicode(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("bū")
|
||||
assert caplog.records[0].levelname == "INFO"
|
||||
@@ -141,7 +262,7 @@ def test_unicode(caplog):
|
||||
assert "bū" in caplog.text
|
||||
|
||||
|
||||
def test_clear(caplog):
|
||||
def test_clear(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("bū")
|
||||
assert len(caplog.records)
|
||||
@@ -152,7 +273,9 @@ def test_clear(caplog):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logging_during_setup_and_teardown(caplog):
|
||||
def logging_during_setup_and_teardown(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> Iterator[None]:
|
||||
caplog.set_level("INFO")
|
||||
logger.info("a_setup_log")
|
||||
yield
|
||||
@@ -160,7 +283,9 @@ def logging_during_setup_and_teardown(caplog):
|
||||
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
|
||||
|
||||
|
||||
def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown):
|
||||
def test_caplog_captures_for_all_stages(
|
||||
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
|
||||
) -> None:
|
||||
assert not caplog.records
|
||||
assert not caplog.get_records("call")
|
||||
logger.info("a_call_log")
|
||||
@@ -169,25 +294,31 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow
|
||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
|
||||
# This reaches into private API, don't use this type of thing in real tests!
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
caplog_records = caplog._item.stash[caplog_records_key]
|
||||
assert set(caplog_records) == {"setup", "call"}
|
||||
|
||||
|
||||
def test_clear_for_call_stage(caplog, logging_during_setup_and_teardown):
|
||||
def test_clear_for_call_stage(
|
||||
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
|
||||
) -> None:
|
||||
logger.info("a_call_log")
|
||||
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
|
||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
caplog_records = caplog._item.stash[caplog_records_key]
|
||||
assert set(caplog_records) == {"setup", "call"}
|
||||
|
||||
caplog.clear()
|
||||
|
||||
assert caplog.get_records("call") == []
|
||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
caplog_records = caplog._item.stash[caplog_records_key]
|
||||
assert set(caplog_records) == {"setup", "call"}
|
||||
|
||||
logging.info("a_call_log_after_clear")
|
||||
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
|
||||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
caplog_records = caplog._item.stash[caplog_records_key]
|
||||
assert set(caplog_records) == {"setup", "call"}
|
||||
|
||||
|
||||
def test_ini_controls_global_log_level(pytester: Pytester) -> None:
|
||||
|
||||
@@ -81,7 +81,7 @@ def test_root_logger_affected(pytester: Pytester) -> None:
|
||||
# not the info one, because the default level of the root logger is
|
||||
# WARNING.
|
||||
assert os.path.isfile(log_file)
|
||||
with open(log_file) as rfh:
|
||||
with open(log_file, encoding="utf-8") as rfh:
|
||||
contents = rfh.read()
|
||||
assert "info text going to logger" not in contents
|
||||
assert "warning text going to logger" in contents
|
||||
@@ -656,7 +656,7 @@ def test_log_file_cli(pytester: Pytester) -> None:
|
||||
# make sure that we get a '0' exit code for the testsuite
|
||||
assert result.ret == 0
|
||||
assert os.path.isfile(log_file)
|
||||
with open(log_file) as rfh:
|
||||
with open(log_file, encoding="utf-8") as rfh:
|
||||
contents = rfh.read()
|
||||
assert "This log message will be shown" in contents
|
||||
assert "This log message won't be shown" not in contents
|
||||
@@ -687,7 +687,7 @@ def test_log_file_cli_level(pytester: Pytester) -> None:
|
||||
# make sure that we get a '0' exit code for the testsuite
|
||||
assert result.ret == 0
|
||||
assert os.path.isfile(log_file)
|
||||
with open(log_file) as rfh:
|
||||
with open(log_file, encoding="utf-8") as rfh:
|
||||
contents = rfh.read()
|
||||
assert "This log message will be shown" in contents
|
||||
assert "This log message won't be shown" not in contents
|
||||
@@ -738,7 +738,7 @@ def test_log_file_ini(pytester: Pytester) -> None:
|
||||
# make sure that we get a '0' exit code for the testsuite
|
||||
assert result.ret == 0
|
||||
assert os.path.isfile(log_file)
|
||||
with open(log_file) as rfh:
|
||||
with open(log_file, encoding="utf-8") as rfh:
|
||||
contents = rfh.read()
|
||||
assert "This log message will be shown" in contents
|
||||
assert "This log message won't be shown" not in contents
|
||||
@@ -777,7 +777,7 @@ def test_log_file_ini_level(pytester: Pytester) -> None:
|
||||
# make sure that we get a '0' exit code for the testsuite
|
||||
assert result.ret == 0
|
||||
assert os.path.isfile(log_file)
|
||||
with open(log_file) as rfh:
|
||||
with open(log_file, encoding="utf-8") as rfh:
|
||||
contents = rfh.read()
|
||||
assert "This log message will be shown" in contents
|
||||
assert "This log message won't be shown" not in contents
|
||||
@@ -985,7 +985,7 @@ def test_log_in_hooks(pytester: Pytester) -> None:
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"])
|
||||
with open(log_file) as rfh:
|
||||
with open(log_file, encoding="utf-8") as rfh:
|
||||
contents = rfh.read()
|
||||
assert "sessionstart" in contents
|
||||
assert "runtestloop" in contents
|
||||
@@ -1021,7 +1021,7 @@ def test_log_in_runtest_logreport(pytester: Pytester) -> None:
|
||||
"""
|
||||
)
|
||||
pytester.runpytest()
|
||||
with open(log_file) as rfh:
|
||||
with open(log_file, encoding="utf-8") as rfh:
|
||||
contents = rfh.read()
|
||||
assert contents.count("logreport") == 3
|
||||
|
||||
@@ -1065,11 +1065,11 @@ def test_log_set_path(pytester: Pytester) -> None:
|
||||
"""
|
||||
)
|
||||
pytester.runpytest()
|
||||
with open(os.path.join(report_dir_base, "test_first")) as rfh:
|
||||
with open(os.path.join(report_dir_base, "test_first"), encoding="utf-8") as rfh:
|
||||
content = rfh.read()
|
||||
assert "message from test 1" in content
|
||||
|
||||
with open(os.path.join(report_dir_base, "test_second")) as rfh:
|
||||
with open(os.path.join(report_dir_base, "test_second"), encoding="utf-8") as rfh:
|
||||
content = rfh.read()
|
||||
assert "message from test 2" in content
|
||||
|
||||
@@ -1167,8 +1167,8 @@ def test_log_file_cli_subdirectories_are_successfully_created(
|
||||
assert result.ret == ExitCode.OK
|
||||
|
||||
|
||||
def test_disable_loggers(testdir):
|
||||
testdir.makepyfile(
|
||||
def test_disable_loggers(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
@@ -1181,13 +1181,13 @@ def test_disable_loggers(testdir):
|
||||
assert caplog.record_tuples == [('test', 10, 'Visible text!')]
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--log-disable=disabled", "-s")
|
||||
result = pytester.runpytest("--log-disable=disabled", "-s")
|
||||
assert result.ret == ExitCode.OK
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_disable_loggers_does_not_propagate(testdir):
|
||||
testdir.makepyfile(
|
||||
def test_disable_loggers_does_not_propagate(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
@@ -1205,13 +1205,13 @@ def test_disable_loggers_does_not_propagate(testdir):
|
||||
"""
|
||||
)
|
||||
|
||||
result = testdir.runpytest("--log-disable=parent.child", "-s")
|
||||
result = pytester.runpytest("--log-disable=parent.child", "-s")
|
||||
assert result.ret == ExitCode.OK
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_log_disabling_works_with_log_cli(testdir):
|
||||
testdir.makepyfile(
|
||||
def test_log_disabling_works_with_log_cli(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
disabled_log = logging.getLogger('disabled')
|
||||
@@ -1222,7 +1222,7 @@ def test_log_disabling_works_with_log_cli(testdir):
|
||||
disabled_log.warning("This string will be suppressed.")
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest(
|
||||
result = pytester.runpytest(
|
||||
"--log-cli-level=DEBUG",
|
||||
"--log-disable=disabled",
|
||||
)
|
||||
@@ -1234,3 +1234,100 @@ def test_log_disabling_works_with_log_cli(testdir):
|
||||
"WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed."
|
||||
)
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_without_date_format_log(pytester: Pytester) -> None:
|
||||
"""Check that date is not printed by default."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_foo():
|
||||
logger.warning('text')
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(
|
||||
["WARNING test_without_date_format_log:test_without_date_format_log.py:6 text"]
|
||||
)
|
||||
|
||||
|
||||
def test_date_format_log(pytester: Pytester) -> None:
|
||||
"""Check that log_date_format affects output."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_foo():
|
||||
logger.warning('text')
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||
log_date_format=%Y-%m-%d %H:%M:%S
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}; WARNING; text"])
|
||||
|
||||
|
||||
def test_date_format_percentf_log(pytester: Pytester) -> None:
|
||||
"""Make sure that microseconds are printed in log."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_foo():
|
||||
logger.warning('text')
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||
log_date_format=%Y-%m-%d %H:%M:%S.%f
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}; WARNING; text"])
|
||||
|
||||
|
||||
def test_date_format_percentf_tz_log(pytester: Pytester) -> None:
|
||||
"""Make sure that timezone and microseconds are properly formatted together."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_foo():
|
||||
logger.warning('text')
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||
log_date_format=%Y-%m-%d %H:%M:%S.%f%z
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.re_match_lines(
|
||||
[r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}[+-][0-9\.]+; WARNING; text"]
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
anyio[curio,trio]==3.6.2
|
||||
django==4.1.7
|
||||
anyio[curio,trio]==3.7.0
|
||||
django==4.2.2
|
||||
pytest-asyncio==0.21.0
|
||||
pytest-bdd==6.1.1
|
||||
pytest-cov==4.0.0
|
||||
pytest-cov==4.1.0
|
||||
pytest-django==4.5.2
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==3.2.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-mock==3.11.1
|
||||
pytest-rerunfailures==11.1.2
|
||||
pytest-sugar==0.9.5
|
||||
pytest-sugar==0.9.7
|
||||
pytest-trio==0.7.0
|
||||
pytest-twisted==1.14.0
|
||||
twisted==22.8.0
|
||||
pytest-xvfb==2.0.0
|
||||
pytest-xvfb==3.0.0
|
||||
|
||||
@@ -122,6 +122,23 @@ class TestApprox:
|
||||
],
|
||||
)
|
||||
|
||||
assert_approx_raises_regex(
|
||||
{"a": 1.0, "b": None, "c": None},
|
||||
{
|
||||
"a": None,
|
||||
"b": 1000.0,
|
||||
"c": None,
|
||||
},
|
||||
[
|
||||
r" comparison failed. Mismatched elements: 2 / 3:",
|
||||
r" Max absolute difference: -inf",
|
||||
r" Max relative difference: -inf",
|
||||
r" Index \| Obtained\s+\| Expected\s+",
|
||||
rf" a \| {SOME_FLOAT} \| None",
|
||||
rf" b \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}",
|
||||
],
|
||||
)
|
||||
|
||||
assert_approx_raises_regex(
|
||||
[1.0, 2.0, 3.0, 4.0],
|
||||
[1.0, 3.0, 3.0, 5.0],
|
||||
|
||||
@@ -60,7 +60,8 @@ class TestModule:
|
||||
""".format(
|
||||
str(root2)
|
||||
)
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with monkeypatch.context() as mp:
|
||||
mp.chdir(root2)
|
||||
@@ -832,7 +833,8 @@ class TestConftestCustomization:
|
||||
mod = outcome.get_result()
|
||||
mod.obj.hello = "world"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
b.joinpath("test_module.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -840,7 +842,8 @@ class TestConftestCustomization:
|
||||
def test_hello():
|
||||
assert hello == "world"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reprec = pytester.inline_run()
|
||||
reprec.assertoutcome(passed=1)
|
||||
@@ -861,7 +864,8 @@ class TestConftestCustomization:
|
||||
for func in result:
|
||||
func._some123 = "world"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
b.joinpath("test_module.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -874,7 +878,8 @@ class TestConftestCustomization:
|
||||
def test_hello(obj):
|
||||
assert obj == "world"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reprec = pytester.inline_run()
|
||||
reprec.assertoutcome(passed=1)
|
||||
@@ -897,25 +902,29 @@ class TestConftestCustomization:
|
||||
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
|
||||
"""Ensure we can collect files with weird file extensions as Python
|
||||
modules (#2369)"""
|
||||
# We'll implement a little finder and loader to import files containing
|
||||
# Implement a little meta path finder to import files containing
|
||||
# Python source code whose file extension is ".narf".
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
import sys, os, imp
|
||||
import sys
|
||||
import os.path
|
||||
from importlib.util import spec_from_loader
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from _pytest.python import Module
|
||||
|
||||
class Loader(object):
|
||||
def load_module(self, name):
|
||||
return imp.load_source(name, name + ".narf")
|
||||
class Finder(object):
|
||||
def find_module(self, name, path=None):
|
||||
if os.path.exists(name + ".narf"):
|
||||
return Loader()
|
||||
sys.meta_path.append(Finder())
|
||||
class MetaPathFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if os.path.exists(fullname + ".narf"):
|
||||
return spec_from_loader(
|
||||
fullname,
|
||||
SourceFileLoader(fullname, fullname + ".narf"),
|
||||
)
|
||||
sys.meta_path.append(MetaPathFinder())
|
||||
|
||||
def pytest_collect_file(file_path, parent):
|
||||
if file_path.suffix == ".narf":
|
||||
return Module.from_parent(path=file_path, parent=parent)"""
|
||||
return Module.from_parent(path=file_path, parent=parent)
|
||||
"""
|
||||
)
|
||||
pytester.makefile(
|
||||
".narf",
|
||||
@@ -970,7 +979,8 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
|
||||
def pytest_runtest_teardown(item):
|
||||
assert item.path.stem == "test_in_sub1"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub2.joinpath("conftest.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -983,10 +993,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
|
||||
def pytest_runtest_teardown(item):
|
||||
assert item.path.stem == "test_in_sub2"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
|
||||
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
|
||||
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass", encoding="utf-8")
|
||||
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass", encoding="utf-8")
|
||||
result = pytester.runpytest("-v", "-s")
|
||||
result.assert_outcomes(passed=2)
|
||||
|
||||
@@ -1003,9 +1014,9 @@ class TestTracebackCutting:
|
||||
with pytest.raises(pytest.skip.Exception) as excinfo:
|
||||
pytest.skip("xxx")
|
||||
assert excinfo.traceback[-1].frame.code.name == "skip"
|
||||
assert excinfo.traceback[-1].ishidden()
|
||||
assert excinfo.traceback[-1].ishidden(excinfo)
|
||||
assert excinfo.traceback[-2].frame.code.name == "test_skip_simple"
|
||||
assert not excinfo.traceback[-2].ishidden()
|
||||
assert not excinfo.traceback[-2].ishidden(excinfo)
|
||||
|
||||
def test_traceback_argsetup(self, pytester: Pytester) -> None:
|
||||
pytester.makeconftest(
|
||||
@@ -1374,7 +1385,8 @@ def test_skip_duplicates_by_default(pytester: Pytester) -> None:
|
||||
def test_real():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = pytester.runpytest(str(a), str(a))
|
||||
result.stdout.fnmatch_lines(["*collected 1 item*"])
|
||||
@@ -1394,7 +1406,8 @@ def test_keep_duplicates(pytester: Pytester) -> None:
|
||||
def test_real():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = pytester.runpytest("--keep-duplicates", str(a), str(a))
|
||||
result.stdout.fnmatch_lines(["*collected 2 item*"])
|
||||
@@ -1439,8 +1452,12 @@ def test_package_with_modules(pytester: Pytester) -> None:
|
||||
sub2_test = sub2.joinpath("test")
|
||||
sub2_test.mkdir(parents=True)
|
||||
|
||||
sub1_test.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
|
||||
sub2_test.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
|
||||
sub1_test.joinpath("test_in_sub1.py").write_text(
|
||||
"def test_1(): pass", encoding="utf-8"
|
||||
)
|
||||
sub2_test.joinpath("test_in_sub2.py").write_text(
|
||||
"def test_2(): pass", encoding="utf-8"
|
||||
)
|
||||
|
||||
# Execute from .
|
||||
result = pytester.runpytest("-v", "-s")
|
||||
@@ -1484,9 +1501,11 @@ def test_package_ordering(pytester: Pytester) -> None:
|
||||
sub2_test = sub2.joinpath("test")
|
||||
sub2_test.mkdir(parents=True)
|
||||
|
||||
root.joinpath("Test_root.py").write_text("def test_1(): pass")
|
||||
sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass")
|
||||
sub2_test.joinpath("test_sub2.py").write_text("def test_3(): pass")
|
||||
root.joinpath("Test_root.py").write_text("def test_1(): pass", encoding="utf-8")
|
||||
sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass", encoding="utf-8")
|
||||
sub2_test.joinpath("test_sub2.py").write_text(
|
||||
"def test_3(): pass", encoding="utf-8"
|
||||
)
|
||||
|
||||
# Execute from .
|
||||
result = pytester.runpytest("-v", "-s")
|
||||
|
||||
@@ -287,7 +287,8 @@ class TestFillFixtures:
|
||||
def spam():
|
||||
return 'spam'
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
testfile = subdir.joinpath("test_spam.py")
|
||||
testfile.write_text(
|
||||
@@ -296,7 +297,8 @@ class TestFillFixtures:
|
||||
def test_spam(spam):
|
||||
assert spam == "spam"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||
@@ -359,7 +361,8 @@ class TestFillFixtures:
|
||||
def spam(request):
|
||||
return request.param
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
testfile = subdir.joinpath("test_spam.py")
|
||||
testfile.write_text(
|
||||
@@ -371,7 +374,8 @@ class TestFillFixtures:
|
||||
assert spam == params['spam']
|
||||
params['spam'] += 1
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["*3 passed*"])
|
||||
@@ -403,7 +407,8 @@ class TestFillFixtures:
|
||||
def spam(request):
|
||||
return request.param
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
testfile = subdir.joinpath("test_spam.py")
|
||||
testfile.write_text(
|
||||
@@ -415,7 +420,8 @@ class TestFillFixtures:
|
||||
assert spam == params['spam']
|
||||
params['spam'] += 1
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["*3 passed*"])
|
||||
@@ -1037,10 +1043,11 @@ class TestRequestBasic:
|
||||
def arg1():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
p = b.joinpath("test_module.py")
|
||||
p.write_text("def test_func(arg1): pass")
|
||||
p.write_text("def test_func(arg1): pass", encoding="utf-8")
|
||||
result = pytester.runpytest(p, "--fixtures")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
@@ -1617,7 +1624,8 @@ class TestFixtureManagerParseFactories:
|
||||
def one():
|
||||
return 1
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
package.joinpath("test_x.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -1625,7 +1633,8 @@ class TestFixtureManagerParseFactories:
|
||||
def test_x(one):
|
||||
assert one == 1
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub = package.joinpath("sub")
|
||||
sub.mkdir()
|
||||
@@ -1638,7 +1647,8 @@ class TestFixtureManagerParseFactories:
|
||||
def one():
|
||||
return 2
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub.joinpath("test_y.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -1646,7 +1656,8 @@ class TestFixtureManagerParseFactories:
|
||||
def test_x(one):
|
||||
assert one == 2
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reprec = pytester.inline_run()
|
||||
reprec.assertoutcome(passed=2)
|
||||
@@ -1671,7 +1682,8 @@ class TestFixtureManagerParseFactories:
|
||||
def teardown_module():
|
||||
values[:] = []
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
package.joinpath("test_x.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -1680,7 +1692,8 @@ class TestFixtureManagerParseFactories:
|
||||
def test_x():
|
||||
assert values == ["package"]
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
package = pytester.mkdir("package2")
|
||||
package.joinpath("__init__.py").write_text(
|
||||
@@ -1692,7 +1705,8 @@ class TestFixtureManagerParseFactories:
|
||||
def teardown_module():
|
||||
values[:] = []
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
package.joinpath("test_x.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -1701,7 +1715,8 @@ class TestFixtureManagerParseFactories:
|
||||
def test_x():
|
||||
assert values == ["package2"]
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reprec = pytester.inline_run()
|
||||
reprec.assertoutcome(passed=2)
|
||||
@@ -1714,7 +1729,7 @@ class TestFixtureManagerParseFactories:
|
||||
)
|
||||
pytester.syspathinsert(pytester.path.name)
|
||||
package = pytester.mkdir("package")
|
||||
package.joinpath("__init__.py").write_text("")
|
||||
package.joinpath("__init__.py").write_text("", encoding="utf-8")
|
||||
package.joinpath("conftest.py").write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
@@ -1731,7 +1746,8 @@ class TestFixtureManagerParseFactories:
|
||||
yield values
|
||||
values.pop()
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
package.joinpath("test_x.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -1742,7 +1758,8 @@ class TestFixtureManagerParseFactories:
|
||||
def test_package(one):
|
||||
assert values == ["package-auto", "package"]
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reprec = pytester.inline_run()
|
||||
reprec.assertoutcome(passed=2)
|
||||
@@ -1892,8 +1909,12 @@ class TestAutouseDiscovery:
|
||||
"""
|
||||
)
|
||||
conftest.rename(a.joinpath(conftest.name))
|
||||
a.joinpath("test_something.py").write_text("def test_func(): pass")
|
||||
b.joinpath("test_otherthing.py").write_text("def test_func(): pass")
|
||||
a.joinpath("test_something.py").write_text(
|
||||
"def test_func(): pass", encoding="utf-8"
|
||||
)
|
||||
b.joinpath("test_otherthing.py").write_text(
|
||||
"def test_func(): pass", encoding="utf-8"
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
"""
|
||||
@@ -1939,7 +1960,8 @@ class TestAutouseManagement:
|
||||
import sys
|
||||
sys._myapp = "hello"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub = pkgdir.joinpath("tests")
|
||||
sub.mkdir()
|
||||
@@ -1952,7 +1974,8 @@ class TestAutouseManagement:
|
||||
def test_app():
|
||||
assert sys._myapp == "hello"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reprec = pytester.inline_run("-s")
|
||||
reprec.assertoutcome(passed=1)
|
||||
@@ -2882,7 +2905,7 @@ class TestFixtureMarker:
|
||||
def browser(request):
|
||||
|
||||
def finalize():
|
||||
sys.stdout.write_text('Finalized')
|
||||
sys.stdout.write_text('Finalized', encoding='utf-8')
|
||||
request.addfinalizer(finalize)
|
||||
return {}
|
||||
"""
|
||||
@@ -2900,7 +2923,8 @@ class TestFixtureMarker:
|
||||
def test_browser(browser):
|
||||
assert browser['visited'] is True
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reprec = pytester.runpytest("-s")
|
||||
for test in ["test_browser"]:
|
||||
@@ -3855,7 +3879,8 @@ class TestParameterizedSubRequest:
|
||||
def fix_with_param(request):
|
||||
return request.param
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
testfile = tests_dir.joinpath("test_foos.py")
|
||||
@@ -3867,7 +3892,8 @@ class TestParameterizedSubRequest:
|
||||
def test_foo(request):
|
||||
request.getfixturevalue('fix_with_param')
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
os.chdir(tests_dir)
|
||||
@@ -4196,7 +4222,7 @@ class TestScopeOrdering:
|
||||
└── test_2.py
|
||||
"""
|
||||
root = pytester.mkdir("root")
|
||||
root.joinpath("__init__.py").write_text("values = []")
|
||||
root.joinpath("__init__.py").write_text("values = []", encoding="utf-8")
|
||||
sub1 = root.joinpath("sub1")
|
||||
sub1.mkdir()
|
||||
sub1.joinpath("__init__.py").touch()
|
||||
@@ -4211,7 +4237,8 @@ class TestScopeOrdering:
|
||||
yield values
|
||||
assert values.pop() == "pre-sub1"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub1.joinpath("test_1.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -4220,7 +4247,8 @@ class TestScopeOrdering:
|
||||
def test_1(fix):
|
||||
assert values == ["pre-sub1"]
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub2 = root.joinpath("sub2")
|
||||
sub2.mkdir()
|
||||
@@ -4236,7 +4264,8 @@ class TestScopeOrdering:
|
||||
yield values
|
||||
assert values.pop() == "pre-sub2"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub2.joinpath("test_2.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -4245,7 +4274,8 @@ class TestScopeOrdering:
|
||||
def test_2(fix):
|
||||
assert values == ["pre-sub2"]
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reprec = pytester.inline_run()
|
||||
reprec.assertoutcome(passed=2)
|
||||
|
||||
@@ -1443,7 +1443,8 @@ class TestMetafuncFunctional:
|
||||
def pytest_generate_tests(metafunc):
|
||||
assert metafunc.function.__name__ == "test_1"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub2.joinpath("conftest.py").write_text(
|
||||
textwrap.dedent(
|
||||
@@ -1451,10 +1452,15 @@ class TestMetafuncFunctional:
|
||||
def pytest_generate_tests(metafunc):
|
||||
assert metafunc.function.__name__ == "test_2"
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sub1.joinpath("test_in_sub1.py").write_text(
|
||||
"def test_1(): pass", encoding="utf-8"
|
||||
)
|
||||
sub2.joinpath("test_in_sub2.py").write_text(
|
||||
"def test_2(): pass", encoding="utf-8"
|
||||
)
|
||||
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
|
||||
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
|
||||
result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1)
|
||||
result.assert_outcomes(passed=3)
|
||||
|
||||
|
||||
@@ -1392,14 +1392,14 @@ def test_sequence_comparison_uses_repr(pytester: Pytester) -> None:
|
||||
def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"])
|
||||
a = pytester.mkdir("a")
|
||||
a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2")
|
||||
a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8")
|
||||
a.joinpath("conftest.py").write_text(
|
||||
'def pytest_assertrepr_compare(): return ["summary a"]'
|
||||
'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8"
|
||||
)
|
||||
b = pytester.mkdir("b")
|
||||
b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2")
|
||||
b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8")
|
||||
b.joinpath("conftest.py").write_text(
|
||||
'def pytest_assertrepr_compare(): return ["summary b"]'
|
||||
'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8"
|
||||
)
|
||||
|
||||
result = pytester.runpytest()
|
||||
|
||||
@@ -160,7 +160,8 @@ class TestAssertionRewrite:
|
||||
"def special_asserter():\n"
|
||||
" def special_assert(x, y):\n"
|
||||
" assert x == y\n"
|
||||
" return special_assert\n"
|
||||
" return special_assert\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
pytester.makeconftest('pytest_plugins = ["plugin"]')
|
||||
pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n")
|
||||
@@ -173,7 +174,9 @@ class TestAssertionRewrite:
|
||||
pytester.makepyfile(test_y="x = 1")
|
||||
xdir = pytester.mkdir("x")
|
||||
pytester.mkpydir(str(xdir.joinpath("test_Y")))
|
||||
xdir.joinpath("test_Y").joinpath("__init__.py").write_text("x = 2")
|
||||
xdir.joinpath("test_Y").joinpath("__init__.py").write_text(
|
||||
"x = 2", encoding="utf-8"
|
||||
)
|
||||
pytester.makepyfile(
|
||||
"import test_y\n"
|
||||
"import test_Y\n"
|
||||
@@ -726,7 +729,7 @@ class TestAssertionRewrite:
|
||||
|
||||
class TestRewriteOnImport:
|
||||
def test_pycache_is_a_file(self, pytester: Pytester) -> None:
|
||||
pytester.path.joinpath("__pycache__").write_text("Hello")
|
||||
pytester.path.joinpath("__pycache__").write_text("Hello", encoding="utf-8")
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_rewritten():
|
||||
@@ -903,7 +906,8 @@ def test_rewritten():
|
||||
pkg.joinpath("test_blah.py").write_text(
|
||||
"""
|
||||
def test_rewritten():
|
||||
assert "@py_builtins" in globals()"""
|
||||
assert "@py_builtins" in globals()""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert pytester.runpytest().ret == 0
|
||||
|
||||
@@ -1066,7 +1070,7 @@ class TestAssertionRewriteHookDetails:
|
||||
source = tmp_path / "source.py"
|
||||
pyc = Path(str(source) + "c")
|
||||
|
||||
source.write_text("def test(): pass")
|
||||
source.write_text("def test(): pass", encoding="utf-8")
|
||||
py_compile.compile(str(source), str(pyc))
|
||||
|
||||
contents = pyc.read_bytes()
|
||||
@@ -1092,7 +1096,7 @@ class TestAssertionRewriteHookDetails:
|
||||
fn = tmp_path / "source.py"
|
||||
pyc = Path(str(fn) + "c")
|
||||
|
||||
fn.write_text("def test(): assert True")
|
||||
fn.write_text("def test(): assert True", encoding="utf-8")
|
||||
|
||||
source_stat, co = _rewrite_test(fn, config)
|
||||
_write_pyc(state, co, source_stat, pyc)
|
||||
@@ -1157,7 +1161,7 @@ class TestAssertionRewriteHookDetails:
|
||||
return False
|
||||
|
||||
def rewrite_self():
|
||||
with open(__file__, 'w') as self:
|
||||
with open(__file__, 'w', encoding='utf-8') as self:
|
||||
self.write('def reloaded(): return True')
|
||||
""",
|
||||
test_fun="""
|
||||
@@ -1187,9 +1191,10 @@ class TestAssertionRewriteHookDetails:
|
||||
data = pkgutil.get_data('foo.test_foo', 'data.txt')
|
||||
assert data == b'Hey'
|
||||
"""
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
path.joinpath("data.txt").write_text("Hey")
|
||||
path.joinpath("data.txt").write_text("Hey", encoding="utf-8")
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
@@ -1436,6 +1441,118 @@ class TestIssue10743:
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 8), reason="walrus operator not available in py<38"
|
||||
)
|
||||
class TestIssue11028:
|
||||
def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_in_string():
|
||||
assert (obj := "foo") in obj
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_operator_in_operand_json_dumps(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import json
|
||||
|
||||
def test_json_encoder():
|
||||
assert (obj := "foo") in json.dumps(obj)
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_operator_equals_operand_function(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def f(a):
|
||||
return a
|
||||
|
||||
def test_call_other_function_arg():
|
||||
assert (obj := "foo") == f(obj)
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_operator_equals_operand_function_keyword_arg(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def f(a='test'):
|
||||
return a
|
||||
|
||||
def test_call_other_function_k_arg():
|
||||
assert (obj := "foo") == f(a=obj)
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_operator_equals_operand_function_arg_as_function(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def f(a='test'):
|
||||
return a
|
||||
|
||||
def test_function_of_function():
|
||||
assert (obj := "foo") == f(f(obj))
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_operator_gt_operand_function(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def add_one(a):
|
||||
return a + 1
|
||||
|
||||
def test_gt():
|
||||
assert (obj := 4) > add_one(obj)
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
|
||||
|
||||
|
||||
class TestIssue11239:
|
||||
@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="Only Python 3.8+")
|
||||
def test_assertion_walrus_different_test_cases(self, pytester: Pytester) -> None:
|
||||
"""Regression for (#11239)
|
||||
|
||||
Walrus operator rewriting would leak to separate test cases if they used the same variables.
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_1():
|
||||
state = {"x": 2}.get("x")
|
||||
assert state is not None
|
||||
|
||||
def test_2():
|
||||
db = {"x": 2}
|
||||
assert (state := db.get("x")) is not None
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user