Merge branch 'main' into Improvement-catch-duplicate-values-when-determining-param-indices-in-metafunc-parametrize
This commit is contained in:
commit
9b10ae75fd
|
@ -22,7 +22,7 @@ jobs:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
|
@ -1,44 +1,60 @@
|
||||||
name: deploy
|
name: deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
inputs:
|
||||||
# These tags are protected, see:
|
version:
|
||||||
# https://github.com/pytest-dev/pytest/settings/tag_protection
|
description: 'Release version'
|
||||||
- "[0-9]+.[0-9]+.[0-9]+"
|
required: true
|
||||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
default: '1.2.3'
|
||||||
|
|
||||||
|
|
||||||
# Set permissions at the job level.
|
# Set permissions at the job level.
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
package:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v1.5
|
uses: hynek/build-and-inspect-python-package@v1.5.4
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: github.repository == 'pytest-dev/pytest'
|
if: github.repository == 'pytest-dev/pytest'
|
||||||
needs: [build]
|
needs: [package]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment: deploy
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download Package
|
- name: Download Package
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Packages
|
name: Packages
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Publish package to PyPI
|
- name: Publish package to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.8
|
uses: pypa/gh-action-pypi-publish@v1.8.11
|
||||||
|
|
||||||
|
- 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 }} ${{ github.event.inputs.version }} ${{ github.sha }}
|
||||||
|
git push origin ${{ github.event.inputs.version }}
|
||||||
|
|
||||||
release-notes:
|
release-notes:
|
||||||
|
|
||||||
|
@ -51,16 +67,16 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
|
|
@ -27,12 +27,12 @@ jobs:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.8"
|
python-version: "3.8"
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
debug-only: false
|
debug-only: false
|
||||||
days-before-issue-stale: 14
|
days-before-issue-stale: 14
|
||||||
|
|
|
@ -27,7 +27,19 @@ concurrency:
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Build and Check Package
|
||||||
|
uses: hynek/build-and-inspect-python-package@v1.5.4
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
needs: [package]
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -58,7 +70,6 @@ jobs:
|
||||||
"macos-py310",
|
"macos-py310",
|
||||||
"macos-py312",
|
"macos-py312",
|
||||||
|
|
||||||
"docs",
|
|
||||||
"doctesting",
|
"doctesting",
|
||||||
"plugins",
|
"plugins",
|
||||||
]
|
]
|
||||||
|
@ -145,14 +156,10 @@ jobs:
|
||||||
tox_env: "py312-xdist"
|
tox_env: "py312-xdist"
|
||||||
|
|
||||||
- name: "plugins"
|
- name: "plugins"
|
||||||
python: "3.9"
|
python: "3.12"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "plugins"
|
tox_env: "plugins"
|
||||||
|
|
||||||
- name: "docs"
|
|
||||||
python: "3.8"
|
|
||||||
os: ubuntu-latest
|
|
||||||
tox_env: "docs"
|
|
||||||
- name: "doctesting"
|
- name: "doctesting"
|
||||||
python: "3.8"
|
python: "3.8"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -160,13 +167,19 @@ jobs:
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Download Package
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Packages
|
||||||
|
path: dist
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python }}
|
- name: Set up Python ${{ matrix.python }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||||
|
@ -178,11 +191,13 @@ jobs:
|
||||||
|
|
||||||
- name: Test without coverage
|
- name: Test without coverage
|
||||||
if: "! matrix.use_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
|
- name: Test with coverage
|
||||||
if: "matrix.use_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
|
- name: Generate coverage report
|
||||||
if: "matrix.use_coverage"
|
if: "matrix.use_coverage"
|
||||||
|
@ -196,10 +211,3 @@ jobs:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
verbose: true
|
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
|
|
||||||
|
|
|
@ -20,14 +20,14 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.8"
|
python-version: "3.11"
|
||||||
cache: pip
|
cache: pip
|
||||||
- name: requests-cache
|
- name: requests-cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.7.0
|
rev: 23.11.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet]
|
||||||
- repo: https://github.com/asottile/blacken-docs
|
- repo: https://github.com/asottile/blacken-docs
|
||||||
rev: 1.15.0
|
rev: 1.16.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
additional_dependencies: [black==23.7.0]
|
additional_dependencies: [black==23.7.0]
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
|
@ -21,7 +21,7 @@ repos:
|
||||||
exclude: _pytest/(debugging|hookspec).py
|
exclude: _pytest/(debugging|hookspec).py
|
||||||
language_version: python3
|
language_version: python3
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/PyCQA/autoflake
|
||||||
rev: v2.2.0
|
rev: v2.2.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: autoflake
|
- id: autoflake
|
||||||
name: autoflake
|
name: autoflake
|
||||||
|
@ -37,17 +37,17 @@ repos:
|
||||||
- flake8-typing-imports==1.12.0
|
- flake8-typing-imports==1.12.0
|
||||||
- flake8-docstrings==1.5.0
|
- flake8-docstrings==1.5.0
|
||||||
- repo: https://github.com/asottile/reorder-python-imports
|
- repo: https://github.com/asottile/reorder-python-imports
|
||||||
rev: v3.10.0
|
rev: v3.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
args: ['--application-directories=.:src', --py38-plus]
|
args: ['--application-directories=.:src', --py38-plus]
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.10.1
|
rev: v3.15.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py38-plus]
|
args: [--py38-plus]
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
rev: v2.4.0
|
rev: v2.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: setup-cfg-fmt
|
- id: setup-cfg-fmt
|
||||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||||
|
@ -56,7 +56,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-use-type-annotations
|
- id: python-use-type-annotations
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.4.1
|
rev: v1.7.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|
|
@ -9,6 +9,10 @@ python:
|
||||||
path: .
|
path: .
|
||||||
- requirements: doc/en/requirements.txt
|
- requirements: doc/en/requirements.txt
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: doc/en/conf.py
|
||||||
|
fail_on_warning: true
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-20.04
|
||||||
tools:
|
tools:
|
||||||
|
|
17
AUTHORS
17
AUTHORS
|
@ -48,6 +48,7 @@ Ariel Pillemer
|
||||||
Armin Rigo
|
Armin Rigo
|
||||||
Aron Coyle
|
Aron Coyle
|
||||||
Aron Curzon
|
Aron Curzon
|
||||||
|
Arthur Richard
|
||||||
Ashish Kurmi
|
Ashish Kurmi
|
||||||
Aviral Verma
|
Aviral Verma
|
||||||
Aviv Palivoda
|
Aviv Palivoda
|
||||||
|
@ -56,6 +57,7 @@ Barney Gale
|
||||||
Ben Gartner
|
Ben Gartner
|
||||||
Ben Webb
|
Ben Webb
|
||||||
Benjamin Peterson
|
Benjamin Peterson
|
||||||
|
Benjamin Schubert
|
||||||
Bernard Pratz
|
Bernard Pratz
|
||||||
Bo Wu
|
Bo Wu
|
||||||
Bob Ippolito
|
Bob Ippolito
|
||||||
|
@ -143,6 +145,7 @@ Feng Ma
|
||||||
Florian Bruhin
|
Florian Bruhin
|
||||||
Florian Dahlitz
|
Florian Dahlitz
|
||||||
Floris Bruynooghe
|
Floris Bruynooghe
|
||||||
|
Fraser Stark
|
||||||
Gabriel Landau
|
Gabriel Landau
|
||||||
Gabriel Reis
|
Gabriel Reis
|
||||||
Garvit Shubham
|
Garvit Shubham
|
||||||
|
@ -170,6 +173,7 @@ Ian Lesperance
|
||||||
Ilya Konstantinov
|
Ilya Konstantinov
|
||||||
Ionuț Turturică
|
Ionuț Turturică
|
||||||
Isaac Virshup
|
Isaac Virshup
|
||||||
|
Israel Fruchter
|
||||||
Itxaso Aizpurua
|
Itxaso Aizpurua
|
||||||
Iwan Briquemont
|
Iwan Briquemont
|
||||||
Jaap Broekhuizen
|
Jaap Broekhuizen
|
||||||
|
@ -185,6 +189,7 @@ Javier Romero
|
||||||
Jeff Rackauckas
|
Jeff Rackauckas
|
||||||
Jeff Widman
|
Jeff Widman
|
||||||
Jenni Rinker
|
Jenni Rinker
|
||||||
|
Jens Tröger
|
||||||
John Eddie Ayson
|
John Eddie Ayson
|
||||||
John Litborn
|
John Litborn
|
||||||
John Towler
|
John Towler
|
||||||
|
@ -233,6 +238,7 @@ Maho
|
||||||
Maik Figura
|
Maik Figura
|
||||||
Mandeep Bhutani
|
Mandeep Bhutani
|
||||||
Manuel Krebber
|
Manuel Krebber
|
||||||
|
Marc Mueller
|
||||||
Marc Schlaich
|
Marc Schlaich
|
||||||
Marcelo Duarte Trevisani
|
Marcelo Duarte Trevisani
|
||||||
Marcin Bachry
|
Marcin Bachry
|
||||||
|
@ -263,6 +269,7 @@ Michal Wajszczuk
|
||||||
Michał Zięba
|
Michał Zięba
|
||||||
Mickey Pashov
|
Mickey Pashov
|
||||||
Mihai Capotă
|
Mihai Capotă
|
||||||
|
Mihail Milushev
|
||||||
Mike Hoyle (hoylemd)
|
Mike Hoyle (hoylemd)
|
||||||
Mike Lundy
|
Mike Lundy
|
||||||
Milan Lesnek
|
Milan Lesnek
|
||||||
|
@ -270,6 +277,7 @@ Miro Hrončok
|
||||||
Nathaniel Compton
|
Nathaniel Compton
|
||||||
Nathaniel Waisbrot
|
Nathaniel Waisbrot
|
||||||
Ned Batchelder
|
Ned Batchelder
|
||||||
|
Neil Martin
|
||||||
Neven Mundar
|
Neven Mundar
|
||||||
Nicholas Devenish
|
Nicholas Devenish
|
||||||
Nicholas Murphy
|
Nicholas Murphy
|
||||||
|
@ -287,6 +295,7 @@ Ondřej Súkup
|
||||||
Oscar Benjamin
|
Oscar Benjamin
|
||||||
Parth Patel
|
Parth Patel
|
||||||
Patrick Hayes
|
Patrick Hayes
|
||||||
|
Patrick Lannigan
|
||||||
Paul Müller
|
Paul Müller
|
||||||
Paul Reece
|
Paul Reece
|
||||||
Pauli Virtanen
|
Pauli Virtanen
|
||||||
|
@ -326,26 +335,32 @@ Ronny Pfannschmidt
|
||||||
Ross Lawley
|
Ross Lawley
|
||||||
Ruaridh Williamson
|
Ruaridh Williamson
|
||||||
Russel Winder
|
Russel Winder
|
||||||
|
Ryan Puddephatt
|
||||||
Ryan Wooden
|
Ryan Wooden
|
||||||
Sadra Barikbin
|
Sadra Barikbin
|
||||||
Saiprasad Kale
|
Saiprasad Kale
|
||||||
Samuel Colvin
|
Samuel Colvin
|
||||||
Samuel Dion-Girardeau
|
Samuel Dion-Girardeau
|
||||||
Samuel Searles-Bryant
|
Samuel Searles-Bryant
|
||||||
|
Samuel Therrien (Avasam)
|
||||||
Samuele Pedroni
|
Samuele Pedroni
|
||||||
Sanket Duthade
|
Sanket Duthade
|
||||||
Sankt Petersbug
|
Sankt Petersbug
|
||||||
Saravanan Padmanaban
|
Saravanan Padmanaban
|
||||||
|
Sean Malloy
|
||||||
Segev Finer
|
Segev Finer
|
||||||
Serhii Mozghovyi
|
Serhii Mozghovyi
|
||||||
Seth Junot
|
Seth Junot
|
||||||
Shantanu Jain
|
Shantanu Jain
|
||||||
|
Sharad Nair
|
||||||
Shubham Adep
|
Shubham Adep
|
||||||
|
Simon Blanchard
|
||||||
Simon Gomizelj
|
Simon Gomizelj
|
||||||
Simon Holesch
|
Simon Holesch
|
||||||
Simon Kerr
|
Simon Kerr
|
||||||
Skylar Downes
|
Skylar Downes
|
||||||
Srinivas Reddy Thatiparthy
|
Srinivas Reddy Thatiparthy
|
||||||
|
Stefaan Lippens
|
||||||
Stefan Farmbauer
|
Stefan Farmbauer
|
||||||
Stefan Scherfke
|
Stefan Scherfke
|
||||||
Stefan Zimmermann
|
Stefan Zimmermann
|
||||||
|
@ -359,6 +374,7 @@ Tadek Teleżyński
|
||||||
Takafumi Arakaki
|
Takafumi Arakaki
|
||||||
Taneli Hukkinen
|
Taneli Hukkinen
|
||||||
Tanvi Mehta
|
Tanvi Mehta
|
||||||
|
Tanya Agarwal
|
||||||
Tarcisio Fischer
|
Tarcisio Fischer
|
||||||
Tareq Alayan
|
Tareq Alayan
|
||||||
Tatiana Ovary
|
Tatiana Ovary
|
||||||
|
@ -379,6 +395,7 @@ Tor Colvin
|
||||||
Trevor Bekolay
|
Trevor Bekolay
|
||||||
Tushar Sadhwani
|
Tushar Sadhwani
|
||||||
Tyler Goodlet
|
Tyler Goodlet
|
||||||
|
Tyler Smart
|
||||||
Tzu-ping Chung
|
Tzu-ping Chung
|
||||||
Vasily Kuznetsov
|
Vasily Kuznetsov
|
||||||
Victor Maryama
|
Victor Maryama
|
||||||
|
|
|
@ -50,7 +50,7 @@ Fix bugs
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
|
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.
|
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
|
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
|
||||||
|
@ -197,8 +197,9 @@ Short version
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
#. Fork the repository.
|
#. Fork the repository.
|
||||||
|
#. Fetch tags from upstream if necessary (if you cloned only main `git fetch --tags https://github.com/pytest-dev/pytest`).
|
||||||
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
||||||
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
#. Follow `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_ for naming.
|
||||||
#. Tests are run using ``tox``::
|
#. Tests are run using ``tox``::
|
||||||
|
|
||||||
tox -e linting,py39
|
tox -e linting,py39
|
||||||
|
@ -236,6 +237,7 @@ Here is a simple overview, with pytest-specific bits:
|
||||||
|
|
||||||
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
|
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
|
||||||
$ cd pytest
|
$ cd pytest
|
||||||
|
$ git fetch --tags https://github.com/pytest-dev/pytest
|
||||||
# now, create your own branch off "main":
|
# now, create your own branch off "main":
|
||||||
|
|
||||||
$ git checkout -b your-bugfix-branch-name main
|
$ git checkout -b your-bugfix-branch-name main
|
||||||
|
@ -280,7 +282,7 @@ Here is a simple overview, with pytest-specific bits:
|
||||||
This command will run tests via the "tox" tool against Python 3.9
|
This command will run tests via the "tox" tool against Python 3.9
|
||||||
and also perform "lint" coding-style checks.
|
and also perform "lint" coding-style checks.
|
||||||
|
|
||||||
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming.
|
#. You can now edit your local working copy and run the tests again as necessary. Please follow `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_ for naming.
|
||||||
|
|
||||||
You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
|
You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
|
||||||
(e.g. enter pdb on failure) to pytest you can do::
|
(e.g. enter pdb on failure) to pytest you can do::
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
:target: https://codecov.io/gh/pytest-dev/pytest
|
:target: https://codecov.io/gh/pytest-dev/pytest
|
||||||
:alt: Code coverage Status
|
:alt: Code coverage Status
|
||||||
|
|
||||||
.. image:: https://github.com/pytest-dev/pytest/workflows/test/badge.svg
|
.. image:: https://github.com/pytest-dev/pytest/actions/workflows/test.yml/badge.svg
|
||||||
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
|
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
|
||||||
|
|
||||||
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg
|
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg
|
||||||
|
|
|
@ -133,14 +133,12 @@ Releasing
|
||||||
|
|
||||||
Both automatic and manual processes described above follow the same steps from this point onward.
|
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
|
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
|
||||||
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
|
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
|
||||||
|
as source.
|
||||||
|
|
||||||
git fetch upstream
|
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
and tag the repository.
|
||||||
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>`_.
|
|
||||||
|
|
||||||
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
||||||
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
Fixed that fake intermediate modules generated by ``--import-mode=importlib`` would not include the
|
|
||||||
child modules as attributes of the parent modules.
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Added :func:`ExceptionInfo.group_contains() <pytest.ExceptionInfo.group_contains>`, an assertion
|
||||||
|
helper that tests if an `ExceptionGroup` contains a matching exception.
|
|
@ -1,2 +0,0 @@
|
||||||
markers are now considered in the reverse mro order to ensure base class markers are considered first
|
|
||||||
this resolves a regression.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Test functions returning a value other than None will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Added more comprehensive set assertion rewrites for comparisons other than equality ``==``, with
|
||||||
|
the following operations now providing better failure messages: ``!=``, ``<=``, ``>=``, ``<``, and ``>``.
|
|
@ -1 +0,0 @@
|
||||||
Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
|
|
|
@ -1,2 +0,0 @@
|
||||||
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.
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Use pytestconfig instead of request.config in cache example
|
||||||
|
|
||||||
|
to be consistent with the API documentation.
|
|
@ -0,0 +1 @@
|
||||||
|
Updated documentation and tests to refer to hyphonated options: replaced ``--junitxml`` with ``--junit-xml`` and ``--collectonly`` with ``--collect-only``.
|
|
@ -1,2 +1 @@
|
||||||
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27
|
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27 <https://devguide.python.org/versions/>`__.
|
||||||
<https://devguide.python.org/versions/>`__.
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
(This entry is meant to assist plugins which access private pytest internals to instantiate ``FixtureRequest`` objects.)
|
||||||
|
|
||||||
|
:class:`~pytest.FixtureRequest` is now an abstract class which can't be instantiated directly.
|
||||||
|
A new concrete ``TopRequest`` subclass of ``FixtureRequest`` has been added for the ``request`` fixture in test functions,
|
||||||
|
as counterpart to the existing ``SubRequest`` subclass for the ``request`` fixture in fixture functions.
|
|
@ -0,0 +1,11 @@
|
||||||
|
Sanitized the handling of the ``default`` parameter when defining configuration options.
|
||||||
|
|
||||||
|
Previously if ``default`` was not supplied for :meth:`parser.addini <pytest.Parser.addini>` and the configuration option value was not defined in a test session, then calls to :func:`config.getini <pytest.Config.getini>` returned an *empty list* or an *empty string* depending on whether ``type`` was supplied or not respectively, which is clearly incorrect. Also, ``None`` was not honored even if ``default=None`` was used explicitly while defining the option.
|
||||||
|
|
||||||
|
Now the behavior of :meth:`parser.addini <pytest.Parser.addini>` is as follows:
|
||||||
|
|
||||||
|
* If ``default`` is NOT passed but ``type`` is provided, then a type-specific default will be returned. For example ``type=bool`` will return ``False``, ``type=str`` will return ``""``, etc.
|
||||||
|
* If ``default=None`` is passed and the option is not defined in a test session, then ``None`` will be returned, regardless of the ``type``.
|
||||||
|
* If neither ``default`` nor ``type`` are provided, assume ``type=str`` and return ``""`` as default (this is as per previous behavior).
|
||||||
|
|
||||||
|
The team decided to not introduce a deprecation period for this change, as doing so would be complicated both in terms of communicating this to the community as well as implementing it, and also because the team believes this change should not break existing plugins except in rare cases.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Logging to a file using the ``--log-file`` option will use ``--log-level``, ``--log-format`` and ``--log-date-format`` as fallback
|
||||||
|
if ``--log-file-level``, ``--log-file-format`` and ``--log-file-date-format`` are not provided respectively.
|
|
@ -0,0 +1,3 @@
|
||||||
|
The :fixture:`pytester` fixture now uses the :fixture:`monkeypatch` fixture to manage the current working directory.
|
||||||
|
If you use ``pytester`` in combination with :func:`monkeypatch.undo() <pytest.MonkeyPatch.undo>`, the CWD might get restored.
|
||||||
|
Use :func:`monkeypatch.context() <pytest.MonkeyPatch.context>` instead.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Corrected the spelling of ``Config.ArgsSource.INVOCATION_DIR``.
|
||||||
|
The previous spelling ``INCOVATION_DIR`` remains as an alias.
|
|
@ -0,0 +1 @@
|
||||||
|
pluggy>=1.3.0 is now required. This adds typing to :class:`~pytest.PytestPluginManager`.
|
|
@ -0,0 +1,5 @@
|
||||||
|
Added the new :confval:`verbosity_assertions` configuration option for fine-grained control of failed assertions verbosity.
|
||||||
|
|
||||||
|
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
|
||||||
|
|
||||||
|
For plugin authors, :attr:`config.get_verbosity <pytest.Config.get_verbosity>` can be used to retrieve the verbosity level for a specific verbosity type.
|
|
@ -0,0 +1 @@
|
||||||
|
:func:`pytest.deprecated_call` now also considers warnings of type :class:`FutureWarning`.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Parametrized tests now *really do* ensure that the ids given to each input are unique - for
|
||||||
|
example, ``a, a, a0`` now results in ``a1, a2, a0`` instead of the previous (buggy) ``a0, a1, a0``.
|
||||||
|
This necessarily means changing nodeids where these were previously colliding, and for
|
||||||
|
readability adds an underscore when non-unique ids end in a number.
|
|
@ -0,0 +1,3 @@
|
||||||
|
Improved very verbose diff output to color it as a diff instead of only red.
|
||||||
|
|
||||||
|
Improved the error reporting to better separate each section.
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed crash when using an empty string for the same parametrized value more than once.
|
|
@ -0,0 +1 @@
|
||||||
|
Handle an edge case where :data:`sys.stderr` and :data:`sys.__stderr__` might already be closed when :ref:`faulthandler` is tearing down.
|
|
@ -0,0 +1 @@
|
||||||
|
Improved the documentation and type signature for :func:`pytest.mark.xfail <pytest.mark.xfail>`'s ``condition`` param to use ``False`` as the default value.
|
|
@ -0,0 +1,2 @@
|
||||||
|
Added :func:`LogCaptureFixture.filtering() <pytest.LogCaptureFixture.filtering>` context manager that
|
||||||
|
adds a given :class:`logging.Filter` object to the caplog fixture.
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed the selftests to pass correctly if ``FORCE_COLOR``, ``NO_COLOR`` or ``PY_COLORS`` is set in the calling environment.
|
|
@ -0,0 +1,3 @@
|
||||||
|
pytest's ``setup.py`` file is removed.
|
||||||
|
If you relied on this file, e.g. to install pytest using ``setup.py install``,
|
||||||
|
please see `Why you shouldn't invoke setup.py directly <https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html#summary>`_ for alternatives.
|
|
@ -0,0 +1,3 @@
|
||||||
|
The classes :class:`~_pytest.nodes.Node`, :class:`~pytest.Collector`, :class:`~pytest.Item`, :class:`~pytest.File`, :class:`~_pytest.nodes.FSCollector` are now marked abstract (see :mod:`abc`).
|
||||||
|
|
||||||
|
We do not expect this change to affect users and plugin authors, it will only cause errors when the code is already wrong or problematic.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Improved the very verbose diff for every standard library container types: the indentation is now consistent and the markers are on their own separate lines, which should reduce the diffs shown to users.
|
||||||
|
|
||||||
|
Previously, the default python pretty printer was used to generate the output, which puts opening and closing
|
||||||
|
markers on the same line as the first/last entry, in addition to not having consistent indentation.
|
|
@ -0,0 +1 @@
|
||||||
|
Removes unhelpful error message from assertion rewrite mechanism when exceptions raised in __iter__ methods, and instead treats them as un-iterable.
|
|
@ -1,4 +1,4 @@
|
||||||
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
|
:func:`~pytest.warns` now re-emits unmatched warnings when the context
|
||||||
closes -- previously it would consume all warnings, hiding those that were not
|
closes -- previously it would consume all warnings, hiding those that were not
|
||||||
matched by the function.
|
matched by the function.
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||||
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
|
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
|
||||||
|
|
||||||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||||
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc).
|
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junit-xml``, improved colors in terminal, etc).
|
||||||
* ``bugfix``: fixes a bug.
|
* ``bugfix``: fixes a bug.
|
||||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||||
* ``deprecation``: feature deprecation.
|
* ``deprecation``: feature deprecation.
|
||||||
|
|
|
@ -6,6 +6,9 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-7.4.3
|
||||||
|
release-7.4.2
|
||||||
|
release-7.4.1
|
||||||
release-7.4.0
|
release-7.4.0
|
||||||
release-7.3.2
|
release-7.3.2
|
||||||
release-7.3.1
|
release-7.3.1
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -22,7 +22,7 @@ b) transitional: the old and new API don't conflict
|
||||||
|
|
||||||
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
||||||
|
|
||||||
A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationwarning`).
|
A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationWarning`).
|
||||||
|
|
||||||
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
cachedir: .pytest_cache
|
cachedir: .pytest_cache
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 0 items
|
collected 0 items
|
||||||
cache -- .../_pytest/cacheprovider.py:528
|
cache -- .../_pytest/cacheprovider.py:532
|
||||||
Return a cache object that can persist state between testing sessions.
|
Return a cache object that can persist state between testing sessions.
|
||||||
|
|
||||||
cache.get(key, default)
|
cache.get(key, default)
|
||||||
|
@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert captured.out == "hello\n"
|
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
|
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||||
namespace of doctests.
|
namespace of doctests.
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,63 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. 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)
|
pytest 7.4.0 (2023-06-23)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
@ -356,7 +413,7 @@ Improvements
|
||||||
|
|
||||||
|
|
||||||
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
||||||
enhance match comparison for :py:func:`_pytest._code.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
|
enhance match comparison for :py:func:`pytest.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
|
||||||
|
|
||||||
|
|
||||||
- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
|
- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
|
||||||
|
@ -365,7 +422,7 @@ Improvements
|
||||||
|
|
||||||
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
||||||
|
|
||||||
:mod:`tomli` is no longer a dependency on Python 3.11.
|
`tomli` is no longer a dependency on Python 3.11.
|
||||||
|
|
||||||
|
|
||||||
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
|
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
|
||||||
|
@ -400,7 +457,7 @@ Bug Fixes
|
||||||
|
|
||||||
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
|
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
|
||||||
|
|
||||||
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`pytest.Node.iter_markers` instead.
|
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` instead.
|
||||||
|
|
||||||
|
|
||||||
- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
|
- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
|
||||||
|
@ -653,7 +710,7 @@ Bug Fixes
|
||||||
- `#9355 <https://github.com/pytest-dev/pytest/issues/9355>`_: Fixed error message prints function decorators when using assert in Python 3.8 and above.
|
- `#9355 <https://github.com/pytest-dev/pytest/issues/9355>`_: Fixed error message prints function decorators when using assert in Python 3.8 and above.
|
||||||
|
|
||||||
|
|
||||||
- `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure :attr:`pytest.Config.inifile` is available during the :func:`pytest_cmdline_main <_pytest.hookspec.pytest_cmdline_main>` hook (regression during ``7.0.0rc1``).
|
- `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure `pytest.Config.inifile` is available during the :hook:`pytest_cmdline_main` hook (regression during ``7.0.0rc1``).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -798,7 +855,7 @@ Deprecations
|
||||||
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
||||||
|
|
||||||
|
|
||||||
- `#8447 <https://github.com/pytest-dev/pytest/issues/8447>`_: Defining a custom pytest node type which is both an :class:`pytest.Item <Item>` and a :class:`pytest.Collector <Collector>` (e.g. :class:`pytest.File <File>`) now issues a warning.
|
- `#8447 <https://github.com/pytest-dev/pytest/issues/8447>`_: Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
|
||||||
It was never sanely supported and triggers hard to debug errors.
|
It was never sanely supported and triggers hard to debug errors.
|
||||||
|
|
||||||
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
|
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
|
||||||
|
@ -840,7 +897,7 @@ Features
|
||||||
- `#7132 <https://github.com/pytest-dev/pytest/issues/7132>`_: Added two environment variables :envvar:`PYTEST_THEME` and :envvar:`PYTEST_THEME_MODE` to let the users customize the pygments theme used.
|
- `#7132 <https://github.com/pytest-dev/pytest/issues/7132>`_: Added two environment variables :envvar:`PYTEST_THEME` and :envvar:`PYTEST_THEME_MODE` to let the users customize the pygments theme used.
|
||||||
|
|
||||||
|
|
||||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: Added :meth:`cache.mkdir() <pytest.Cache.mkdir>`, which is similar to the existing :meth:`cache.makedir() <pytest.Cache.makedir>`,
|
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: Added :meth:`cache.mkdir() <pytest.Cache.mkdir>`, which is similar to the existing ``cache.makedir()``,
|
||||||
but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``.
|
but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``.
|
||||||
|
|
||||||
Added a ``paths`` type to :meth:`parser.addini() <pytest.Parser.addini>`,
|
Added a ``paths`` type to :meth:`parser.addini() <pytest.Parser.addini>`,
|
||||||
|
@ -866,7 +923,7 @@ Features
|
||||||
- ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`.
|
- ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`.
|
||||||
- ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`.
|
- ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`.
|
||||||
- ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`.
|
- ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`.
|
||||||
- ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.RunResult>` type used in :class:`~pytest.RunResult` and others.
|
- ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.LineMatcher>` type used in :class:`~pytest.RunResult` and others.
|
||||||
- ``pytest.TestReport`` for the :class:`TestReport <pytest.TestReport>` type used in various hooks.
|
- ``pytest.TestReport`` for the :class:`TestReport <pytest.TestReport>` type used in various hooks.
|
||||||
- ``pytest.CollectReport`` for the :class:`CollectReport <pytest.CollectReport>` type used in various hooks.
|
- ``pytest.CollectReport`` for the :class:`CollectReport <pytest.CollectReport>` type used in various hooks.
|
||||||
|
|
||||||
|
@ -899,7 +956,7 @@ Features
|
||||||
|
|
||||||
|
|
||||||
- `#8251 <https://github.com/pytest-dev/pytest/issues/8251>`_: Implement ``Node.path`` as a ``pathlib.Path``. Both the old ``fspath`` and this new attribute gets set no matter whether ``path`` or ``fspath`` (deprecated) is passed to the constructor. It is a replacement for the ``fspath`` attribute (which represents the same path as ``py.path.local``). While ``fspath`` is not deprecated yet
|
- `#8251 <https://github.com/pytest-dev/pytest/issues/8251>`_: Implement ``Node.path`` as a ``pathlib.Path``. Both the old ``fspath`` and this new attribute gets set no matter whether ``path`` or ``fspath`` (deprecated) is passed to the constructor. It is a replacement for the ``fspath`` attribute (which represents the same path as ``py.path.local``). While ``fspath`` is not deprecated yet
|
||||||
due to the ongoing migration of methods like :meth:`~_pytest.Item.reportinfo`, we expect to deprecate it in a future release.
|
due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`, we expect to deprecate it in a future release.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
||||||
|
@ -931,7 +988,7 @@ Features
|
||||||
See :ref:`plugin-stash` for details.
|
See :ref:`plugin-stash` for details.
|
||||||
|
|
||||||
|
|
||||||
- `#8953 <https://github.com/pytest-dev/pytest/issues/8953>`_: :class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a
|
- `#8953 <https://github.com/pytest-dev/pytest/issues/8953>`_: :class:`~pytest.RunResult` method :meth:`~pytest.RunResult.assert_outcomes` now accepts a
|
||||||
``warnings`` argument to assert the total number of warnings captured.
|
``warnings`` argument to assert the total number of warnings captured.
|
||||||
|
|
||||||
|
|
||||||
|
@ -943,7 +1000,7 @@ Features
|
||||||
used.
|
used.
|
||||||
|
|
||||||
|
|
||||||
- `#9113 <https://github.com/pytest-dev/pytest/issues/9113>`_: :class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a
|
- `#9113 <https://github.com/pytest-dev/pytest/issues/9113>`_: :class:`~pytest.RunResult` method :meth:`~pytest.RunResult.assert_outcomes` now accepts a
|
||||||
``deselected`` argument to assert the total number of deselected tests.
|
``deselected`` argument to assert the total number of deselected tests.
|
||||||
|
|
||||||
|
|
||||||
|
@ -956,7 +1013,7 @@ Improvements
|
||||||
|
|
||||||
- `#7480 <https://github.com/pytest-dev/pytest/issues/7480>`_: A deprecation scheduled to be removed in a major version X (e.g. pytest 7, 8, 9, ...) now uses warning category `PytestRemovedInXWarning`,
|
- `#7480 <https://github.com/pytest-dev/pytest/issues/7480>`_: A deprecation scheduled to be removed in a major version X (e.g. pytest 7, 8, 9, ...) now uses warning category `PytestRemovedInXWarning`,
|
||||||
a subclass of :class:`~pytest.PytestDeprecationWarning`,
|
a subclass of :class:`~pytest.PytestDeprecationWarning`,
|
||||||
instead of :class:`PytestDeprecationWarning` directly.
|
instead of :class:`~pytest.PytestDeprecationWarning` directly.
|
||||||
|
|
||||||
See :ref:`backwards-compatibility` for more details.
|
See :ref:`backwards-compatibility` for more details.
|
||||||
|
|
||||||
|
@ -995,7 +1052,7 @@ Improvements
|
||||||
|
|
||||||
- `#8803 <https://github.com/pytest-dev/pytest/issues/8803>`_: It is now possible to add colors to custom log levels on cli log.
|
- `#8803 <https://github.com/pytest-dev/pytest/issues/8803>`_: It is now possible to add colors to custom log levels on cli log.
|
||||||
|
|
||||||
By using :func:`add_color_level <_pytest.logging.add_color_level>` from a ``pytest_configure`` hook, colors can be added::
|
By using ``add_color_level`` from a :hook:`pytest_configure` hook, colors can be added::
|
||||||
|
|
||||||
logging_plugin = config.pluginmanager.get_plugin('logging-plugin')
|
logging_plugin = config.pluginmanager.get_plugin('logging-plugin')
|
||||||
logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan')
|
logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan')
|
||||||
|
@ -1060,7 +1117,7 @@ Bug Fixes
|
||||||
|
|
||||||
- `#8503 <https://github.com/pytest-dev/pytest/issues/8503>`_: :meth:`pytest.MonkeyPatch.syspath_prepend` no longer fails when
|
- `#8503 <https://github.com/pytest-dev/pytest/issues/8503>`_: :meth:`pytest.MonkeyPatch.syspath_prepend` no longer fails when
|
||||||
``setuptools`` is not installed.
|
``setuptools`` is not installed.
|
||||||
It now only calls :func:`pkg_resources.fixup_namespace_packages` if
|
It now only calls ``pkg_resources.fixup_namespace_packages`` if
|
||||||
``pkg_resources`` was previously imported, because it is not needed otherwise.
|
``pkg_resources`` was previously imported, because it is not needed otherwise.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1287,7 +1344,7 @@ Features
|
||||||
|
|
||||||
This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future.
|
This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future.
|
||||||
|
|
||||||
Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface.
|
Internally, the old ``pytest.Testdir`` is now a thin wrapper around :class:`~pytest.Pytester`, preserving the old interface.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7695`: A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
|
- :issue:`7695`: A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
|
||||||
|
@ -1325,7 +1382,7 @@ Features
|
||||||
Improvements
|
Improvements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- :issue:`1265`: Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method.
|
- :issue:`1265`: Added an ``__str__`` implementation to the :class:`~pytest.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`2044`: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS".
|
- :issue:`2044`: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS".
|
||||||
|
@ -1389,7 +1446,7 @@ Bug Fixes
|
||||||
- :issue:`7911`: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
|
- :issue:`7911`: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7913`: Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved.
|
- :issue:`7913`: Fixed a crash or hang in :meth:`pytester.spawn <pytest.Pytester.spawn>` when the :mod:`readline` module is involved.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7951`: Fixed handling of recursive symlinks when collecting tests.
|
- :issue:`7951`: Fixed handling of recursive symlinks when collecting tests.
|
||||||
|
@ -1506,7 +1563,7 @@ Deprecations
|
||||||
if you use this and want a replacement.
|
if you use this and want a replacement.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7255`: The :hook:`pytest_warning_captured` hook is deprecated in favor
|
- :issue:`7255`: The ``pytest_warning_captured`` hook is deprecated in favor
|
||||||
of :hook:`pytest_warning_recorded`, and will be removed in a future version.
|
of :hook:`pytest_warning_recorded`, and will be removed in a future version.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1534,8 +1591,8 @@ Improvements
|
||||||
- :issue:`7572`: When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace.
|
- :issue:`7572`: When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7685`: Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`.
|
- :issue:`7685`: Added two new attributes :attr:`rootpath <pytest.Config.rootpath>` and :attr:`inipath <pytest.Config.inipath>` to :class:`~pytest.Config`.
|
||||||
These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes,
|
These attributes are :class:`pathlib.Path` versions of the existing ``rootdir`` and ``inifile`` attributes,
|
||||||
and should be preferred over them when possible.
|
and should be preferred over them when possible.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1606,7 +1663,7 @@ Trivial/Internal Changes
|
||||||
- :issue:`7587`: The dependency on the ``more-itertools`` package has been removed.
|
- :issue:`7587`: The dependency on the ``more-itertools`` package has been removed.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7631`: The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple,
|
- :issue:`7631`: The result type of :meth:`capfd.readouterr() <pytest.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple,
|
||||||
but should behave like one in all respects. This was done for technical reasons.
|
but should behave like one in all respects. This was done for technical reasons.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1984,10 +2041,10 @@ Improvements
|
||||||
- :issue:`7128`: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. This is more consistent with how other tools show `--version`.
|
- :issue:`7128`: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. This is more consistent with how other tools show `--version`.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7133`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file.
|
- :issue:`7133`: :meth:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7159`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <_pytest.logging.LogCaptureFixture.at_level>` no longer affect
|
- :issue:`7159`: :meth:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <pytest.LogCaptureFixture.at_level>` no longer affect
|
||||||
the level of logs that are shown in the *Captured log report* report section.
|
the level of logs that are shown in the *Captured log report* report section.
|
||||||
|
|
||||||
|
|
||||||
|
@ -2082,7 +2139,7 @@ Bug Fixes
|
||||||
parameter when Python is called with the ``-bb`` flag.
|
parameter when Python is called with the ``-bb`` flag.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7143`: Fix :meth:`pytest.File.from_parent` so it forwards extra keyword arguments to the constructor.
|
- :issue:`7143`: Fix :meth:`pytest.File.from_parent <_pytest.nodes.Node.from_parent>` so it forwards extra keyword arguments to the constructor.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
|
- :issue:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
|
||||||
|
@ -2333,7 +2390,7 @@ Improvements
|
||||||
- :issue:`6384`: Make `--showlocals` work also with `--tb=short`.
|
- :issue:`6384`: Make `--showlocals` work also with `--tb=short`.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6653`: Add support for matching lines consecutively with :attr:`LineMatcher <_pytest.pytester.LineMatcher>`'s :func:`~_pytest.pytester.LineMatcher.fnmatch_lines` and :func:`~_pytest.pytester.LineMatcher.re_match_lines`.
|
- :issue:`6653`: Add support for matching lines consecutively with :class:`~pytest.LineMatcher`'s :func:`~pytest.LineMatcher.fnmatch_lines` and :func:`~pytest.LineMatcher.re_match_lines`.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6658`: Code is now highlighted in tracebacks when ``pygments`` is installed.
|
- :issue:`6658`: Code is now highlighted in tracebacks when ``pygments`` is installed.
|
||||||
|
@ -2401,7 +2458,7 @@ Bug Fixes
|
||||||
- :issue:`6597`: Fix node ids which contain a parametrized empty-string variable.
|
- :issue:`6597`: Fix node ids which contain a parametrized empty-string variable.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc.
|
- :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's ``testdir.runpytest`` etc.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger.
|
- :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger.
|
||||||
|
@ -2467,7 +2524,7 @@ Bug Fixes
|
||||||
``multiprocessing`` module.
|
``multiprocessing`` module.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6436`: :class:`FixtureDef <_pytest.fixtures.FixtureDef>` objects now properly register their finalizers with autouse and
|
- :issue:`6436`: :class:`~pytest.FixtureDef` objects now properly register their finalizers with autouse and
|
||||||
parameterized fixtures that execute before them in the fixture stack so they are torn
|
parameterized fixtures that execute before them in the fixture stack so they are torn
|
||||||
down at the right times, and in the right order.
|
down at the right times, and in the right order.
|
||||||
|
|
||||||
|
@ -2523,7 +2580,7 @@ Improvements
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
---------
|
---------
|
||||||
|
|
||||||
- :issue:`5914`: pytester: fix :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` when used after positive matching.
|
- :issue:`5914`: pytester: fix :py:func:`~pytest.LineMatcher.no_fnmatch_line` when used after positive matching.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`6082`: Fix line detection for doctest samples inside :py:class:`python:property` docstrings, as a workaround to :bpo:`17446`.
|
- :issue:`6082`: Fix line detection for doctest samples inside :py:class:`python:property` docstrings, as a workaround to :bpo:`17446`.
|
||||||
|
@ -2587,8 +2644,8 @@ Features
|
||||||
rather than implicitly.
|
rather than implicitly.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`5914`: :fixture:`testdir` learned two new functions, :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` and
|
- :issue:`5914`: :fixture:`testdir` learned two new functions, :py:func:`~pytest.LineMatcher.no_fnmatch_line` and
|
||||||
:py:func:`~_pytest.pytester.LineMatcher.no_re_match_line`.
|
:py:func:`~pytest.LineMatcher.no_re_match_line`.
|
||||||
|
|
||||||
The functions are used to ensure the captured text *does not* match the given
|
The functions are used to ensure the captured text *does not* match the given
|
||||||
pattern.
|
pattern.
|
||||||
|
@ -6440,7 +6497,7 @@ Changes
|
||||||
* fix :issue:`2013`: turn RecordedWarning into ``namedtuple``,
|
* fix :issue:`2013`: turn RecordedWarning into ``namedtuple``,
|
||||||
to give it a comprehensible repr while preventing unwarranted modification.
|
to give it a comprehensible repr while preventing unwarranted modification.
|
||||||
|
|
||||||
* fix :issue:`2208`: ensure an iteration limit for _pytest.compat.get_real_func.
|
* fix :issue:`2208`: ensure an iteration limit for ``_pytest.compat.get_real_func``.
|
||||||
Thanks :user:`RonnyPfannschmidt` for the report and PR.
|
Thanks :user:`RonnyPfannschmidt` for the report and PR.
|
||||||
|
|
||||||
* Hooks are now verified after collection is complete, rather than right after loading installed plugins. This
|
* Hooks are now verified after collection is complete, rather than right after loading installed plugins. This
|
||||||
|
|
|
@ -169,6 +169,50 @@ extlinks = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nitpicky = True
|
||||||
|
nitpick_ignore = [
|
||||||
|
# TODO (fix in pluggy?)
|
||||||
|
("py:class", "HookCaller"),
|
||||||
|
("py:class", "HookspecMarker"),
|
||||||
|
("py:exc", "PluginValidationError"),
|
||||||
|
# Might want to expose/TODO (https://github.com/pytest-dev/pytest/issues/7469)
|
||||||
|
("py:class", "ExceptionRepr"),
|
||||||
|
("py:class", "Exit"),
|
||||||
|
("py:class", "SubRequest"),
|
||||||
|
("py:class", "SubRequest"),
|
||||||
|
("py:class", "TerminalReporter"),
|
||||||
|
("py:class", "_pytest._code.code.TerminalRepr"),
|
||||||
|
("py:class", "_pytest.fixtures.FixtureFunctionMarker"),
|
||||||
|
("py:class", "_pytest.logging.LogCaptureHandler"),
|
||||||
|
("py:class", "_pytest.mark.structures.ParameterSet"),
|
||||||
|
# Intentionally undocumented/private
|
||||||
|
("py:class", "_pytest._code.code.Traceback"),
|
||||||
|
("py:class", "_pytest._py.path.LocalPath"),
|
||||||
|
("py:class", "_pytest.capture.CaptureResult"),
|
||||||
|
("py:class", "_pytest.compat.NotSetType"),
|
||||||
|
("py:class", "_pytest.python.PyCollector"),
|
||||||
|
("py:class", "_pytest.python.PyobjMixin"),
|
||||||
|
("py:class", "_pytest.python_api.RaisesContext"),
|
||||||
|
("py:class", "_pytest.recwarn.WarningsChecker"),
|
||||||
|
("py:class", "_pytest.reports.BaseReport"),
|
||||||
|
# Undocumented third parties
|
||||||
|
("py:class", "_tracing.TagTracerSub"),
|
||||||
|
("py:class", "warnings.WarningMessage"),
|
||||||
|
# Undocumented type aliases
|
||||||
|
("py:class", "LEGACY_PATH"),
|
||||||
|
("py:class", "_PluggyPlugin"),
|
||||||
|
# TypeVars
|
||||||
|
("py:class", "_pytest._code.code.E"),
|
||||||
|
("py:class", "_pytest.fixtures.FixtureFunction"),
|
||||||
|
("py:class", "_pytest.nodes._NodeType"),
|
||||||
|
("py:class", "_pytest.python_api.E"),
|
||||||
|
("py:class", "_pytest.recwarn.T"),
|
||||||
|
("py:class", "_pytest.runner.TResult"),
|
||||||
|
("py:obj", "_pytest.fixtures.FixtureValue"),
|
||||||
|
("py:obj", "_pytest.stash.T"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
sys.path.append(os.path.abspath("_themes"))
|
sys.path.append(os.path.abspath("_themes"))
|
||||||
|
|
|
@ -177,7 +177,7 @@ arguments they only pass on to the superclass.
|
||||||
resolved in future versions as we slowly get rid of the :pypi:`py`
|
resolved in future versions as we slowly get rid of the :pypi:`py`
|
||||||
dependency (see :issue:`9283` for a longer discussion).
|
dependency (see :issue:`9283` for a longer discussion).
|
||||||
|
|
||||||
Due to the ongoing migration of methods like :meth:`~_pytest.Item.reportinfo`
|
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
|
||||||
which still is expected to return a ``py.path.local`` object, nodes still have
|
which still is expected to return a ``py.path.local`` object, nodes still have
|
||||||
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
|
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
|
||||||
no matter what argument was used in the constructor. We expect to deprecate the
|
no matter what argument was used in the constructor. We expect to deprecate the
|
||||||
|
@ -336,7 +336,7 @@ Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
|
||||||
|
|
||||||
.. deprecated:: 7.0
|
.. deprecated:: 7.0
|
||||||
|
|
||||||
Defining a custom pytest node type which is both an :class:`pytest.Item <Item>` and a :class:`pytest.Collector <Collector>` (e.g. :class:`pytest.File <File>`) now issues a warning.
|
Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
|
||||||
It was never sanely supported and triggers hard to debug errors.
|
It was never sanely supported and triggers hard to debug errors.
|
||||||
|
|
||||||
Some plugins providing linting/code analysis have been using this as a hack.
|
Some plugins providing linting/code analysis have been using this as a hack.
|
||||||
|
@ -348,8 +348,8 @@ Instead, a separate collector node should be used, which collects the item. See
|
||||||
|
|
||||||
.. _uncooperative-constructors-deprecated:
|
.. _uncooperative-constructors-deprecated:
|
||||||
|
|
||||||
Constructors of custom :class:`pytest.Node` subclasses should take ``**kwargs``
|
Constructors of custom :class:`~_pytest.nodes.Node` subclasses should take ``**kwargs``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. deprecated:: 7.0
|
.. deprecated:: 7.0
|
||||||
|
|
||||||
|
@ -645,7 +645,7 @@ By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading
|
||||||
pytest 6.0, where the default format will be ``xunit2``.
|
pytest 6.0, where the default format will be ``xunit2``.
|
||||||
|
|
||||||
In order to let users know about the transition, pytest will issue a warning in case
|
In order to let users know about the transition, pytest will issue a warning in case
|
||||||
the ``--junitxml`` option is given in the command line but ``junit_family`` is not explicitly
|
the ``--junit-xml`` option is given in the command line but ``junit_family`` is not explicitly
|
||||||
configured in ``pytest.ini``.
|
configured in ``pytest.ini``.
|
||||||
|
|
||||||
Services known to support the ``xunit2`` format:
|
Services known to support the ``xunit2`` format:
|
||||||
|
|
|
@ -136,7 +136,7 @@ Or select multiple nodes:
|
||||||
|
|
||||||
Node IDs for failing tests are displayed in the test summary info
|
Node IDs for failing tests are displayed in the test summary info
|
||||||
when running pytest with the ``-rf`` option. You can also
|
when running pytest with the ``-rf`` option. You can also
|
||||||
construct Node IDs from the output of ``pytest --collectonly``.
|
construct Node IDs from the output of ``pytest --collect-only``.
|
||||||
|
|
||||||
Using ``-k expr`` to select tests based on their name
|
Using ``-k expr`` to select tests based on their name
|
||||||
-------------------------------------------------------
|
-------------------------------------------------------
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
Parametrizing tests
|
Parametrizing tests
|
||||||
=================================================
|
=================================================
|
||||||
|
|
||||||
.. currentmodule:: _pytest.python
|
|
||||||
|
|
||||||
``pytest`` allows to easily parametrize test functions.
|
``pytest`` allows to easily parametrize test functions.
|
||||||
For basic docs, see :ref:`parametrize-basics`.
|
For basic docs, see :ref:`parametrize-basics`.
|
||||||
|
|
||||||
|
@ -185,7 +183,7 @@ A quick port of "testscenarios"
|
||||||
Here is a quick port to run tests configured with :pypi:`testscenarios`,
|
Here is a quick port to run tests configured with :pypi:`testscenarios`,
|
||||||
an add-on from Robert Collins for the standard unittest framework. We
|
an add-on from Robert Collins for the standard unittest framework. We
|
||||||
only have to work a bit to construct the correct arguments for pytest's
|
only have to work a bit to construct the correct arguments for pytest's
|
||||||
:py:func:`Metafunc.parametrize`:
|
:py:func:`Metafunc.parametrize <pytest.Metafunc.parametrize>`:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,7 @@ Now we'll get feedback on a bad argument:
|
||||||
|
|
||||||
|
|
||||||
If you need to provide more detailed error messages, you can use the
|
If you need to provide more detailed error messages, you can use the
|
||||||
``type`` parameter and raise ``pytest.UsageError``:
|
``type`` parameter and raise :exc:`pytest.UsageError`:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -1090,4 +1090,4 @@ application with standard ``pytest`` command-line options:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
|
./app_main --pytest --verbose --tb=long --junit=xml=results.xml test-suite/
|
||||||
|
|
|
@ -34,7 +34,7 @@ a function/method call.
|
||||||
|
|
||||||
**Assert** is where we look at that resulting state and check if it looks how
|
**Assert** is where we look at that resulting state and check if it looks how
|
||||||
we'd expect after the dust has settled. It's where we gather evidence to say the
|
we'd expect after the dust has settled. It's where we gather evidence to say the
|
||||||
behavior does or does not aligns with what we expect. The ``assert`` in our test
|
behavior does or does not align with what we expect. The ``assert`` in our test
|
||||||
is where we take that measurement/observation and apply our judgement to it. If
|
is where we take that measurement/observation and apply our judgement to it. If
|
||||||
something should be green, we'd say ``assert thing == "green"``.
|
something should be green, we'd say ``assert thing == "green"``.
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,7 @@ A note about fixture cleanup
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
pytest does not do any special processing for :data:`SIGTERM <signal.SIGTERM>` and
|
pytest does not do any special processing for :data:`SIGTERM <signal.SIGTERM>` and
|
||||||
:data:`SIGQUIT <signal.SIGQUIT>` signals (:data:`SIGINT <signal.SIGINT>` is handled naturally
|
``SIGQUIT`` signals (:data:`SIGINT <signal.SIGINT>` is handled naturally
|
||||||
by the Python runtime via :class:`KeyboardInterrupt`), so fixtures that manage external resources which are important
|
by the Python runtime via :class:`KeyboardInterrupt`), so fixtures that manage external resources which are important
|
||||||
to be cleared when the Python process is terminated (by those signals) might leak resources.
|
to be cleared when the Python process is terminated (by those signals) might leak resources.
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,6 @@ funcarg mechanism, see :ref:`historical funcargs and pytest.funcargs`.
|
||||||
If you are new to pytest, then you can simply ignore this
|
If you are new to pytest, then you can simply ignore this
|
||||||
section and read the other sections.
|
section and read the other sections.
|
||||||
|
|
||||||
.. currentmodule:: _pytest
|
|
||||||
|
|
||||||
Shortcomings of the previous ``pytest_funcarg__`` mechanism
|
Shortcomings of the previous ``pytest_funcarg__`` mechanism
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -46,7 +44,7 @@ There are several limitations and difficulties with this approach:
|
||||||
|
|
||||||
2. parametrizing the "db" resource is not straight forward:
|
2. parametrizing the "db" resource is not straight forward:
|
||||||
you need to apply a "parametrize" decorator or implement a
|
you need to apply a "parametrize" decorator or implement a
|
||||||
:py:func:`~hookspec.pytest_generate_tests` hook
|
:hook:`pytest_generate_tests` hook
|
||||||
calling :py:func:`~pytest.Metafunc.parametrize` which
|
calling :py:func:`~pytest.Metafunc.parametrize` which
|
||||||
performs parametrization at the places where the resource
|
performs parametrization at the places where the resource
|
||||||
is used. Moreover, you need to modify the factory to use an
|
is used. Moreover, you need to modify the factory to use an
|
||||||
|
@ -94,7 +92,7 @@ Direct parametrization of funcarg resource factories
|
||||||
|
|
||||||
Previously, funcarg factories could not directly cause parametrization.
|
Previously, funcarg factories could not directly cause parametrization.
|
||||||
You needed to specify a ``@parametrize`` decorator on your test function
|
You needed to specify a ``@parametrize`` decorator on your test function
|
||||||
or implement a ``pytest_generate_tests`` hook to perform
|
or implement a :hook:`pytest_generate_tests` hook to perform
|
||||||
parametrization, i.e. calling a test multiple times with different value
|
parametrization, i.e. calling a test multiple times with different value
|
||||||
sets. pytest-2.3 introduces a decorator for use on the factory itself:
|
sets. pytest-2.3 introduces a decorator for use on the factory itself:
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 7.4.0
|
pytest 7.4.3
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
@ -97,6 +97,30 @@ Use the :ref:`raises <assertraises>` helper to assert that some code raises an e
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
f()
|
f()
|
||||||
|
|
||||||
|
You can also use the context provided by :ref:`raises <assertraises>` to
|
||||||
|
assert that an expected exception is part of a raised ``ExceptionGroup``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# content of test_exceptiongroup.py
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def f():
|
||||||
|
raise ExceptionGroup(
|
||||||
|
"Group message",
|
||||||
|
[
|
||||||
|
RuntimeError(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exception_in_group():
|
||||||
|
with pytest.raises(ExceptionGroup) as excinfo:
|
||||||
|
f()
|
||||||
|
assert excinfo.group_contains(RuntimeError)
|
||||||
|
assert not excinfo.group_contains(TypeError)
|
||||||
|
|
||||||
Execute the test function with “quiet” reporting mode:
|
Execute the test function with “quiet” reporting mode:
|
||||||
|
|
||||||
.. code-block:: pytest
|
.. code-block:: pytest
|
||||||
|
|
|
@ -112,7 +112,7 @@ More details can be found in the :pull:`original PR <3317>`.
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
in a future major release of pytest we will introduce class based markers,
|
in a future major release of pytest we will introduce class based markers,
|
||||||
at which point markers will no longer be limited to instances of :py:class:`~_pytest.mark.Mark`.
|
at which point markers will no longer be limited to instances of :py:class:`~pytest.Mark`.
|
||||||
|
|
||||||
|
|
||||||
cache plugin integrated into the core
|
cache plugin integrated into the core
|
||||||
|
|
|
@ -98,6 +98,27 @@ and if you need to have access to the actual exception info you may use:
|
||||||
the actual exception raised. The main attributes of interest are
|
the actual exception raised. The main attributes of interest are
|
||||||
``.type``, ``.value`` and ``.traceback``.
|
``.type``, ``.value`` and ``.traceback``.
|
||||||
|
|
||||||
|
Note that ``pytest.raises`` will match the exception type or any subclasses (like the standard ``except`` statement).
|
||||||
|
If you want to check if a block of code is raising an exact exception type, you need to check that explicitly:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def test_foo_not_implemented():
|
||||||
|
def foo():
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError) as excinfo:
|
||||||
|
foo()
|
||||||
|
assert excinfo.type is RuntimeError
|
||||||
|
|
||||||
|
The :func:`pytest.raises` call will succeed, even though the function raises :class:`NotImplementedError`, because
|
||||||
|
:class:`NotImplementedError` is a subclass of :class:`RuntimeError`; however the following `assert` statement will
|
||||||
|
catch the problem.
|
||||||
|
|
||||||
|
Matching exception messages
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can pass a ``match`` keyword parameter to the context-manager to test
|
You can pass a ``match`` keyword parameter to the context-manager to test
|
||||||
that a regular expression matches on the string representation of an exception
|
that a regular expression matches on the string representation of an exception
|
||||||
(similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``):
|
(similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``):
|
||||||
|
@ -115,36 +136,111 @@ that a regular expression matches on the string representation of an exception
|
||||||
with pytest.raises(ValueError, match=r".* 123 .*"):
|
with pytest.raises(ValueError, match=r".* 123 .*"):
|
||||||
myfunc()
|
myfunc()
|
||||||
|
|
||||||
The regexp parameter of the ``match`` method is matched with the ``re.search``
|
Notes:
|
||||||
function, so in the above example ``match='123'`` would have worked as
|
|
||||||
well.
|
|
||||||
|
|
||||||
There's an alternate form of the :func:`pytest.raises` function where you pass
|
* The ``match`` parameter is matched with the :func:`re.search`
|
||||||
a function that will be executed with the given ``*args`` and ``**kwargs`` and
|
function, so in the above example ``match='123'`` would have worked as well.
|
||||||
assert that the given exception is raised:
|
* The ``match`` parameter also matches against `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``.
|
||||||
|
|
||||||
|
|
||||||
|
Matching exception groups
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
|
||||||
|
method to test for exceptions returned as part of an ``ExceptionGroup``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
pytest.raises(ExpectedException, func, *args, **kwargs)
|
def test_exception_in_group():
|
||||||
|
with pytest.raises(RuntimeError) as excinfo:
|
||||||
|
raise ExceptionGroup(
|
||||||
|
"Group message",
|
||||||
|
[
|
||||||
|
RuntimeError("Exception 123 raised"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert excinfo.group_contains(RuntimeError, match=r".* 123 .*")
|
||||||
|
assert not excinfo.group_contains(TypeError)
|
||||||
|
|
||||||
|
The optional ``match`` keyword parameter works the same way as for
|
||||||
|
:func:`pytest.raises`.
|
||||||
|
|
||||||
|
By default ``group_contains()`` will recursively search for a matching
|
||||||
|
exception at any level of nested ``ExceptionGroup`` instances. You can
|
||||||
|
specify a ``depth`` keyword parameter if you only want to match an
|
||||||
|
exception at a specific level; exceptions contained directly in the top
|
||||||
|
``ExceptionGroup`` would match ``depth=1``.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def test_exception_in_group_at_given_depth():
|
||||||
|
with pytest.raises(RuntimeError) as excinfo:
|
||||||
|
raise ExceptionGroup(
|
||||||
|
"Group message",
|
||||||
|
[
|
||||||
|
RuntimeError(),
|
||||||
|
ExceptionGroup(
|
||||||
|
"Nested group",
|
||||||
|
[
|
||||||
|
TypeError(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert excinfo.group_contains(RuntimeError, depth=1)
|
||||||
|
assert excinfo.group_contains(TypeError, depth=2)
|
||||||
|
assert not excinfo.group_contains(RuntimeError, depth=2)
|
||||||
|
assert not excinfo.group_contains(TypeError, depth=1)
|
||||||
|
|
||||||
|
Alternate form (legacy)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
There is an alternate form where you pass
|
||||||
|
a function that will be executed, along ``*args`` and ``**kwargs``, and :func:`pytest.raises`
|
||||||
|
will execute the function with the arguments and assert that the given exception is raised:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def func(x):
|
||||||
|
if x <= 0:
|
||||||
|
raise ValueError("x needs to be larger than zero")
|
||||||
|
|
||||||
|
|
||||||
|
pytest.raises(ValueError, func, x=-1)
|
||||||
|
|
||||||
The reporter will provide you with helpful output in case of failures such as *no
|
The reporter will provide you with helpful output in case of failures such as *no
|
||||||
exception* or *wrong exception*.
|
exception* or *wrong exception*.
|
||||||
|
|
||||||
Note that it is also possible to specify a "raises" argument to
|
This form was the original :func:`pytest.raises` API, developed before the ``with`` statement was
|
||||||
``pytest.mark.xfail``, which checks that the test is failing in a more
|
added to the Python language. Nowadays, this form is rarely used, with the context-manager form (using ``with``)
|
||||||
|
being considered more readable.
|
||||||
|
Nonetheless, this form is fully supported and not deprecated in any way.
|
||||||
|
|
||||||
|
xfail mark and pytest.raises
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
It is also possible to specify a ``raises`` argument to
|
||||||
|
:ref:`pytest.mark.xfail <pytest.mark.xfail ref>`, which checks that the test is failing in a more
|
||||||
specific way than just having any exception raised:
|
specific way than just having any exception raised:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
def f():
|
||||||
|
raise IndexError()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(raises=IndexError)
|
@pytest.mark.xfail(raises=IndexError)
|
||||||
def test_f():
|
def test_f():
|
||||||
f()
|
f()
|
||||||
|
|
||||||
Using :func:`pytest.raises` is likely to be better for cases where you are
|
|
||||||
testing exceptions your own code is deliberately raising, whereas using
|
This will only "xfail" if the test fails by raising ``IndexError`` or subclasses.
|
||||||
``@pytest.mark.xfail`` with a check function is probably better for something
|
|
||||||
like documenting unfixed bugs (where the test describes what "should" happen)
|
* Using :ref:`pytest.mark.xfail <pytest.mark.xfail ref>` with the ``raises`` parameter is probably better for something
|
||||||
or bugs in dependencies.
|
like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies.
|
||||||
|
|
||||||
|
* Using :func:`pytest.raises` is likely to be better for cases where you are
|
||||||
|
testing exceptions your own code is deliberately raising, which is the majority of cases.
|
||||||
|
|
||||||
|
|
||||||
.. _`assertwarns`:
|
.. _`assertwarns`:
|
||||||
|
|
|
@ -176,14 +176,21 @@ with more recent files coming first.
|
||||||
Behavior when no tests failed in the last run
|
Behavior when no tests failed in the last run
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
When no tests failed in the last run, or when no cached ``lastfailed`` data was
|
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
|
||||||
found, ``pytest`` can be configured either to run all of the tests or no tests,
|
Determines whether to execute tests when there are no previously (known)
|
||||||
using the ``--last-failed-no-failures`` option, which takes one of the following values:
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
|
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
|
||||||
pytest --last-failed --last-failed-no-failures none # run no tests and exit
|
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
|
||||||
|
|
||||||
The new config.cache object
|
The new config.cache object
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
@ -206,12 +213,12 @@ across pytest invocations:
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mydata(request):
|
def mydata(pytestconfig):
|
||||||
val = request.config.cache.get("example/value", None)
|
val = pytestconfig.cache.get("example/value", None)
|
||||||
if val is None:
|
if val is None:
|
||||||
expensive_computation()
|
expensive_computation()
|
||||||
val = 42
|
val = 42
|
||||||
request.config.cache.set("example/value", val)
|
pytestconfig.cache.set("example/value", val)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -382,8 +382,6 @@ warnings: a WarningsRecorder instance. To view the recorded warnings, you can
|
||||||
iterate over this instance, call ``len`` on it to get the number of recorded
|
iterate over this instance, call ``len`` on it to get the number of recorded
|
||||||
warnings, or index into it to get a particular recorded warning.
|
warnings, or index into it to get a particular recorded warning.
|
||||||
|
|
||||||
.. currentmodule:: _pytest.warnings
|
|
||||||
|
|
||||||
Full API: :class:`~_pytest.recwarn.WarningsRecorder`.
|
Full API: :class:`~_pytest.recwarn.WarningsRecorder`.
|
||||||
|
|
||||||
.. _`warns use cases`:
|
.. _`warns use cases`:
|
||||||
|
|
|
@ -1271,7 +1271,7 @@ configured in multiple ways.
|
||||||
Extending the previous example, we can flag the fixture to create two
|
Extending the previous example, we can flag the fixture to create two
|
||||||
``smtp_connection`` fixture instances which will cause all tests using the fixture
|
``smtp_connection`` fixture instances which will cause all tests using the fixture
|
||||||
to run twice. The fixture function gets access to each parameter
|
to run twice. The fixture function gets access to each parameter
|
||||||
through the special :py:class:`request <FixtureRequest>` object:
|
through the special :py:class:`request <pytest.FixtureRequest>` object:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
|
@ -241,7 +241,7 @@ through ``add_color_level()``. Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.hookimpl
|
@pytest.hookimpl(trylast=True)
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
|
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,12 @@ Examples for modifying traceback printing:
|
||||||
pytest -l # show local variables (shortcut)
|
pytest -l # show local variables (shortcut)
|
||||||
pytest --no-showlocals # hide local variables (if addopts enables them)
|
pytest --no-showlocals # hide local variables (if addopts enables them)
|
||||||
|
|
||||||
|
pytest --capture=fd # default, capture at the file descriptor level
|
||||||
|
pytest --capture=sys # capture at the sys level
|
||||||
|
pytest --capture=no # don't capture
|
||||||
|
pytest -s # don't capture (shortcut)
|
||||||
|
pytest --capture=tee-sys # capture to logs but also output to sys level streams
|
||||||
|
|
||||||
pytest --tb=auto # (default) 'long' tracebacks for the first and last
|
pytest --tb=auto # (default) 'long' tracebacks for the first and last
|
||||||
# entry, but 'short' style for the other entries
|
# entry, but 'short' style for the other entries
|
||||||
pytest --tb=long # exhaustive, informative traceback formatting
|
pytest --tb=long # exhaustive, informative traceback formatting
|
||||||
|
@ -36,6 +42,16 @@ option you make sure a trace is shown.
|
||||||
Verbosity
|
Verbosity
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
|
|
||||||
|
Examples for modifying printing verbosity:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pytest --quiet # quiet - less verbose - mode
|
||||||
|
pytest -q # quiet - less verbose - mode (shortcut)
|
||||||
|
pytest -v # increase verbosity, display individual test names
|
||||||
|
pytest -vv # more verbose, display more details from the test output
|
||||||
|
pytest -vvv # not a standard , but may be used for even more detail in certain setups
|
||||||
|
|
||||||
The ``-v`` flag controls the verbosity of pytest output in various aspects: test session progress, assertion
|
The ``-v`` flag controls the verbosity of pytest output in various aspects: test session progress, assertion
|
||||||
details when tests fail, fixtures details with ``--fixtures``, etc.
|
details when tests fail, fixtures details with ``--fixtures``, etc.
|
||||||
|
|
||||||
|
@ -270,6 +286,20 @@ situations, for example you are shown even fixtures that start with ``_`` if you
|
||||||
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
|
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
|
||||||
however some plugins might make use of higher verbosity.
|
however some plugins might make use of higher verbosity.
|
||||||
|
|
||||||
|
.. _`pytest.fine_grained_verbosity`:
|
||||||
|
|
||||||
|
Fine-grained verbosity
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently.
|
||||||
|
This is done by setting a verbosity level in the configuration file for the specific aspect of the output.
|
||||||
|
|
||||||
|
:confval:`verbosity_assertions`: Controls how verbose the assertion output should be when pytest is executed. Running
|
||||||
|
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
|
||||||
|
the file is shown by a single character in the output.
|
||||||
|
|
||||||
|
(Note: currently this is the only option available, but more might be added in the future).
|
||||||
|
|
||||||
.. _`pytest.detailed_failed_tests_usage`:
|
.. _`pytest.detailed_failed_tests_usage`:
|
||||||
|
|
||||||
Producing a detailed summary report
|
Producing a detailed summary report
|
||||||
|
@ -478,7 +508,7 @@ integration servers, use this invocation:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest --junitxml=path
|
pytest --junit-xml=path
|
||||||
|
|
||||||
to create an XML file at ``path``.
|
to create an XML file at ``path``.
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
|
||||||
d = tmp_path / "sub"
|
d = tmp_path / "sub"
|
||||||
d.mkdir()
|
d.mkdir()
|
||||||
p = d / "hello.txt"
|
p = d / "hello.txt"
|
||||||
p.write_text(CONTENT)
|
p.write_text(CONTENT, encoding="utf-8")
|
||||||
assert p.read_text() == CONTENT
|
assert p.read_text(encoding="utf-8") == CONTENT
|
||||||
assert len(list(tmp_path.iterdir())) == 1
|
assert len(list(tmp_path.iterdir())) == 1
|
||||||
> assert 0
|
> assert 0
|
||||||
E assert 0
|
E assert 0
|
||||||
|
|
|
@ -59,10 +59,6 @@ The remaining hook functions will not be called in this case.
|
||||||
hook wrappers: executing around other hooks
|
hook wrappers: executing around other hooks
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
.. currentmodule:: _pytest.core
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pytest plugins can implement hook wrappers which wrap the execution
|
pytest plugins can implement hook wrappers which wrap the execution
|
||||||
of other hook implementations. A hook wrapper is a generator function
|
of other hook implementations. A hook wrapper is a generator function
|
||||||
which yields exactly once. When pytest invokes hooks it first executes
|
which yields exactly once. When pytest invokes hooks it first executes
|
||||||
|
@ -165,6 +161,7 @@ Here is the order of execution:
|
||||||
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
|
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
|
||||||
in which case it will influence the ordering of hook wrappers among each other.
|
in which case it will influence the ordering of hook wrappers among each other.
|
||||||
|
|
||||||
|
.. _`declaringhooks`:
|
||||||
|
|
||||||
Declaring new hooks
|
Declaring new hooks
|
||||||
------------------------
|
------------------------
|
||||||
|
@ -174,13 +171,11 @@ Declaring new hooks
|
||||||
This is a quick overview on how to add new hooks and how they work in general, but a more complete
|
This is a quick overview on how to add new hooks and how they work in general, but a more complete
|
||||||
overview can be found in `the pluggy documentation <https://pluggy.readthedocs.io/en/latest/>`__.
|
overview can be found in `the pluggy documentation <https://pluggy.readthedocs.io/en/latest/>`__.
|
||||||
|
|
||||||
.. currentmodule:: _pytest.hookspec
|
|
||||||
|
|
||||||
Plugins and ``conftest.py`` files may declare new hooks that can then be
|
Plugins and ``conftest.py`` files may declare new hooks that can then be
|
||||||
implemented by other plugins in order to alter behaviour or interact with
|
implemented by other plugins in order to alter behaviour or interact with
|
||||||
the new plugin:
|
the new plugin:
|
||||||
|
|
||||||
.. autofunction:: pytest_addhooks
|
.. autofunction:: _pytest.hookspec.pytest_addhooks
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
Hooks are usually declared as do-nothing functions that contain only
|
Hooks are usually declared as do-nothing functions that contain only
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
.. sidebar:: Next Open Trainings
|
.. sidebar:: Next Open Trainings
|
||||||
|
|
||||||
- `pytest: Professionelles Testen (nicht nur) für Python <https://workshoptage.ch/workshops/2023/pytest-professionelles-testen-nicht-nur-fuer-python-2/>`_, at `Workshoptage 2023 <https://workshoptage.ch/>`_, **September 5th**, `OST <https://www.ost.ch/en>`_ Campus **Rapperswil, Switzerland**
|
|
||||||
- `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**
|
- `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>`.
|
||||||
|
|
|
@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
|
||||||
setup.cfg
|
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.
|
if they have a ``[tool:pytest]`` section.
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
|
@ -11,9 +11,6 @@ Fixtures reference
|
||||||
.. seealso:: :ref:`about-fixtures`
|
.. seealso:: :ref:`about-fixtures`
|
||||||
.. seealso:: :ref:`how-to-fixtures`
|
.. seealso:: :ref:`how-to-fixtures`
|
||||||
|
|
||||||
|
|
||||||
.. currentmodule:: _pytest.python
|
|
||||||
|
|
||||||
.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection
|
.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,15 +73,13 @@ Built-in fixtures
|
||||||
:class:`pathlib.Path` objects.
|
:class:`pathlib.Path` objects.
|
||||||
|
|
||||||
:fixture:`tmpdir`
|
:fixture:`tmpdir`
|
||||||
Provide a :class:`py.path.local` object to a temporary
|
Provide a `py.path.local <https://py.readthedocs.io/en/latest/path.html>`_ object to a temporary
|
||||||
directory which is unique to each test function;
|
directory which is unique to each test function;
|
||||||
replaced by :fixture:`tmp_path`.
|
replaced by :fixture:`tmp_path`.
|
||||||
|
|
||||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
|
||||||
|
|
||||||
:fixture:`tmpdir_factory`
|
:fixture:`tmpdir_factory`
|
||||||
Make session-scoped temporary directories and return
|
Make session-scoped temporary directories and return
|
||||||
:class:`py.path.local` objects;
|
``py.path.local`` objects;
|
||||||
replaced by :fixture:`tmp_path_factory`.
|
replaced by :fixture:`tmp_path_factory`.
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,7 +93,7 @@ Fixture availability is determined from the perspective of the test. A fixture
|
||||||
is only available for tests to request if they are in the scope that fixture is
|
is only available for tests to request if they are in the scope that fixture is
|
||||||
defined in. If a fixture is defined inside a class, it can only be requested by
|
defined in. If a fixture is defined inside a class, it can only be requested by
|
||||||
tests inside that class. But if a fixture is defined inside the global scope of
|
tests inside that class. But if a fixture is defined inside the global scope of
|
||||||
the module, than every test in that module, even if it's defined inside a class,
|
the module, then every test in that module, even if it's defined inside a class,
|
||||||
can request it.
|
can request it.
|
||||||
|
|
||||||
Similarly, a test can also only be affected by an autouse fixture if that test
|
Similarly, a test can also only be affected by an autouse fixture if that test
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,5 @@
|
||||||
|
:tocdepth: 3
|
||||||
|
|
||||||
.. _`api-reference`:
|
.. _`api-reference`:
|
||||||
|
|
||||||
API Reference
|
API Reference
|
||||||
|
@ -77,7 +79,7 @@ pytest.xfail
|
||||||
pytest.exit
|
pytest.exit
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
.. autofunction:: pytest.exit(reason, [returncode=False, msg=None])
|
.. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
|
||||||
|
|
||||||
pytest.main
|
pytest.main
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
@ -237,22 +239,23 @@ pytest.mark.xfail
|
||||||
|
|
||||||
Marks a test function as *expected to fail*.
|
Marks a test function as *expected to fail*.
|
||||||
|
|
||||||
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False)
|
.. py:function:: pytest.mark.xfail(condition=False, *, reason=None, raises=None, run=True, strict=xfail_strict)
|
||||||
|
|
||||||
:type condition: bool or str
|
:keyword Union[bool, str] condition:
|
||||||
:param condition:
|
|
||||||
Condition for marking the test function as xfail (``True/False`` or a
|
Condition for marking the test function as xfail (``True/False`` or a
|
||||||
:ref:`condition string <string conditions>`). If a bool, you also have
|
:ref:`condition string <string conditions>`). If a ``bool``, you also have
|
||||||
to specify ``reason`` (see :ref:`condition string <string conditions>`).
|
to specify ``reason`` (see :ref:`condition string <string conditions>`).
|
||||||
:keyword str reason:
|
:keyword str reason:
|
||||||
Reason why the test function is marked as xfail.
|
Reason why the test function is marked as xfail.
|
||||||
:keyword Type[Exception] raises:
|
:keyword Type[Exception] raises:
|
||||||
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test.
|
Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test.
|
||||||
|
Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works).
|
||||||
|
|
||||||
:keyword bool run:
|
:keyword bool run:
|
||||||
If the test function should actually be executed. If ``False``, the function will always xfail and will
|
Whether the test function should actually be executed. If ``False``, the function will always xfail and will
|
||||||
not be executed (useful if a function is segfaulting).
|
not be executed (useful if a function is segfaulting).
|
||||||
:keyword bool strict:
|
:keyword bool strict:
|
||||||
* If ``False`` (the default) the function will be shown in the terminal output as ``xfailed`` if it fails
|
* If ``False`` the function will be shown in the terminal output as ``xfailed`` if it fails
|
||||||
and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This
|
and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This
|
||||||
is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later.
|
is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later.
|
||||||
* If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it
|
* If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it
|
||||||
|
@ -260,6 +263,8 @@ Marks a test function as *expected to fail*.
|
||||||
that are always failing and there should be a clear indication if they unexpectedly start to pass (for example
|
that are always failing and there should be a clear indication if they unexpectedly start to pass (for example
|
||||||
a new release of a library fixes a known bug).
|
a new release of a library fixes a known bug).
|
||||||
|
|
||||||
|
Defaults to :confval:`xfail_strict`, which is ``False`` by default.
|
||||||
|
|
||||||
|
|
||||||
Custom marks
|
Custom marks
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
@ -607,10 +612,30 @@ Hooks
|
||||||
|
|
||||||
**Tutorial**: :ref:`writing-plugins`
|
**Tutorial**: :ref:`writing-plugins`
|
||||||
|
|
||||||
.. currentmodule:: _pytest.hookspec
|
|
||||||
|
|
||||||
Reference to all hooks which can be implemented by :ref:`conftest.py files <localplugin>` and :ref:`plugins <plugins>`.
|
Reference to all hooks which can be implemented by :ref:`conftest.py files <localplugin>` and :ref:`plugins <plugins>`.
|
||||||
|
|
||||||
|
@pytest.hookimpl
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. function:: pytest.hookimpl
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
pytest's decorator for marking functions as hook implementations.
|
||||||
|
|
||||||
|
See :ref:`writinghooks` and :func:`pluggy.HookimplMarker`.
|
||||||
|
|
||||||
|
@pytest.hookspec
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. function:: pytest.hookspec
|
||||||
|
:decorator:
|
||||||
|
|
||||||
|
pytest's decorator for marking functions as hook specifications.
|
||||||
|
|
||||||
|
See :ref:`declaringhooks` and :func:`pluggy.HookspecMarker`.
|
||||||
|
|
||||||
|
.. currentmodule:: _pytest.hookspec
|
||||||
|
|
||||||
Bootstrapping hooks
|
Bootstrapping hooks
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -796,6 +821,7 @@ Node
|
||||||
|
|
||||||
.. autoclass:: _pytest.nodes.Node()
|
.. autoclass:: _pytest.nodes.Node()
|
||||||
:members:
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Collector
|
Collector
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
@ -978,10 +1004,10 @@ TestShortLogReport
|
||||||
.. autoclass:: pytest.TestShortLogReport()
|
.. autoclass:: pytest.TestShortLogReport()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
_Result
|
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
|
Stash
|
||||||
~~~~~
|
~~~~~
|
||||||
|
@ -1132,7 +1158,10 @@ When set (regardless of value), pytest will use color in terminal output.
|
||||||
Exceptions
|
Exceptions
|
||||||
----------
|
----------
|
||||||
|
|
||||||
.. autoclass:: pytest.UsageError()
|
.. autoexception:: pytest.UsageError()
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoexception:: pytest.FixtureLookupError()
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
.. _`warnings ref`:
|
.. _`warnings ref`:
|
||||||
|
@ -1638,11 +1667,11 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
Additionally, ``pytest`` will attempt to intelligently identify and ignore a
|
Additionally, ``pytest`` will attempt to intelligently identify and ignore a
|
||||||
virtualenv by the presence of an activation script. Any directory deemed to
|
virtualenv by the presence of an activation script. Any directory deemed to
|
||||||
be the root of a virtual environment will not be considered during test
|
be the root of a virtual environment will not be considered during test
|
||||||
collection unless ``‑‑collect‑in‑virtualenv`` is given. Note also that
|
collection unless ``--collect-in-virtualenv`` is given. Note also that
|
||||||
``norecursedirs`` takes precedence over ``‑‑collect‑in‑virtualenv``; e.g. if
|
``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if
|
||||||
you intend to run tests in a virtualenv with a base directory that matches
|
you intend to run tests in a virtualenv with a base directory that matches
|
||||||
``'.*'`` you *must* override ``norecursedirs`` in addition to using the
|
``'.*'`` you *must* override ``norecursedirs`` in addition to using the
|
||||||
``‑‑collect‑in‑virtualenv`` flag.
|
``--collect-in-virtualenv`` flag.
|
||||||
|
|
||||||
|
|
||||||
.. confval:: python_classes
|
.. confval:: python_classes
|
||||||
|
@ -1817,6 +1846,19 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
clean_db
|
clean_db
|
||||||
|
|
||||||
|
|
||||||
|
.. confval:: verbosity_assertions
|
||||||
|
|
||||||
|
Set a verbosity level specifically for assertion related output, overriding the application wide level.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
verbosity_assertions = 2
|
||||||
|
|
||||||
|
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
|
||||||
|
"auto" can be used to explicitly use the global verbosity level.
|
||||||
|
|
||||||
|
|
||||||
.. confval:: xfail_strict
|
.. confval:: xfail_strict
|
||||||
|
|
||||||
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
|
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
|
||||||
|
@ -1890,8 +1932,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
tests. Optional argument: glob (default: '*').
|
tests. Optional argument: glob (default: '*').
|
||||||
--cache-clear Remove all cache contents at start of test run
|
--cache-clear Remove all cache contents at start of test run
|
||||||
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
||||||
Which tests to run with no previously (known)
|
With ``--lf``, determines whether to execute tests
|
||||||
failures
|
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
|
--sw, --stepwise Exit on test failure and continue from last failing
|
||||||
test next time
|
test next time
|
||||||
--sw-skip, --stepwise-skip
|
--sw-skip, --stepwise-skip
|
||||||
|
|
|
@ -2,7 +2,7 @@ pallets-sphinx-themes
|
||||||
pluggy>=1.2.0
|
pluggy>=1.2.0
|
||||||
pygments-pytest>=2.3.0
|
pygments-pytest>=2.3.0
|
||||||
sphinx-removed-in>=0.2.0
|
sphinx-removed-in>=0.2.0
|
||||||
sphinx>=5,<6
|
sphinx>=5,<8
|
||||||
sphinxcontrib-trio
|
sphinxcontrib-trio
|
||||||
sphinxcontrib-svg2pdfconverter
|
sphinxcontrib-svg2pdfconverter
|
||||||
# Pin packaging because it no longer handles 'latest' version, which
|
# Pin packaging because it no longer handles 'latest' version, which
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = [
|
requires = [
|
||||||
# sync with setup.py until we discard non-pep-517/518
|
|
||||||
"setuptools>=45.0",
|
"setuptools>=45.0",
|
||||||
"setuptools-scm[toml]>=6.2.3",
|
"setuptools-scm[toml]>=6.2.3",
|
||||||
]
|
]
|
||||||
|
@ -17,7 +16,12 @@ python_classes = ["Test", "Acceptance"]
|
||||||
python_functions = ["test"]
|
python_functions = ["test"]
|
||||||
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
|
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
|
||||||
testpaths = ["testing"]
|
testpaths = ["testing"]
|
||||||
norecursedirs = ["testing/example_scripts"]
|
norecursedirs = [
|
||||||
|
"testing/example_scripts",
|
||||||
|
".*",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
]
|
||||||
xfail_strict = true
|
xfail_strict = true
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
"error",
|
"error",
|
||||||
|
|
|
@ -31,10 +31,22 @@ class InvalidFeatureRelease(Exception):
|
||||||
SLUG = "pytest-dev/pytest"
|
SLUG = "pytest-dev/pytest"
|
||||||
|
|
||||||
PR_BODY = """\
|
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
|
Once all builds pass and it has been **approved** by one or more maintainers, start the \
|
||||||
can be released by pushing a tag `{version}` to this repository.
|
[deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
|
||||||
|
|
||||||
|
* `Use workflow from`: `release-{version}`.
|
||||||
|
* `Release version`: `{version}`.
|
||||||
|
|
||||||
|
Or execute on the command line:
|
||||||
|
|
||||||
|
```console
|
||||||
|
gh workflow run deploy.yml -r release-{version} -f version={version}
|
||||||
|
```
|
||||||
|
|
||||||
|
After the workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,26 @@ FILE_HEAD = r"""
|
||||||
|
|
||||||
.. _plugin-list:
|
.. _plugin-list:
|
||||||
|
|
||||||
Plugin List
|
Pytest Plugin List
|
||||||
===========
|
==================
|
||||||
|
|
||||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||||
automatically together with a manually-maintained list in `the source
|
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
|
||||||
Packages classified as inactive are excluded.
|
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.
|
||||||
|
|
||||||
|
|
||||||
.. The following conditional uses a different format for this list when
|
.. The following conditional uses a different format for this list when
|
||||||
creating a PDF, because otherwise the table gets far too wide for the
|
creating a PDF, because otherwise the table gets far too wide for the
|
||||||
page.
|
page.
|
||||||
|
@ -44,6 +56,8 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||||
)
|
)
|
||||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||||
"logassert",
|
"logassert",
|
||||||
|
"nuts",
|
||||||
|
"flask_fixture",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ py_modules = py
|
||||||
install_requires =
|
install_requires =
|
||||||
iniconfig
|
iniconfig
|
||||||
packaging
|
packaging
|
||||||
pluggy>=1.2.0,<2.0
|
pluggy>=1.3.0,<2.0
|
||||||
colorama;sys_platform=="win32"
|
colorama;sys_platform=="win32"
|
||||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
||||||
tomli>=1.0.0;python_version<"3.11"
|
tomli>=1.0.0;python_version<"3.11"
|
||||||
|
|
|
@ -697,6 +697,14 @@ class ExceptionInfo(Generic[E]):
|
||||||
)
|
)
|
||||||
return fmt.repr_excinfo(self)
|
return fmt.repr_excinfo(self)
|
||||||
|
|
||||||
|
def _stringify_exception(self, exc: BaseException) -> str:
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
str(exc),
|
||||||
|
*getattr(exc, "__notes__", []),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
|
def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
|
||||||
"""Check whether the regular expression `regexp` matches the string
|
"""Check whether the regular expression `regexp` matches the string
|
||||||
representation of the exception using :func:`python:re.search`.
|
representation of the exception using :func:`python:re.search`.
|
||||||
|
@ -704,12 +712,7 @@ class ExceptionInfo(Generic[E]):
|
||||||
If it matches `True` is returned, otherwise an `AssertionError` is raised.
|
If it matches `True` is returned, otherwise an `AssertionError` is raised.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
value = "\n".join(
|
value = self._stringify_exception(self.value)
|
||||||
[
|
|
||||||
str(self.value),
|
|
||||||
*getattr(self.value, "__notes__", []),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
|
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
|
||||||
if regexp == value:
|
if regexp == value:
|
||||||
msg += "\n Did you mean to `re.escape()` the regex?"
|
msg += "\n Did you mean to `re.escape()` the regex?"
|
||||||
|
@ -717,6 +720,69 @@ class ExceptionInfo(Generic[E]):
|
||||||
# Return True to allow for "assert excinfo.match()".
|
# Return True to allow for "assert excinfo.match()".
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _group_contains(
|
||||||
|
self,
|
||||||
|
exc_group: BaseExceptionGroup[BaseException],
|
||||||
|
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
|
||||||
|
match: Union[str, Pattern[str], None],
|
||||||
|
target_depth: Optional[int] = None,
|
||||||
|
current_depth: int = 1,
|
||||||
|
) -> bool:
|
||||||
|
"""Return `True` if a `BaseExceptionGroup` contains a matching exception."""
|
||||||
|
if (target_depth is not None) and (current_depth > target_depth):
|
||||||
|
# already descended past the target depth
|
||||||
|
return False
|
||||||
|
for exc in exc_group.exceptions:
|
||||||
|
if isinstance(exc, BaseExceptionGroup):
|
||||||
|
if self._group_contains(
|
||||||
|
exc, expected_exception, match, target_depth, current_depth + 1
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
if (target_depth is not None) and (current_depth != target_depth):
|
||||||
|
# not at the target depth, no match
|
||||||
|
continue
|
||||||
|
if not isinstance(exc, expected_exception):
|
||||||
|
continue
|
||||||
|
if match is not None:
|
||||||
|
value = self._stringify_exception(exc)
|
||||||
|
if not re.search(match, value):
|
||||||
|
continue
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def group_contains(
|
||||||
|
self,
|
||||||
|
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
|
||||||
|
*,
|
||||||
|
match: Union[str, Pattern[str], None] = None,
|
||||||
|
depth: Optional[int] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Check whether a captured exception group contains a matching exception.
|
||||||
|
|
||||||
|
:param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
|
||||||
|
The expected exception type, or a tuple if one of multiple possible
|
||||||
|
exception types are expected.
|
||||||
|
|
||||||
|
:param str | Pattern[str] | None match:
|
||||||
|
If specified, a string containing a regular expression,
|
||||||
|
or a regular expression object, that is tested against the string
|
||||||
|
representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
|
||||||
|
using :func:`re.search`.
|
||||||
|
|
||||||
|
To match a literal string that may contain :ref:`special characters
|
||||||
|
<re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
|
||||||
|
|
||||||
|
:param Optional[int] depth:
|
||||||
|
If `None`, will search for a matching exception at any nesting depth.
|
||||||
|
If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
|
||||||
|
the exceptions contained within the topmost exception group).
|
||||||
|
"""
|
||||||
|
msg = "Captured exception is not an instance of `BaseExceptionGroup`"
|
||||||
|
assert isinstance(self.value, BaseExceptionGroup), msg
|
||||||
|
msg = "`depth` must be >= 1 if specified"
|
||||||
|
assert (depth is None) or (depth >= 1), msg
|
||||||
|
return self._group_contains(self.value, expected_exception, match, depth)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class FormattedExcinfo:
|
class FormattedExcinfo:
|
||||||
|
|
|
@ -0,0 +1,701 @@
|
||||||
|
# This module was imported from the cpython standard library
|
||||||
|
# (https://github.com/python/cpython/) at commit
|
||||||
|
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Original Author: Fred L. Drake, Jr.
|
||||||
|
# fdrake@acm.org
|
||||||
|
#
|
||||||
|
# This is a simple little module I wrote to make life easier. I didn't
|
||||||
|
# see anything quite like it in the library, though I may have overlooked
|
||||||
|
# something. I wrote this when I was trying to read some heavily nested
|
||||||
|
# tuples with fairly non-descriptive content. This is modeled very much
|
||||||
|
# after Lisp/Scheme - style pretty-printing of lists. If you find it
|
||||||
|
# useful, thank small children who sleep at night.
|
||||||
|
import collections as _collections
|
||||||
|
import dataclasses as _dataclasses
|
||||||
|
import re
|
||||||
|
import types as _types
|
||||||
|
from io import StringIO as _StringIO
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
|
from typing import IO
|
||||||
|
from typing import Iterator
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Set
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class _safe_key:
|
||||||
|
"""Helper function for key functions when sorting unorderable objects.
|
||||||
|
|
||||||
|
The wrapped-object will fallback to a Py2.x style comparison for
|
||||||
|
unorderable types (sorting first comparing the type name and then by
|
||||||
|
the obj ids). Does not work recursively, so dict.items() must have
|
||||||
|
_safe_key applied to both the key and the value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ["obj"]
|
||||||
|
|
||||||
|
def __init__(self, obj):
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
try:
|
||||||
|
return self.obj < other.obj
|
||||||
|
except TypeError:
|
||||||
|
return (str(type(self.obj)), id(self.obj)) < (
|
||||||
|
str(type(other.obj)),
|
||||||
|
id(other.obj),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_tuple(t):
|
||||||
|
"""Helper function for comparing 2-tuples"""
|
||||||
|
return _safe_key(t[0]), _safe_key(t[1])
|
||||||
|
|
||||||
|
|
||||||
|
class PrettyPrinter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
indent: int = 4,
|
||||||
|
width: int = 80,
|
||||||
|
depth: Optional[int] = None,
|
||||||
|
*,
|
||||||
|
sort_dicts: bool = True,
|
||||||
|
underscore_numbers: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Handle pretty printing operations onto a stream using a set of
|
||||||
|
configured parameters.
|
||||||
|
|
||||||
|
indent
|
||||||
|
Number of spaces to indent for each level of nesting.
|
||||||
|
|
||||||
|
width
|
||||||
|
Attempted maximum number of columns in the output.
|
||||||
|
|
||||||
|
depth
|
||||||
|
The maximum depth to print out nested structures.
|
||||||
|
|
||||||
|
sort_dicts
|
||||||
|
If true, dict keys are sorted.
|
||||||
|
|
||||||
|
"""
|
||||||
|
indent = int(indent)
|
||||||
|
width = int(width)
|
||||||
|
if indent < 0:
|
||||||
|
raise ValueError("indent must be >= 0")
|
||||||
|
if depth is not None and depth <= 0:
|
||||||
|
raise ValueError("depth must be > 0")
|
||||||
|
if not width:
|
||||||
|
raise ValueError("width must be != 0")
|
||||||
|
self._depth = depth
|
||||||
|
self._indent_per_level = indent
|
||||||
|
self._width = width
|
||||||
|
self._sort_dicts = sort_dicts
|
||||||
|
self._underscore_numbers = underscore_numbers
|
||||||
|
|
||||||
|
def pformat(self, object: Any) -> str:
|
||||||
|
sio = _StringIO()
|
||||||
|
self._format(object, sio, 0, 0, set(), 0)
|
||||||
|
return sio.getvalue()
|
||||||
|
|
||||||
|
def _format(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
objid = id(object)
|
||||||
|
if objid in context:
|
||||||
|
stream.write(_recursion(object))
|
||||||
|
return
|
||||||
|
|
||||||
|
p = self._dispatch.get(type(object).__repr__, None)
|
||||||
|
if p is not None:
|
||||||
|
context.add(objid)
|
||||||
|
p(self, object, stream, indent, allowance, context, level + 1)
|
||||||
|
context.remove(objid)
|
||||||
|
elif (
|
||||||
|
_dataclasses.is_dataclass(object)
|
||||||
|
and not isinstance(object, type)
|
||||||
|
and object.__dataclass_params__.repr
|
||||||
|
and
|
||||||
|
# Check dataclass has generated repr method.
|
||||||
|
hasattr(object.__repr__, "__wrapped__")
|
||||||
|
and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
|
||||||
|
):
|
||||||
|
context.add(objid)
|
||||||
|
self._pprint_dataclass(
|
||||||
|
object, stream, indent, allowance, context, level + 1
|
||||||
|
)
|
||||||
|
context.remove(objid)
|
||||||
|
else:
|
||||||
|
stream.write(self._repr(object, context, level))
|
||||||
|
|
||||||
|
def _pprint_dataclass(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
cls_name = object.__class__.__name__
|
||||||
|
items = [
|
||||||
|
(f.name, getattr(object, f.name))
|
||||||
|
for f in _dataclasses.fields(object)
|
||||||
|
if f.repr
|
||||||
|
]
|
||||||
|
stream.write(cls_name + "(")
|
||||||
|
self._format_namespace_items(items, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch: Dict[
|
||||||
|
Callable[..., str],
|
||||||
|
Callable[["PrettyPrinter", Any, IO[str], int, int, Set[int], int], None],
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
def _pprint_dict(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
write = stream.write
|
||||||
|
write("{")
|
||||||
|
if self._sort_dicts:
|
||||||
|
items = sorted(object.items(), key=_safe_tuple)
|
||||||
|
else:
|
||||||
|
items = object.items()
|
||||||
|
self._format_dict_items(items, stream, indent, allowance, context, level)
|
||||||
|
write("}")
|
||||||
|
|
||||||
|
_dispatch[dict.__repr__] = _pprint_dict
|
||||||
|
|
||||||
|
def _pprint_ordered_dict(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
cls = object.__class__
|
||||||
|
stream.write(cls.__name__ + "(")
|
||||||
|
self._pprint_dict(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
|
||||||
|
|
||||||
|
def _pprint_list(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write("[")
|
||||||
|
self._format_items(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write("]")
|
||||||
|
|
||||||
|
_dispatch[list.__repr__] = _pprint_list
|
||||||
|
|
||||||
|
def _pprint_tuple(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write("(")
|
||||||
|
self._format_items(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[tuple.__repr__] = _pprint_tuple
|
||||||
|
|
||||||
|
def _pprint_set(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
typ = object.__class__
|
||||||
|
if typ is set:
|
||||||
|
stream.write("{")
|
||||||
|
endchar = "}"
|
||||||
|
else:
|
||||||
|
stream.write(typ.__name__ + "({")
|
||||||
|
endchar = "})"
|
||||||
|
object = sorted(object, key=_safe_key)
|
||||||
|
self._format_items(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write(endchar)
|
||||||
|
|
||||||
|
_dispatch[set.__repr__] = _pprint_set
|
||||||
|
_dispatch[frozenset.__repr__] = _pprint_set
|
||||||
|
|
||||||
|
def _pprint_str(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
write = stream.write
|
||||||
|
if not len(object):
|
||||||
|
write(repr(object))
|
||||||
|
return
|
||||||
|
chunks = []
|
||||||
|
lines = object.splitlines(True)
|
||||||
|
if level == 1:
|
||||||
|
indent += 1
|
||||||
|
allowance += 1
|
||||||
|
max_width1 = max_width = self._width - indent
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
rep = repr(line)
|
||||||
|
if i == len(lines) - 1:
|
||||||
|
max_width1 -= allowance
|
||||||
|
if len(rep) <= max_width1:
|
||||||
|
chunks.append(rep)
|
||||||
|
else:
|
||||||
|
# A list of alternating (non-space, space) strings
|
||||||
|
parts = re.findall(r"\S*\s*", line)
|
||||||
|
assert parts
|
||||||
|
assert not parts[-1]
|
||||||
|
parts.pop() # drop empty last part
|
||||||
|
max_width2 = max_width
|
||||||
|
current = ""
|
||||||
|
for j, part in enumerate(parts):
|
||||||
|
candidate = current + part
|
||||||
|
if j == len(parts) - 1 and i == len(lines) - 1:
|
||||||
|
max_width2 -= allowance
|
||||||
|
if len(repr(candidate)) > max_width2:
|
||||||
|
if current:
|
||||||
|
chunks.append(repr(current))
|
||||||
|
current = part
|
||||||
|
else:
|
||||||
|
current = candidate
|
||||||
|
if current:
|
||||||
|
chunks.append(repr(current))
|
||||||
|
if len(chunks) == 1:
|
||||||
|
write(rep)
|
||||||
|
return
|
||||||
|
if level == 1:
|
||||||
|
write("(")
|
||||||
|
for i, rep in enumerate(chunks):
|
||||||
|
if i > 0:
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
write(rep)
|
||||||
|
if level == 1:
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[str.__repr__] = _pprint_str
|
||||||
|
|
||||||
|
def _pprint_bytes(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
write = stream.write
|
||||||
|
if len(object) <= 4:
|
||||||
|
write(repr(object))
|
||||||
|
return
|
||||||
|
parens = level == 1
|
||||||
|
if parens:
|
||||||
|
indent += 1
|
||||||
|
allowance += 1
|
||||||
|
write("(")
|
||||||
|
delim = ""
|
||||||
|
for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
|
||||||
|
write(delim)
|
||||||
|
write(rep)
|
||||||
|
if not delim:
|
||||||
|
delim = "\n" + " " * indent
|
||||||
|
if parens:
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[bytes.__repr__] = _pprint_bytes
|
||||||
|
|
||||||
|
def _pprint_bytearray(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
write = stream.write
|
||||||
|
write("bytearray(")
|
||||||
|
self._pprint_bytes(
|
||||||
|
bytes(object), stream, indent + 10, allowance + 1, context, level + 1
|
||||||
|
)
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[bytearray.__repr__] = _pprint_bytearray
|
||||||
|
|
||||||
|
def _pprint_mappingproxy(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write("mappingproxy(")
|
||||||
|
self._format(object.copy(), stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
|
||||||
|
|
||||||
|
def _pprint_simplenamespace(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if type(object) is _types.SimpleNamespace:
|
||||||
|
# The SimpleNamespace repr is "namespace" instead of the class
|
||||||
|
# name, so we do the same here. For subclasses; use the class name.
|
||||||
|
cls_name = "namespace"
|
||||||
|
else:
|
||||||
|
cls_name = object.__class__.__name__
|
||||||
|
items = object.__dict__.items()
|
||||||
|
stream.write(cls_name + "(")
|
||||||
|
self._format_namespace_items(items, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
|
||||||
|
|
||||||
|
def _format_dict_items(
|
||||||
|
self,
|
||||||
|
items: List[Tuple[Any, Any]],
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
write = stream.write
|
||||||
|
item_indent = indent + self._indent_per_level
|
||||||
|
delimnl = "\n" + " " * item_indent
|
||||||
|
for key, ent in items:
|
||||||
|
write(delimnl)
|
||||||
|
write(self._repr(key, context, level))
|
||||||
|
write(": ")
|
||||||
|
self._format(ent, stream, item_indent, 1, context, level)
|
||||||
|
write(",")
|
||||||
|
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
|
||||||
|
def _format_namespace_items(
|
||||||
|
self,
|
||||||
|
items: List[Tuple[Any, Any]],
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
write = stream.write
|
||||||
|
item_indent = indent + self._indent_per_level
|
||||||
|
delimnl = "\n" + " " * item_indent
|
||||||
|
for key, ent in items:
|
||||||
|
write(delimnl)
|
||||||
|
write(key)
|
||||||
|
write("=")
|
||||||
|
if id(ent) in context:
|
||||||
|
# Special-case representation of recursion to match standard
|
||||||
|
# recursive dataclass repr.
|
||||||
|
write("...")
|
||||||
|
else:
|
||||||
|
self._format(
|
||||||
|
ent,
|
||||||
|
stream,
|
||||||
|
item_indent + len(key) + 1,
|
||||||
|
1,
|
||||||
|
context,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
|
||||||
|
write(",")
|
||||||
|
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
|
||||||
|
def _format_items(
|
||||||
|
self,
|
||||||
|
items: List[Any],
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
write = stream.write
|
||||||
|
item_indent = indent + self._indent_per_level
|
||||||
|
delimnl = "\n" + " " * item_indent
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
write(delimnl)
|
||||||
|
self._format(item, stream, item_indent, 1, context, level)
|
||||||
|
write(",")
|
||||||
|
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
|
||||||
|
def _repr(self, object: Any, context: Set[int], level: int) -> str:
|
||||||
|
return self.format(object, context.copy(), self._depth, level)
|
||||||
|
|
||||||
|
def format(
|
||||||
|
self, object: Any, context: Set[int], maxlevels: Optional[int], level: int
|
||||||
|
) -> str:
|
||||||
|
return self._safe_repr(object, context, maxlevels, level)
|
||||||
|
|
||||||
|
def _pprint_default_dict(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
rdf = self._repr(object.default_factory, context, level)
|
||||||
|
stream.write(f"{object.__class__.__name__}({rdf}, ")
|
||||||
|
self._pprint_dict(object, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
|
||||||
|
|
||||||
|
def _pprint_counter(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write(object.__class__.__name__ + "(")
|
||||||
|
|
||||||
|
if object:
|
||||||
|
stream.write("{")
|
||||||
|
items = object.most_common()
|
||||||
|
self._format_dict_items(items, stream, indent, allowance, context, level)
|
||||||
|
stream.write("}")
|
||||||
|
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.Counter.__repr__] = _pprint_counter
|
||||||
|
|
||||||
|
def _pprint_chain_map(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
|
||||||
|
stream.write(object.__class__.__name__ + "(")
|
||||||
|
self._format_items(object.maps, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
|
||||||
|
|
||||||
|
def _pprint_deque(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
stream.write(object.__class__.__name__ + "(")
|
||||||
|
if object.maxlen is not None:
|
||||||
|
stream.write("maxlen=%d, " % object.maxlen)
|
||||||
|
stream.write("[")
|
||||||
|
|
||||||
|
self._format_items(object, stream, indent, allowance + 1, context, level)
|
||||||
|
stream.write("])")
|
||||||
|
|
||||||
|
_dispatch[_collections.deque.__repr__] = _pprint_deque
|
||||||
|
|
||||||
|
def _pprint_user_dict(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
|
||||||
|
|
||||||
|
def _pprint_user_list(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserList.__repr__] = _pprint_user_list
|
||||||
|
|
||||||
|
def _pprint_user_string(
|
||||||
|
self,
|
||||||
|
object: Any,
|
||||||
|
stream: IO[str],
|
||||||
|
indent: int,
|
||||||
|
allowance: int,
|
||||||
|
context: Set[int],
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserString.__repr__] = _pprint_user_string
|
||||||
|
|
||||||
|
def _safe_repr(
|
||||||
|
self, object: Any, context: Set[int], maxlevels: Optional[int], level: int
|
||||||
|
) -> str:
|
||||||
|
typ = type(object)
|
||||||
|
if typ in _builtin_scalars:
|
||||||
|
return repr(object)
|
||||||
|
|
||||||
|
r = getattr(typ, "__repr__", None)
|
||||||
|
|
||||||
|
if issubclass(typ, int) and r is int.__repr__:
|
||||||
|
if self._underscore_numbers:
|
||||||
|
return f"{object:_d}"
|
||||||
|
else:
|
||||||
|
return repr(object)
|
||||||
|
|
||||||
|
if issubclass(typ, dict) and r is dict.__repr__:
|
||||||
|
if not object:
|
||||||
|
return "{}"
|
||||||
|
objid = id(object)
|
||||||
|
if maxlevels and level >= maxlevels:
|
||||||
|
return "{...}"
|
||||||
|
if objid in context:
|
||||||
|
return _recursion(object)
|
||||||
|
context.add(objid)
|
||||||
|
components: List[str] = []
|
||||||
|
append = components.append
|
||||||
|
level += 1
|
||||||
|
if self._sort_dicts:
|
||||||
|
items = sorted(object.items(), key=_safe_tuple)
|
||||||
|
else:
|
||||||
|
items = object.items()
|
||||||
|
for k, v in items:
|
||||||
|
krepr = self.format(k, context, maxlevels, level)
|
||||||
|
vrepr = self.format(v, context, maxlevels, level)
|
||||||
|
append(f"{krepr}: {vrepr}")
|
||||||
|
context.remove(objid)
|
||||||
|
return "{%s}" % ", ".join(components)
|
||||||
|
|
||||||
|
if (issubclass(typ, list) and r is list.__repr__) or (
|
||||||
|
issubclass(typ, tuple) and r is tuple.__repr__
|
||||||
|
):
|
||||||
|
if issubclass(typ, list):
|
||||||
|
if not object:
|
||||||
|
return "[]"
|
||||||
|
format = "[%s]"
|
||||||
|
elif len(object) == 1:
|
||||||
|
format = "(%s,)"
|
||||||
|
else:
|
||||||
|
if not object:
|
||||||
|
return "()"
|
||||||
|
format = "(%s)"
|
||||||
|
objid = id(object)
|
||||||
|
if maxlevels and level >= maxlevels:
|
||||||
|
return format % "..."
|
||||||
|
if objid in context:
|
||||||
|
return _recursion(object)
|
||||||
|
context.add(objid)
|
||||||
|
components = []
|
||||||
|
append = components.append
|
||||||
|
level += 1
|
||||||
|
for o in object:
|
||||||
|
orepr = self.format(o, context, maxlevels, level)
|
||||||
|
append(orepr)
|
||||||
|
context.remove(objid)
|
||||||
|
return format % ", ".join(components)
|
||||||
|
|
||||||
|
return repr(object)
|
||||||
|
|
||||||
|
|
||||||
|
_builtin_scalars = frozenset({str, bytes, bytearray, float, complex, bool, type(None)})
|
||||||
|
|
||||||
|
|
||||||
|
def _recursion(object: Any) -> str:
|
||||||
|
return f"<Recursion on {type(object).__name__} with id={id(object)}>"
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]:
|
||||||
|
current = b""
|
||||||
|
last = len(object) // 4 * 4
|
||||||
|
for i in range(0, len(object), 4):
|
||||||
|
part = object[i : i + 4]
|
||||||
|
candidate = current + part
|
||||||
|
if i == last:
|
||||||
|
width -= allowance
|
||||||
|
if len(repr(candidate)) > width:
|
||||||
|
if current:
|
||||||
|
yield repr(current)
|
||||||
|
current = part
|
||||||
|
else:
|
||||||
|
current = candidate
|
||||||
|
if current:
|
||||||
|
yield repr(current)
|
|
@ -1,8 +1,5 @@
|
||||||
import pprint
|
import pprint
|
||||||
import reprlib
|
import reprlib
|
||||||
from typing import Any
|
|
||||||
from typing import Dict
|
|
||||||
from typing import IO
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,49 +129,3 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
|
||||||
return repr(obj)
|
return repr(obj)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return _format_repr_exception(exc, obj)
|
return _format_repr_exception(exc, obj)
|
||||||
|
|
||||||
|
|
||||||
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
|
||||||
"""PrettyPrinter that always dispatches (regardless of width)."""
|
|
||||||
|
|
||||||
def _format(
|
|
||||||
self,
|
|
||||||
object: object,
|
|
||||||
stream: IO[str],
|
|
||||||
indent: int,
|
|
||||||
allowance: int,
|
|
||||||
context: Dict[int, Any],
|
|
||||||
level: int,
|
|
||||||
) -> None:
|
|
||||||
# Type ignored because _dispatch is private.
|
|
||||||
p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
objid = id(object)
|
|
||||||
if objid in context or p is None:
|
|
||||||
# Type ignored because _format is private.
|
|
||||||
super()._format( # type: ignore[misc]
|
|
||||||
object,
|
|
||||||
stream,
|
|
||||||
indent,
|
|
||||||
allowance,
|
|
||||||
context,
|
|
||||||
level,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
context[objid] = 1
|
|
||||||
p(self, object, stream, indent, allowance, context, level + 1)
|
|
||||||
del context[objid]
|
|
||||||
|
|
||||||
|
|
||||||
def _pformat_dispatch(
|
|
||||||
object: object,
|
|
||||||
indent: int = 1,
|
|
||||||
width: int = 80,
|
|
||||||
depth: Optional[int] = None,
|
|
||||||
*,
|
|
||||||
compact: bool = False,
|
|
||||||
) -> str:
|
|
||||||
return AlwaysDispatchingPrettyPrinter(
|
|
||||||
indent=indent, width=width, depth=depth, compact=compact
|
|
||||||
).pformat(object)
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from typing import final
|
from typing import final
|
||||||
|
from typing import Literal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
@ -193,15 +194,21 @@ class TerminalWriter:
|
||||||
for indent, new_line in zip(indents, new_lines):
|
for indent, new_line in zip(indents, new_lines):
|
||||||
self.line(indent + new_line)
|
self.line(indent + new_line)
|
||||||
|
|
||||||
def _highlight(self, source: str) -> str:
|
def _highlight(
|
||||||
"""Highlight the given source code if we have markup support."""
|
self, source: str, lexer: Literal["diff", "python"] = "python"
|
||||||
|
) -> str:
|
||||||
|
"""Highlight the given source if we have markup support."""
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
|
|
||||||
if not self.hasmarkup or not self.code_highlight:
|
if not self.hasmarkup or not self.code_highlight:
|
||||||
return source
|
return source
|
||||||
try:
|
try:
|
||||||
from pygments.formatters.terminal import TerminalFormatter
|
from pygments.formatters.terminal import TerminalFormatter
|
||||||
from pygments.lexers.python import PythonLexer
|
|
||||||
|
if lexer == "python":
|
||||||
|
from pygments.lexers.python import PythonLexer as Lexer
|
||||||
|
elif lexer == "diff":
|
||||||
|
from pygments.lexers.diff import DiffLexer as Lexer
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
import pygments.util
|
import pygments.util
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -210,7 +217,7 @@ class TerminalWriter:
|
||||||
try:
|
try:
|
||||||
highlighted: str = highlight(
|
highlighted: str = highlight(
|
||||||
source,
|
source,
|
||||||
PythonLexer(),
|
Lexer(),
|
||||||
TerminalFormatter(
|
TerminalFormatter(
|
||||||
bg=os.getenv("PYTEST_THEME_MODE", "dark"),
|
bg=os.getenv("PYTEST_THEME_MODE", "dark"),
|
||||||
style=os.getenv("PYTEST_THEME"),
|
style=os.getenv("PYTEST_THEME"),
|
||||||
|
|
|
@ -755,7 +755,13 @@ class LocalPath:
|
||||||
if ensure:
|
if ensure:
|
||||||
self.dirpath().ensure(dir=1)
|
self.dirpath().ensure(dir=1)
|
||||||
if encoding:
|
if encoding:
|
||||||
return error.checked_call(io.open, self.strpath, mode, encoding=encoding)
|
# Using type ignore here because of this error:
|
||||||
|
# error: Argument 1 has incompatible type overloaded function;
|
||||||
|
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
|
||||||
|
# Which seems incorrect, given io.open supports the given argument types.
|
||||||
|
return error.checked_call(
|
||||||
|
io.open, self.strpath, mode, encoding=encoding # type:ignore[arg-type]
|
||||||
|
)
|
||||||
return error.checked_call(open, self.strpath, mode)
|
return error.checked_call(open, self.strpath, mode)
|
||||||
|
|
||||||
def _fastjoin(self, name):
|
def _fastjoin(self, name):
|
||||||
|
@ -1261,13 +1267,19 @@ class LocalPath:
|
||||||
@classmethod
|
@classmethod
|
||||||
def mkdtemp(cls, rootdir=None):
|
def mkdtemp(cls, rootdir=None):
|
||||||
"""Return a Path object pointing to a fresh new temporary directory
|
"""Return a Path object pointing to a fresh new temporary directory
|
||||||
(which we created ourself).
|
(which we created ourselves).
|
||||||
"""
|
"""
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
if rootdir is None:
|
if rootdir is None:
|
||||||
rootdir = cls.get_temproot()
|
rootdir = cls.get_temproot()
|
||||||
return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir)))
|
# Using type ignore here because of this error:
|
||||||
|
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
|
||||||
|
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
|
||||||
|
path = error.checked_call(
|
||||||
|
tempfile.mkdtemp, dir=str(rootdir) # type:ignore[arg-type]
|
||||||
|
)
|
||||||
|
return cls(path)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_numbered_dir(
|
def make_numbered_dir(
|
||||||
|
|
|
@ -42,6 +42,14 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
help="Enables the pytest_assertion_pass hook. "
|
help="Enables the pytest_assertion_pass hook. "
|
||||||
"Make sure to delete any previously generated pyc cache files.",
|
"Make sure to delete any previously generated pyc cache files.",
|
||||||
)
|
)
|
||||||
|
Config._add_verbosity_ini(
|
||||||
|
parser,
|
||||||
|
Config.VERBOSITY_ASSERTIONS,
|
||||||
|
help=(
|
||||||
|
"Specify a verbosity level for assertions, overriding the main level. "
|
||||||
|
"Higher levels will provide more detailed explanation when an assertion fails."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_assert_rewrite(*names: str) -> None:
|
def register_assert_rewrite(*names: str) -> None:
|
||||||
|
|
|
@ -13,9 +13,11 @@ import struct
|
||||||
import sys
|
import sys
|
||||||
import tokenize
|
import tokenize
|
||||||
import types
|
import types
|
||||||
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import DefaultDict
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import IO
|
from typing import IO
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
@ -45,6 +47,10 @@ if TYPE_CHECKING:
|
||||||
from _pytest.assertion import AssertionState
|
from _pytest.assertion import AssertionState
|
||||||
|
|
||||||
|
|
||||||
|
class Sentinel:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
assertstate_key = StashKey["AssertionState"]()
|
assertstate_key = StashKey["AssertionState"]()
|
||||||
|
|
||||||
# pytest caches rewritten pycs in pycache dirs
|
# pytest caches rewritten pycs in pycache dirs
|
||||||
|
@ -52,6 +58,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
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):
|
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
||||||
"""PEP302/PEP451 import hook which rewrites asserts."""
|
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||||
|
@ -418,7 +427,10 @@ def _saferepr(obj: object) -> str:
|
||||||
|
|
||||||
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
|
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
|
||||||
"""Get `maxsize` configuration for saferepr based on the given config object."""
|
"""Get `maxsize` configuration for saferepr based on the given config object."""
|
||||||
verbosity = config.getoption("verbose") if config is not None else 0
|
if config is None:
|
||||||
|
verbosity = 0
|
||||||
|
else:
|
||||||
|
verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
return None
|
return None
|
||||||
if verbosity >= 1:
|
if verbosity >= 1:
|
||||||
|
@ -634,6 +646,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
.push_format_context() and .pop_format_context() which allows
|
.push_format_context() and .pop_format_context() which allows
|
||||||
to build another %-formatted string while already building one.
|
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
|
:variables_overwrite: A dict filled with references to variables
|
||||||
that change value within an assert. This happens when a variable is
|
that change value within an assert. This happens when a variable is
|
||||||
reassigned with the walrus operator
|
reassigned with the walrus operator
|
||||||
|
@ -655,7 +669,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
else:
|
else:
|
||||||
self.enable_assertion_pass_hook = False
|
self.enable_assertion_pass_hook = False
|
||||||
self.source = source
|
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:
|
def run(self, mod: ast.Module) -> None:
|
||||||
"""Find all assert statements in *mod* and rewrite them."""
|
"""Find all assert statements in *mod* and rewrite them."""
|
||||||
|
@ -719,9 +736,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
mod.body[pos:pos] = imports
|
mod.body[pos:pos] = imports
|
||||||
|
|
||||||
# Collect asserts.
|
# Collect asserts.
|
||||||
nodes: List[ast.AST] = [mod]
|
self.scope = (mod,)
|
||||||
|
nodes: List[Union[ast.AST, Sentinel]] = [mod]
|
||||||
while nodes:
|
while nodes:
|
||||||
node = nodes.pop()
|
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):
|
for name, field in ast.iter_fields(node):
|
||||||
if isinstance(field, list):
|
if isinstance(field, list):
|
||||||
new: List[ast.AST] = []
|
new: List[ast.AST] = []
|
||||||
|
@ -992,7 +1017,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
pytest_temp = self.variable()
|
pytest_temp = self.variable()
|
||||||
self.variables_overwrite[
|
self.variables_overwrite[self.scope][
|
||||||
v.left.target.id
|
v.left.target.id
|
||||||
] = v.left # type:ignore[assignment]
|
] = v.left # type:ignore[assignment]
|
||||||
v.left.target.id = pytest_temp
|
v.left.target.id = pytest_temp
|
||||||
|
@ -1035,17 +1060,20 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
new_args = []
|
new_args = []
|
||||||
new_kwargs = []
|
new_kwargs = []
|
||||||
for arg in call.args:
|
for arg in call.args:
|
||||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
|
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||||
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
|
self.scope, {}
|
||||||
|
):
|
||||||
|
arg = self.variables_overwrite[self.scope][
|
||||||
|
arg.id
|
||||||
|
] # type:ignore[assignment]
|
||||||
res, expl = self.visit(arg)
|
res, expl = self.visit(arg)
|
||||||
arg_expls.append(expl)
|
arg_expls.append(expl)
|
||||||
new_args.append(res)
|
new_args.append(res)
|
||||||
for keyword in call.keywords:
|
for keyword in call.keywords:
|
||||||
if (
|
if isinstance(
|
||||||
isinstance(keyword.value, ast.Name)
|
keyword.value, ast.Name
|
||||||
and keyword.value.id in self.variables_overwrite
|
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||||
):
|
keyword.value = self.variables_overwrite[self.scope][
|
||||||
keyword.value = self.variables_overwrite[
|
|
||||||
keyword.value.id
|
keyword.value.id
|
||||||
] # type:ignore[assignment]
|
] # type:ignore[assignment]
|
||||||
res, expl = self.visit(keyword.value)
|
res, expl = self.visit(keyword.value)
|
||||||
|
@ -1081,12 +1109,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
# We first check if we have overwritten a variable in the previous assert
|
# We first check if we have overwritten a variable in the previous assert
|
||||||
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
if isinstance(
|
||||||
comp.left = self.variables_overwrite[
|
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
|
comp.left.id
|
||||||
] # type:ignore[assignment]
|
] # type:ignore[assignment]
|
||||||
if isinstance(comp.left, ast.NamedExpr):
|
if isinstance(comp.left, ast.NamedExpr):
|
||||||
self.variables_overwrite[
|
self.variables_overwrite[self.scope][
|
||||||
comp.left.target.id
|
comp.left.target.id
|
||||||
] = comp.left # type:ignore[assignment]
|
] = comp.left # type:ignore[assignment]
|
||||||
left_res, left_expl = self.visit(comp.left)
|
left_res, left_expl = self.visit(comp.left)
|
||||||
|
@ -1106,7 +1136,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
and next_operand.target.id == left_res.id
|
and next_operand.target.id == left_res.id
|
||||||
):
|
):
|
||||||
next_operand.target.id = self.variable()
|
next_operand.target.id = self.variable()
|
||||||
self.variables_overwrite[
|
self.variables_overwrite[self.scope][
|
||||||
left_res.id
|
left_res.id
|
||||||
] = next_operand # type:ignore[assignment]
|
] = next_operand # type:ignore[assignment]
|
||||||
next_res, next_expl = self.visit(next_operand)
|
next_res, next_expl = self.visit(next_operand)
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
"""Utilities for truncating assertion output.
|
"""Utilities for truncating assertion output.
|
||||||
|
|
||||||
Current default behaviour is to truncate assertion explanations at
|
Current default behaviour is to truncate assertion explanations at
|
||||||
~8 terminal lines, unless running in "-vv" mode or running on CI.
|
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
|
||||||
"""
|
"""
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ def truncate_if_required(
|
||||||
|
|
||||||
def _should_truncate_item(item: Item) -> bool:
|
def _should_truncate_item(item: Item) -> bool:
|
||||||
"""Whether or not this test item is eligible for truncation."""
|
"""Whether or not this test item is eligible for truncation."""
|
||||||
verbose = item.config.option.verbose
|
verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
return verbose < 2 and not util.running_on_ci()
|
return verbose < 2 and not util.running_on_ci()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,16 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Literal
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Protocol
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from unicodedata import normalize
|
from unicodedata import normalize
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest._io.saferepr import _pformat_dispatch
|
from _pytest._io.pprint import PrettyPrinter
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest._io.saferepr import saferepr_unlimited
|
from _pytest._io.saferepr import saferepr_unlimited
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
@ -33,6 +35,11 @@ _assertion_pass: Optional[Callable[[int, str, str], None]] = None
|
||||||
_config: Optional[Config] = None
|
_config: Optional[Config] = None
|
||||||
|
|
||||||
|
|
||||||
|
class _HighlightFunc(Protocol):
|
||||||
|
def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
|
||||||
|
"""Apply highlighting to the given source."""
|
||||||
|
|
||||||
|
|
||||||
def format_explanation(explanation: str) -> str:
|
def format_explanation(explanation: str) -> str:
|
||||||
r"""Format an explanation.
|
r"""Format an explanation.
|
||||||
|
|
||||||
|
@ -132,7 +139,7 @@ def isiterable(obj: Any) -> bool:
|
||||||
try:
|
try:
|
||||||
iter(obj)
|
iter(obj)
|
||||||
return not istext(obj)
|
return not istext(obj)
|
||||||
except TypeError:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@ -161,7 +168,7 @@ def assertrepr_compare(
|
||||||
config, op: str, left: Any, right: Any, use_ascii: bool = False
|
config, op: str, left: Any, right: Any, use_ascii: bool = False
|
||||||
) -> Optional[List[str]]:
|
) -> Optional[List[str]]:
|
||||||
"""Return specialised explanations for some operators/operands."""
|
"""Return specialised explanations for some operators/operands."""
|
||||||
verbose = config.getoption("verbose")
|
verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
|
|
||||||
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
|
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
|
||||||
# See issue #3246.
|
# See issue #3246.
|
||||||
|
@ -189,10 +196,27 @@ def assertrepr_compare(
|
||||||
explanation = None
|
explanation = None
|
||||||
try:
|
try:
|
||||||
if op == "==":
|
if op == "==":
|
||||||
explanation = _compare_eq_any(left, right, verbose)
|
writer = config.get_terminal_writer()
|
||||||
|
explanation = _compare_eq_any(left, right, writer._highlight, verbose)
|
||||||
elif op == "not in":
|
elif op == "not in":
|
||||||
if istext(left) and istext(right):
|
if istext(left) and istext(right):
|
||||||
explanation = _notin_text(left, right, verbose)
|
explanation = _notin_text(left, right, verbose)
|
||||||
|
elif op == "!=":
|
||||||
|
if isset(left) and isset(right):
|
||||||
|
explanation = ["Both sets are equal"]
|
||||||
|
elif op == ">=":
|
||||||
|
if isset(left) and isset(right):
|
||||||
|
explanation = _compare_gte_set(left, right, verbose)
|
||||||
|
elif op == "<=":
|
||||||
|
if isset(left) and isset(right):
|
||||||
|
explanation = _compare_lte_set(left, right, verbose)
|
||||||
|
elif op == ">":
|
||||||
|
if isset(left) and isset(right):
|
||||||
|
explanation = _compare_gt_set(left, right, verbose)
|
||||||
|
elif op == "<":
|
||||||
|
if isset(left) and isset(right):
|
||||||
|
explanation = _compare_lt_set(left, right, verbose)
|
||||||
|
|
||||||
except outcomes.Exit:
|
except outcomes.Exit:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -206,10 +230,14 @@ def assertrepr_compare(
|
||||||
if not explanation:
|
if not explanation:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if explanation[0] != "":
|
||||||
|
explanation = [""] + explanation
|
||||||
return [summary] + explanation
|
return [summary] + explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
def _compare_eq_any(
|
||||||
|
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0
|
||||||
|
) -> List[str]:
|
||||||
explanation = []
|
explanation = []
|
||||||
if istext(left) and istext(right):
|
if istext(left) and istext(right):
|
||||||
explanation = _diff_text(left, right, verbose)
|
explanation = _diff_text(left, right, verbose)
|
||||||
|
@ -229,7 +257,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||||
# field values, not the type or field names. But this branch
|
# field values, not the type or field names. But this branch
|
||||||
# intentionally only handles the same-type case, which was often
|
# intentionally only handles the same-type case, which was often
|
||||||
# used in older code bases before dataclasses/attrs were available.
|
# used in older code bases before dataclasses/attrs were available.
|
||||||
explanation = _compare_eq_cls(left, right, verbose)
|
explanation = _compare_eq_cls(left, right, highlighter, verbose)
|
||||||
elif issequence(left) and issequence(right):
|
elif issequence(left) and issequence(right):
|
||||||
explanation = _compare_eq_sequence(left, right, verbose)
|
explanation = _compare_eq_sequence(left, right, verbose)
|
||||||
elif isset(left) and isset(right):
|
elif isset(left) and isset(right):
|
||||||
|
@ -238,7 +266,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||||
explanation = _compare_eq_dict(left, right, verbose)
|
explanation = _compare_eq_dict(left, right, verbose)
|
||||||
|
|
||||||
if isiterable(left) and isiterable(right):
|
if isiterable(left) and isiterable(right):
|
||||||
expl = _compare_eq_iterable(left, right, verbose)
|
expl = _compare_eq_iterable(left, right, highlighter, verbose)
|
||||||
explanation.extend(expl)
|
explanation.extend(expl)
|
||||||
|
|
||||||
return explanation
|
return explanation
|
||||||
|
@ -292,45 +320,31 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
|
|
||||||
"""Move opening/closing parenthesis/bracket to own lines."""
|
|
||||||
opening = lines[0][:1]
|
|
||||||
if opening in ["(", "[", "{"]:
|
|
||||||
lines[0] = " " + lines[0][1:]
|
|
||||||
lines[:] = [opening] + lines
|
|
||||||
closing = lines[-1][-1:]
|
|
||||||
if closing in [")", "]", "}"]:
|
|
||||||
lines[-1] = lines[-1][:-1] + ","
|
|
||||||
lines[:] = lines + [closing]
|
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_iterable(
|
def _compare_eq_iterable(
|
||||||
left: Iterable[Any], right: Iterable[Any], verbose: int = 0
|
left: Iterable[Any],
|
||||||
|
right: Iterable[Any],
|
||||||
|
highligher: _HighlightFunc,
|
||||||
|
verbose: int = 0,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
if verbose <= 0 and not running_on_ci():
|
if verbose <= 0 and not running_on_ci():
|
||||||
return ["Use -v to get more diff"]
|
return ["Use -v to get more diff"]
|
||||||
# dynamic import to speedup pytest
|
# dynamic import to speedup pytest
|
||||||
import difflib
|
import difflib
|
||||||
|
|
||||||
left_formatting = pprint.pformat(left).splitlines()
|
left_formatting = PrettyPrinter().pformat(left).splitlines()
|
||||||
right_formatting = pprint.pformat(right).splitlines()
|
right_formatting = PrettyPrinter().pformat(right).splitlines()
|
||||||
|
|
||||||
# Re-format for different output lengths.
|
explanation = ["", "Full diff:"]
|
||||||
lines_left = len(left_formatting)
|
|
||||||
lines_right = len(right_formatting)
|
|
||||||
if lines_left != lines_right:
|
|
||||||
left_formatting = _pformat_dispatch(left).splitlines()
|
|
||||||
right_formatting = _pformat_dispatch(right).splitlines()
|
|
||||||
|
|
||||||
if lines_left > 1 or lines_right > 1:
|
|
||||||
_surrounding_parens_on_own_lines(left_formatting)
|
|
||||||
_surrounding_parens_on_own_lines(right_formatting)
|
|
||||||
|
|
||||||
explanation = ["Full diff:"]
|
|
||||||
# "right" is the expected base against which we compare "left",
|
# "right" is the expected base against which we compare "left",
|
||||||
# see https://github.com/pytest-dev/pytest/issues/3333
|
# see https://github.com/pytest-dev/pytest/issues/3333
|
||||||
explanation.extend(
|
explanation.extend(
|
||||||
line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
|
highligher(
|
||||||
|
"\n".join(
|
||||||
|
line.rstrip()
|
||||||
|
for line in difflib.ndiff(right_formatting, left_formatting)
|
||||||
|
),
|
||||||
|
lexer="diff",
|
||||||
|
).splitlines()
|
||||||
)
|
)
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
@ -392,15 +406,49 @@ def _compare_eq_set(
|
||||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
explanation = []
|
explanation = []
|
||||||
diff_left = left - right
|
explanation.extend(_set_one_sided_diff("left", left, right))
|
||||||
diff_right = right - left
|
explanation.extend(_set_one_sided_diff("right", right, left))
|
||||||
if diff_left:
|
return explanation
|
||||||
explanation.append("Extra items in the left set:")
|
|
||||||
for item in diff_left:
|
|
||||||
explanation.append(saferepr(item))
|
def _compare_gt_set(
|
||||||
if diff_right:
|
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||||
explanation.append("Extra items in the right set:")
|
) -> List[str]:
|
||||||
for item in diff_right:
|
explanation = _compare_gte_set(left, right, verbose)
|
||||||
|
if not explanation:
|
||||||
|
return ["Both sets are equal"]
|
||||||
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
|
def _compare_lt_set(
|
||||||
|
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||||
|
) -> List[str]:
|
||||||
|
explanation = _compare_lte_set(left, right, verbose)
|
||||||
|
if not explanation:
|
||||||
|
return ["Both sets are equal"]
|
||||||
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
|
def _compare_gte_set(
|
||||||
|
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||||
|
) -> List[str]:
|
||||||
|
return _set_one_sided_diff("right", right, left)
|
||||||
|
|
||||||
|
|
||||||
|
def _compare_lte_set(
|
||||||
|
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||||
|
) -> List[str]:
|
||||||
|
return _set_one_sided_diff("left", left, right)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_one_sided_diff(
|
||||||
|
posn: str, set1: AbstractSet[Any], set2: AbstractSet[Any]
|
||||||
|
) -> List[str]:
|
||||||
|
explanation = []
|
||||||
|
diff = set1 - set2
|
||||||
|
if diff:
|
||||||
|
explanation.append(f"Extra items in the {posn} set:")
|
||||||
|
for item in diff:
|
||||||
explanation.append(saferepr(item))
|
explanation.append(saferepr(item))
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
@ -446,7 +494,9 @@ def _compare_eq_dict(
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
|
def _compare_eq_cls(
|
||||||
|
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int
|
||||||
|
) -> List[str]:
|
||||||
if not has_default_eq(left):
|
if not has_default_eq(left):
|
||||||
return []
|
return []
|
||||||
if isdatacls(left):
|
if isdatacls(left):
|
||||||
|
@ -492,7 +542,9 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
|
||||||
]
|
]
|
||||||
explanation += [
|
explanation += [
|
||||||
indent + line
|
indent + line
|
||||||
for line in _compare_eq_any(field_left, field_right, verbose)
|
for line in _compare_eq_any(
|
||||||
|
field_left, field_right, highlighter, verbose
|
||||||
|
)
|
||||||
]
|
]
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
|
@ -499,7 +499,11 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
dest="last_failed_no_failures",
|
dest="last_failed_no_failures",
|
||||||
choices=("all", "none"),
|
choices=("all", "none"),
|
||||||
default="all",
|
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.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -588,7 +588,7 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class CaptureResult(NamedTuple, Generic[AnyStr]):
|
class CaptureResult(NamedTuple, Generic[AnyStr]):
|
||||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||||
|
|
||||||
out: AnyStr
|
out: AnyStr
|
||||||
err: AnyStr
|
err: AnyStr
|
||||||
|
@ -598,7 +598,7 @@ else:
|
||||||
class CaptureResult(
|
class CaptureResult(
|
||||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||||
):
|
):
|
||||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
|
@ -314,15 +314,24 @@ def safe_isclass(obj: object) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def get_user_id() -> int | None:
|
def get_user_id() -> int | None:
|
||||||
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
|
"""Return the current 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.
|
# win32 does not have a getuid() function.
|
||||||
# On Emscripten, getuid() is a stub that always returns 0.
|
# Emscripten has a return 0 stub.
|
||||||
if sys.platform in ("win32", "emscripten"):
|
|
||||||
return None
|
return None
|
||||||
# getuid shouldn't fail, but cpython defines such a case.
|
else:
|
||||||
# Let's hope for the best.
|
# 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()
|
uid = os.getuid()
|
||||||
return uid if uid != -1 else None
|
return uid if uid != ERROR else None
|
||||||
|
|
||||||
|
|
||||||
# Perform exhaustiveness checking.
|
# Perform exhaustiveness checking.
|
||||||
|
|
|
@ -22,6 +22,7 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import Final
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
@ -37,13 +38,17 @@ from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
import pluggy
|
||||||
from pluggy import HookimplMarker
|
from pluggy import HookimplMarker
|
||||||
|
from pluggy import HookimplOpts
|
||||||
from pluggy import HookspecMarker
|
from pluggy import HookspecMarker
|
||||||
|
from pluggy import HookspecOpts
|
||||||
from pluggy import PluginManager
|
from pluggy import PluginManager
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import _pytest.deprecated
|
import _pytest.deprecated
|
||||||
import _pytest.hookspec
|
import _pytest.hookspec
|
||||||
|
from .compat import PathAwareHookProxy
|
||||||
from .exceptions import PrintHelp as PrintHelp
|
from .exceptions import PrintHelp as PrintHelp
|
||||||
from .exceptions import UsageError as UsageError
|
from .exceptions import UsageError as UsageError
|
||||||
from .findpaths import determine_setup
|
from .findpaths import determine_setup
|
||||||
|
@ -57,6 +62,7 @@ from _pytest.pathlib import bestrelpath
|
||||||
from _pytest.pathlib import import_path
|
from _pytest.pathlib import import_path
|
||||||
from _pytest.pathlib import ImportMode
|
from _pytest.pathlib import ImportMode
|
||||||
from _pytest.pathlib import resolve_package_path
|
from _pytest.pathlib import resolve_package_path
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
from _pytest.stash import Stash
|
from _pytest.stash import Stash
|
||||||
from _pytest.warning_types import PytestConfigWarning
|
from _pytest.warning_types import PytestConfigWarning
|
||||||
from _pytest.warning_types import warn_explicit_for
|
from _pytest.warning_types import warn_explicit_for
|
||||||
|
@ -64,7 +70,7 @@ from _pytest.warning_types import warn_explicit_for
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest._code.code import _TracebackStyle
|
from _pytest._code.code import _TracebackStyle
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
from .argparsing import Argument
|
from .argparsing import Argument, Parser
|
||||||
|
|
||||||
|
|
||||||
_PluggyPlugin = object
|
_PluggyPlugin = object
|
||||||
|
@ -440,15 +446,18 @@ class PytestPluginManager(PluginManager):
|
||||||
# Used to know when we are importing conftests after the pytest_configure stage.
|
# Used to know when we are importing conftests after the pytest_configure stage.
|
||||||
self._configured = False
|
self._configured = False
|
||||||
|
|
||||||
def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
|
def parse_hookimpl_opts(
|
||||||
|
self, plugin: _PluggyPlugin, name: str
|
||||||
|
) -> Optional[HookimplOpts]:
|
||||||
|
""":meta private:"""
|
||||||
# pytest hooks are always prefixed with "pytest_",
|
# pytest hooks are always prefixed with "pytest_",
|
||||||
# so we avoid accessing possibly non-readable attributes
|
# so we avoid accessing possibly non-readable attributes
|
||||||
# (see issue #1073).
|
# (see issue #1073).
|
||||||
if not name.startswith("pytest_"):
|
if not name.startswith("pytest_"):
|
||||||
return
|
return None
|
||||||
# Ignore names which can not be hooks.
|
# Ignore names which can not be hooks.
|
||||||
if name == "pytest_plugins":
|
if name == "pytest_plugins":
|
||||||
return
|
return None
|
||||||
|
|
||||||
opts = super().parse_hookimpl_opts(plugin, name)
|
opts = super().parse_hookimpl_opts(plugin, name)
|
||||||
if opts is not None:
|
if opts is not None:
|
||||||
|
@ -457,18 +466,19 @@ class PytestPluginManager(PluginManager):
|
||||||
method = getattr(plugin, name)
|
method = getattr(plugin, name)
|
||||||
# Consider only actual functions for hooks (#3775).
|
# Consider only actual functions for hooks (#3775).
|
||||||
if not inspect.isroutine(method):
|
if not inspect.isroutine(method):
|
||||||
return
|
return None
|
||||||
# Collect unmarked hooks as long as they have the `pytest_' prefix.
|
# 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")
|
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_hookspec_opts(self, module_or_class, name: str):
|
def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
|
||||||
|
""":meta private:"""
|
||||||
opts = super().parse_hookspec_opts(module_or_class, name)
|
opts = super().parse_hookspec_opts(module_or_class, name)
|
||||||
if opts is None:
|
if opts is None:
|
||||||
method = getattr(module_or_class, name)
|
method = getattr(module_or_class, name)
|
||||||
if name.startswith("pytest_"):
|
if name.startswith("pytest_"):
|
||||||
opts = _get_legacy_hook_marks(
|
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
||||||
method,
|
method,
|
||||||
"spec",
|
"spec",
|
||||||
("firstresult", "historic"),
|
("firstresult", "historic"),
|
||||||
|
@ -558,12 +568,8 @@ class PytestPluginManager(PluginManager):
|
||||||
anchor = absolutepath(current / path)
|
anchor = absolutepath(current / path)
|
||||||
|
|
||||||
# Ensure we do not break if what appears to be an anchor
|
# Ensure we do not break if what appears to be an anchor
|
||||||
# is in fact a very long option (#10169).
|
# is in fact a very long option (#10169, #11394).
|
||||||
try:
|
if safe_exists(anchor):
|
||||||
anchor_exists = anchor.exists()
|
|
||||||
except OSError: # pragma: no cover
|
|
||||||
anchor_exists = False
|
|
||||||
if anchor_exists:
|
|
||||||
self._try_load_conftest(anchor, importmode, rootpath)
|
self._try_load_conftest(anchor, importmode, rootpath)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
|
@ -953,7 +959,8 @@ class Config:
|
||||||
#: Command line arguments.
|
#: Command line arguments.
|
||||||
ARGS = enum.auto()
|
ARGS = enum.auto()
|
||||||
#: Invocation directory.
|
#: Invocation directory.
|
||||||
INCOVATION_DIR = enum.auto()
|
INVOCATION_DIR = enum.auto()
|
||||||
|
INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
|
||||||
#: 'testpaths' configuration value.
|
#: 'testpaths' configuration value.
|
||||||
TESTPATHS = enum.auto()
|
TESTPATHS = enum.auto()
|
||||||
|
|
||||||
|
@ -1003,10 +1010,8 @@ class Config:
|
||||||
# Deprecated alias. Was never public. Can be removed in a few releases.
|
# Deprecated alias. Was never public. Can be removed in a few releases.
|
||||||
self._store = self.stash
|
self._store = self.stash
|
||||||
|
|
||||||
from .compat import PathAwareHookProxy
|
|
||||||
|
|
||||||
self.trace = self.pluginmanager.trace.root.get("config")
|
self.trace = self.pluginmanager.trace.root.get("config")
|
||||||
self.hook = PathAwareHookProxy(self.pluginmanager.hook)
|
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
|
||||||
self._inicache: Dict[str, Any] = {}
|
self._inicache: Dict[str, Any] = {}
|
||||||
self._override_ini: Sequence[str] = ()
|
self._override_ini: Sequence[str] = ()
|
||||||
self._opt2dest: Dict[str, str] = {}
|
self._opt2dest: Dict[str, str] = {}
|
||||||
|
@ -1066,9 +1071,10 @@ class Config:
|
||||||
fin()
|
fin()
|
||||||
|
|
||||||
def get_terminal_writer(self) -> TerminalWriter:
|
def get_terminal_writer(self) -> TerminalWriter:
|
||||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
|
||||||
"terminalreporter"
|
"terminalreporter"
|
||||||
)
|
)
|
||||||
|
assert terminalreporter is not None
|
||||||
return terminalreporter._tw
|
return terminalreporter._tw
|
||||||
|
|
||||||
def pytest_cmdline_parse(
|
def pytest_cmdline_parse(
|
||||||
|
@ -1278,7 +1284,7 @@ class Config:
|
||||||
else:
|
else:
|
||||||
result = []
|
result = []
|
||||||
if not result:
|
if not result:
|
||||||
source = Config.ArgsSource.INCOVATION_DIR
|
source = Config.ArgsSource.INVOCATION_DIR
|
||||||
result = [str(invocation_dir)]
|
result = [str(invocation_dir)]
|
||||||
return result, source
|
return result, source
|
||||||
|
|
||||||
|
@ -1492,6 +1498,27 @@ class Config:
|
||||||
def getini(self, name: str):
|
def getini(self, name: str):
|
||||||
"""Return configuration value from an :ref:`ini file <configfiles>`.
|
"""Return configuration value from an :ref:`ini file <configfiles>`.
|
||||||
|
|
||||||
|
If a configuration value is not defined in an
|
||||||
|
:ref:`ini file <configfiles>`, then the ``default`` value provided while
|
||||||
|
registering the configuration through
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>` will be returned.
|
||||||
|
Please note that you can even provide ``None`` as a valid
|
||||||
|
default value.
|
||||||
|
|
||||||
|
If ``default`` is not provided while registering using
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>`, then a default value
|
||||||
|
based on the ``type`` parameter passed to
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>` will be returned.
|
||||||
|
The default values based on ``type`` are:
|
||||||
|
``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
|
||||||
|
``bool`` : ``False``
|
||||||
|
``string`` : empty string ``""``
|
||||||
|
|
||||||
|
If neither the ``default`` nor the ``type`` parameter is passed
|
||||||
|
while registering the configuration through
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>`, then the configuration
|
||||||
|
is treated as a string and a default empty string '' is returned.
|
||||||
|
|
||||||
If the specified name hasn't been registered through a prior
|
If the specified name hasn't been registered through a prior
|
||||||
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
|
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
|
||||||
plugin), a ValueError is raised.
|
plugin), a ValueError is raised.
|
||||||
|
@ -1518,11 +1545,7 @@ class Config:
|
||||||
try:
|
try:
|
||||||
value = self.inicfg[name]
|
value = self.inicfg[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if default is not None:
|
|
||||||
return default
|
return default
|
||||||
if type is None:
|
|
||||||
return ""
|
|
||||||
return []
|
|
||||||
else:
|
else:
|
||||||
value = override_value
|
value = override_value
|
||||||
# Coerce the values based on types.
|
# Coerce the values based on types.
|
||||||
|
@ -1630,6 +1653,78 @@ class Config:
|
||||||
"""Deprecated, use getoption(skip=True) instead."""
|
"""Deprecated, use getoption(skip=True) instead."""
|
||||||
return self.getoption(name, skip=True)
|
return self.getoption(name, skip=True)
|
||||||
|
|
||||||
|
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
|
||||||
|
VERBOSITY_ASSERTIONS: Final = "assertions"
|
||||||
|
_VERBOSITY_INI_DEFAULT: Final = "auto"
|
||||||
|
|
||||||
|
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||||
|
r"""Retrieve the verbosity level for a fine-grained verbosity type.
|
||||||
|
|
||||||
|
:param verbosity_type: Verbosity type to get level for. If a level is
|
||||||
|
configured for the given type, that value will be returned. If the
|
||||||
|
given type is not a known verbosity type, the global verbosity
|
||||||
|
level will be returned. If the given type is None (default), the
|
||||||
|
global verbosity level will be returned.
|
||||||
|
|
||||||
|
To configure a level for a fine-grained verbosity type, the
|
||||||
|
configuration file should have a setting for the configuration name
|
||||||
|
and a numeric value for the verbosity level. A special value of "auto"
|
||||||
|
can be used to explicitly use the global verbosity level.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
# content of pytest.ini
|
||||||
|
[pytest]
|
||||||
|
verbosity_assertions = 2
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest -v
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(config.get_verbosity()) # 1
|
||||||
|
print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
|
||||||
|
"""
|
||||||
|
global_level = self.option.verbose
|
||||||
|
assert isinstance(global_level, int)
|
||||||
|
if verbosity_type is None:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
ini_name = Config._verbosity_ini_name(verbosity_type)
|
||||||
|
if ini_name not in self._parser._inidict:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
level = self.getini(ini_name)
|
||||||
|
if level == Config._VERBOSITY_INI_DEFAULT:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
return int(level)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _verbosity_ini_name(verbosity_type: str) -> str:
|
||||||
|
return f"verbosity_{verbosity_type}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_verbosity_ini(parser: "Parser", verbosity_type: str, help: str) -> None:
|
||||||
|
"""Add a output verbosity configuration option for the given output type.
|
||||||
|
|
||||||
|
:param parser: Parser for command line arguments and ini-file values.
|
||||||
|
:param verbosity_type: Fine-grained verbosity category.
|
||||||
|
:param help: Description of the output this type controls.
|
||||||
|
|
||||||
|
The value should be retrieved via a call to
|
||||||
|
:py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
|
||||||
|
"""
|
||||||
|
parser.addini(
|
||||||
|
Config._verbosity_ini_name(verbosity_type),
|
||||||
|
help=help,
|
||||||
|
type="string",
|
||||||
|
default=Config._VERBOSITY_INI_DEFAULT,
|
||||||
|
)
|
||||||
|
|
||||||
def _warn_about_missing_assertion(self, mode: str) -> None:
|
def _warn_about_missing_assertion(self, mode: str) -> None:
|
||||||
if not _assertion_supported():
|
if not _assertion_supported():
|
||||||
if mode == "plain":
|
if mode == "plain":
|
||||||
|
|
|
@ -27,6 +27,14 @@ from _pytest.deprecated import check_ispytest
|
||||||
FILE_OR_DIR = "file_or_dir"
|
FILE_OR_DIR = "file_or_dir"
|
||||||
|
|
||||||
|
|
||||||
|
class NotSet:
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "<notset>"
|
||||||
|
|
||||||
|
|
||||||
|
NOT_SET = NotSet()
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class Parser:
|
class Parser:
|
||||||
"""Parser for command line arguments and ini-file values.
|
"""Parser for command line arguments and ini-file values.
|
||||||
|
@ -90,7 +98,7 @@ class Parser:
|
||||||
:param opts:
|
:param opts:
|
||||||
Option names, can be short or long options.
|
Option names, can be short or long options.
|
||||||
:param attrs:
|
:param attrs:
|
||||||
Same attributes as the argparse library's :py:func:`add_argument()
|
Same attributes as the argparse library's :meth:`add_argument()
|
||||||
<argparse.ArgumentParser.add_argument>` function accepts.
|
<argparse.ArgumentParser.add_argument>` function accepts.
|
||||||
|
|
||||||
After command line parsing, options are available on the pytest config
|
After command line parsing, options are available on the pytest config
|
||||||
|
@ -176,7 +184,7 @@ class Parser:
|
||||||
type: Optional[
|
type: Optional[
|
||||||
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
||||||
] = None,
|
] = None,
|
||||||
default: Any = None,
|
default: Any = NOT_SET,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register an ini-file option.
|
"""Register an ini-file option.
|
||||||
|
|
||||||
|
@ -203,10 +211,30 @@ class Parser:
|
||||||
:py:func:`config.getini(name) <pytest.Config.getini>`.
|
:py:func:`config.getini(name) <pytest.Config.getini>`.
|
||||||
"""
|
"""
|
||||||
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
|
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
|
||||||
|
if default is NOT_SET:
|
||||||
|
default = get_ini_default_for_type(type)
|
||||||
|
|
||||||
self._inidict[name] = (help, type, default)
|
self._inidict[name] = (help, type, default)
|
||||||
self._ininames.append(name)
|
self._ininames.append(name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ini_default_for_type(
|
||||||
|
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Used by addini to get the default value for a given ini-option type, when
|
||||||
|
default is not supplied.
|
||||||
|
"""
|
||||||
|
if type is None:
|
||||||
|
return ""
|
||||||
|
elif type in ("paths", "pathlist", "args", "linelist"):
|
||||||
|
return []
|
||||||
|
elif type == "bool":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class ArgumentError(Exception):
|
class ArgumentError(Exception):
|
||||||
"""Raised if an Argument instance is created with invalid or
|
"""Raised if an Argument instance is created with invalid or
|
||||||
inconsistent arguments."""
|
inconsistent arguments."""
|
||||||
|
@ -372,7 +400,7 @@ class OptionGroup:
|
||||||
:param opts:
|
:param opts:
|
||||||
Option names, can be short or long options.
|
Option names, can be short or long options.
|
||||||
:param attrs:
|
:param attrs:
|
||||||
Same attributes as the argparse library's :py:func:`add_argument()
|
Same attributes as the argparse library's :meth:`add_argument()
|
||||||
<argparse.ArgumentParser.add_argument>` function accepts.
|
<argparse.ArgumentParser.add_argument>` function accepts.
|
||||||
"""
|
"""
|
||||||
conflict = set(opts).intersection(
|
conflict = set(opts).intersection(
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Mapping
|
||||||
|
|
||||||
|
import pluggy
|
||||||
|
|
||||||
from ..compat import LEGACY_PATH
|
from ..compat import LEGACY_PATH
|
||||||
from ..compat import legacy_path
|
from ..compat import legacy_path
|
||||||
from ..deprecated import HOOK_LEGACY_PATH_ARG
|
from ..deprecated import HOOK_LEGACY_PATH_ARG
|
||||||
from _pytest.nodes import _check_path
|
|
||||||
|
|
||||||
# hookname: (Path, LEGACY_PATH)
|
# hookname: (Path, LEGACY_PATH)
|
||||||
imply_paths_hooks = {
|
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
|
||||||
"pytest_ignore_collect": ("collection_path", "path"),
|
"pytest_ignore_collect": ("collection_path", "path"),
|
||||||
"pytest_collect_file": ("file_path", "path"),
|
"pytest_collect_file": ("file_path", "path"),
|
||||||
"pytest_pycollect_makemodule": ("module_path", "path"),
|
"pytest_pycollect_makemodule": ("module_path", "path"),
|
||||||
|
@ -18,6 +21,14 @@ imply_paths_hooks = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
|
||||||
|
if Path(fspath) != path:
|
||||||
|
raise ValueError(
|
||||||
|
f"Path({fspath!r}) != {path!r}\n"
|
||||||
|
"if both path and fspath are given they need to be equal"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PathAwareHookProxy:
|
class PathAwareHookProxy:
|
||||||
"""
|
"""
|
||||||
this helper wraps around hook callers
|
this helper wraps around hook callers
|
||||||
|
@ -27,24 +38,24 @@ class PathAwareHookProxy:
|
||||||
this may have to be changed later depending on bugs
|
this may have to be changed later depending on bugs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hook_caller):
|
def __init__(self, hook_relay: pluggy.HookRelay) -> None:
|
||||||
self.__hook_caller = hook_caller
|
self._hook_relay = hook_relay
|
||||||
|
|
||||||
def __dir__(self):
|
def __dir__(self) -> list[str]:
|
||||||
return dir(self.__hook_caller)
|
return dir(self._hook_relay)
|
||||||
|
|
||||||
def __getattr__(self, key, _wraps=functools.wraps):
|
def __getattr__(self, key: str) -> pluggy.HookCaller:
|
||||||
hook = getattr(self.__hook_caller, key)
|
hook: pluggy.HookCaller = getattr(self._hook_relay, key)
|
||||||
if key not in imply_paths_hooks:
|
if key not in imply_paths_hooks:
|
||||||
self.__dict__[key] = hook
|
self.__dict__[key] = hook
|
||||||
return hook
|
return hook
|
||||||
else:
|
else:
|
||||||
path_var, fspath_var = imply_paths_hooks[key]
|
path_var, fspath_var = imply_paths_hooks[key]
|
||||||
|
|
||||||
@_wraps(hook)
|
@functools.wraps(hook)
|
||||||
def fixed_hook(**kw):
|
def fixed_hook(**kw):
|
||||||
path_value: Optional[Path] = kw.pop(path_var, None)
|
path_value: Path | None = kw.pop(path_var, None)
|
||||||
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
|
fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
|
||||||
if fspath_value is not None:
|
if fspath_value is not None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
HOOK_LEGACY_PATH_ARG.format(
|
HOOK_LEGACY_PATH_ARG.format(
|
||||||
|
@ -65,6 +76,8 @@ class PathAwareHookProxy:
|
||||||
kw[fspath_var] = fspath_value
|
kw[fspath_var] = fspath_value
|
||||||
return hook(**kw)
|
return hook(**kw)
|
||||||
|
|
||||||
|
fixed_hook.name = hook.name # type: ignore[attr-defined]
|
||||||
|
fixed_hook.spec = hook.spec # type: ignore[attr-defined]
|
||||||
fixed_hook.__name__ = key
|
fixed_hook.__name__ = key
|
||||||
self.__dict__[key] = fixed_hook
|
self.__dict__[key] = fixed_hook
|
||||||
return fixed_hook
|
return fixed_hook # type: ignore[return-value]
|
||||||
|
|
|
@ -15,6 +15,7 @@ from .exceptions import UsageError
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import commonpath
|
from _pytest.pathlib import commonpath
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
|
|
||||||
|
|
||||||
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
|
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
|
||||||
|
@ -147,14 +148,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
|
||||||
return path
|
return path
|
||||||
return path.parent
|
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
|
# These look like paths but may not exist
|
||||||
possible_paths = (
|
possible_paths = (
|
||||||
absolutepath(get_file_part_from_node_id(arg))
|
absolutepath(get_file_part_from_node_id(arg))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Discover and run doctests in modules and test files."""
|
"""Discover and run doctests in modules and test files."""
|
||||||
import bdb
|
import bdb
|
||||||
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
@ -32,7 +33,7 @@ from _pytest.compat import safe_getattr
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.fixtures import fixture
|
from _pytest.fixtures import fixture
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import TopRequest
|
||||||
from _pytest.nodes import Collector
|
from _pytest.nodes import Collector
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
from _pytest.outcomes import OutcomeException
|
from _pytest.outcomes import OutcomeException
|
||||||
|
@ -254,14 +255,20 @@ class DoctestItem(Item):
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||||
runner: Optional["doctest.DocTestRunner"] = None,
|
runner: "doctest.DocTestRunner",
|
||||||
dtest: Optional["doctest.DocTest"] = None,
|
dtest: "doctest.DocTest",
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name, parent)
|
super().__init__(name, parent)
|
||||||
self.runner = runner
|
self.runner = runner
|
||||||
self.dtest = dtest
|
self.dtest = dtest
|
||||||
|
|
||||||
|
# Stuff needed for fixture support.
|
||||||
self.obj = None
|
self.obj = None
|
||||||
self.fixture_request: Optional[FixtureRequest] = None
|
fm = self.session._fixturemanager
|
||||||
|
fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None)
|
||||||
|
self._fixtureinfo = fixtureinfo
|
||||||
|
self.fixturenames = fixtureinfo.names_closure
|
||||||
|
self._initrequest()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent( # type: ignore
|
def from_parent( # type: ignore
|
||||||
|
@ -276,19 +283,18 @@ class DoctestItem(Item):
|
||||||
"""The public named constructor."""
|
"""The public named constructor."""
|
||||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||||
|
|
||||||
|
def _initrequest(self) -> None:
|
||||||
|
self.funcargs: Dict[str, object] = {}
|
||||||
|
self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
if self.dtest is not None:
|
self._request._fillfixtures()
|
||||||
self.fixture_request = _setup_fixtures(self)
|
globs = dict(getfixture=self._request.getfixturevalue)
|
||||||
globs = dict(getfixture=self.fixture_request.getfixturevalue)
|
for name, value in self._request.getfixturevalue("doctest_namespace").items():
|
||||||
for name, value in self.fixture_request.getfixturevalue(
|
|
||||||
"doctest_namespace"
|
|
||||||
).items():
|
|
||||||
globs[name] = value
|
globs[name] = value
|
||||||
self.dtest.globs.update(globs)
|
self.dtest.globs.update(globs)
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
assert self.dtest is not None
|
|
||||||
assert self.runner is not None
|
|
||||||
_check_all_skipped(self.dtest)
|
_check_all_skipped(self.dtest)
|
||||||
self._disable_output_capturing_for_darwin()
|
self._disable_output_capturing_for_darwin()
|
||||||
failures: List["doctest.DocTestFailure"] = []
|
failures: List["doctest.DocTestFailure"] = []
|
||||||
|
@ -375,7 +381,6 @@ class DoctestItem(Item):
|
||||||
return ReprFailDoctest(reprlocation_lines)
|
return ReprFailDoctest(reprlocation_lines)
|
||||||
|
|
||||||
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
||||||
assert self.dtest is not None
|
|
||||||
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
|
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
|
||||||
|
|
||||||
|
|
||||||
|
@ -395,8 +400,8 @@ def _get_flag_lookup() -> Dict[str, int]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_optionflags(parent):
|
def get_optionflags(config: Config) -> int:
|
||||||
optionflags_str = parent.config.getini("doctest_optionflags")
|
optionflags_str = config.getini("doctest_optionflags")
|
||||||
flag_lookup_table = _get_flag_lookup()
|
flag_lookup_table = _get_flag_lookup()
|
||||||
flag_acc = 0
|
flag_acc = 0
|
||||||
for flag in optionflags_str:
|
for flag in optionflags_str:
|
||||||
|
@ -404,8 +409,8 @@ def get_optionflags(parent):
|
||||||
return flag_acc
|
return flag_acc
|
||||||
|
|
||||||
|
|
||||||
def _get_continue_on_failure(config):
|
def _get_continue_on_failure(config: Config) -> bool:
|
||||||
continue_on_failure = config.getvalue("doctest_continue_on_failure")
|
continue_on_failure: bool = config.getvalue("doctest_continue_on_failure")
|
||||||
if continue_on_failure:
|
if continue_on_failure:
|
||||||
# We need to turn off this if we use pdb since we should stop at
|
# We need to turn off this if we use pdb since we should stop at
|
||||||
# the first failure.
|
# the first failure.
|
||||||
|
@ -428,7 +433,7 @@ class DoctestTextfile(Module):
|
||||||
name = self.path.name
|
name = self.path.name
|
||||||
globs = {"__name__": "__main__"}
|
globs = {"__name__": "__main__"}
|
||||||
|
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self.config)
|
||||||
|
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=False,
|
verbose=False,
|
||||||
|
@ -536,6 +541,23 @@ class DoctestModule(Module):
|
||||||
tests, obj, name, module, source_lines, globs, seen
|
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 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":
|
if self.path.name == "conftest.py":
|
||||||
module = self.config.pluginmanager._importconftest(
|
module = self.config.pluginmanager._importconftest(
|
||||||
self.path,
|
self.path,
|
||||||
|
@ -556,7 +578,7 @@ class DoctestModule(Module):
|
||||||
raise
|
raise
|
||||||
# Uses internal doctest module parsing mechanism.
|
# Uses internal doctest module parsing mechanism.
|
||||||
finder = MockAwareDocTestFinder()
|
finder = MockAwareDocTestFinder()
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self.config)
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=False,
|
verbose=False,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
|
@ -571,22 +593,6 @@ class DoctestModule(Module):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
|
|
||||||
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
|
|
||||||
|
|
||||||
def func() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
doctest_item.funcargs = {} # type: ignore[attr-defined]
|
|
||||||
fm = doctest_item.session._fixturemanager
|
|
||||||
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
|
||||||
node=doctest_item, func=func, cls=None, funcargs=False
|
|
||||||
)
|
|
||||||
fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
|
|
||||||
fixture_request._fillfixtures()
|
|
||||||
return fixture_request
|
|
||||||
|
|
||||||
|
|
||||||
def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
||||||
import doctest
|
import doctest
|
||||||
import re
|
import re
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
@ -10,8 +9,8 @@ from _pytest.nodes import Item
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
|
||||||
|
fault_handler_original_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_stderr_fd_key = StashKey[int]()
|
fault_handler_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_originally_enabled_key = StashKey[bool]()
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
|
@ -25,8 +24,15 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
# at teardown we want to restore the original faulthandler fileno
|
||||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
# but faulthandler has no api to return the original fileno
|
||||||
|
# so here we stash the stderr fileno to be used at teardown
|
||||||
|
# sys.stderr and sys.__stderr__ may be closed or patched during the session
|
||||||
|
# so we can't rely on their values being good at that point (#11572).
|
||||||
|
stderr_fileno = get_stderr_fileno()
|
||||||
|
if faulthandler.is_enabled():
|
||||||
|
config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
|
||||||
|
config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
|
||||||
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,9 +44,10 @@ def pytest_unconfigure(config: Config) -> None:
|
||||||
if fault_handler_stderr_fd_key in config.stash:
|
if fault_handler_stderr_fd_key in config.stash:
|
||||||
os.close(config.stash[fault_handler_stderr_fd_key])
|
os.close(config.stash[fault_handler_stderr_fd_key])
|
||||||
del 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.
|
# Re-enable the faulthandler if it was originally enabled.
|
||||||
faulthandler.enable(file=get_stderr_fileno())
|
if fault_handler_original_stderr_fd_key in config.stash:
|
||||||
|
faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
|
||||||
|
del config.stash[fault_handler_original_stderr_fd_key]
|
||||||
|
|
||||||
|
|
||||||
def get_stderr_fileno() -> int:
|
def get_stderr_fileno() -> int:
|
||||||
|
@ -51,7 +58,7 @@ def get_stderr_fileno() -> int:
|
||||||
if fileno == -1:
|
if fileno == -1:
|
||||||
raise AttributeError()
|
raise AttributeError()
|
||||||
return fileno
|
return fileno
|
||||||
except (AttributeError, io.UnsupportedOperation):
|
except (AttributeError, ValueError):
|
||||||
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
# 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
|
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||||
# This is potentially dangerous, but the best we can do.
|
# This is potentially dangerous, but the best we can do.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import abc
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -7,6 +8,7 @@ from collections import defaultdict
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import AbstractSet
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
@ -133,7 +135,9 @@ def get_scope_node(
|
||||||
import _pytest.python
|
import _pytest.python
|
||||||
|
|
||||||
if scope is Scope.Function:
|
if scope is Scope.Function:
|
||||||
return node.getparent(nodes.Item)
|
# Type ignored because this is actually safe, see:
|
||||||
|
# https://github.com/python/mypy/issues/4717
|
||||||
|
return node.getparent(nodes.Item) # type: ignore[type-abstract]
|
||||||
elif scope is Scope.Class:
|
elif scope is Scope.Class:
|
||||||
return node.getparent(_pytest.python.Class)
|
return node.getparent(_pytest.python.Class)
|
||||||
elif scope is Scope.Module:
|
elif scope is Scope.Module:
|
||||||
|
@ -209,16 +213,14 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
||||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
||||||
for scope in HIGH_SCOPES:
|
for scope in HIGH_SCOPES:
|
||||||
d: Dict[nodes.Item, Dict[FixtureArgKey, None]] = {}
|
scoped_argkeys_cache = argkeys_cache[scope] = {}
|
||||||
argkeys_cache[scope] = d
|
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
|
||||||
item_d: Dict[FixtureArgKey, Deque[nodes.Item]] = defaultdict(deque)
|
|
||||||
items_by_argkey[scope] = item_d
|
|
||||||
for item in items:
|
for item in items:
|
||||||
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
||||||
if keys:
|
if keys:
|
||||||
d[item] = keys
|
scoped_argkeys_cache[item] = keys
|
||||||
for key in keys:
|
for key in keys:
|
||||||
item_d[key].append(item)
|
scoped_items_by_argkey[key].append(item)
|
||||||
items_dict = dict.fromkeys(items, None)
|
items_dict = dict.fromkeys(items, None)
|
||||||
return list(
|
return list(
|
||||||
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
||||||
|
@ -340,26 +342,32 @@ class FuncFixtureInfo:
|
||||||
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
|
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
|
||||||
|
|
||||||
|
|
||||||
class FixtureRequest:
|
class FixtureRequest(abc.ABC):
|
||||||
"""A request for a fixture from a test or fixture function.
|
"""The type of the ``request`` fixture.
|
||||||
|
|
||||||
A request object gives access to the requesting test context and has
|
A request object gives access to the requesting test context and has a
|
||||||
an optional ``param`` attribute in case the fixture is parametrized
|
``param`` attribute in case the fixture is parametrized.
|
||||||
indirectly.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
pyfuncitem: "Function",
|
||||||
|
fixturename: Optional[str],
|
||||||
|
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
|
||||||
|
arg2index: Dict[str, int],
|
||||||
|
fixture_defs: Dict[str, "FixtureDef[Any]"],
|
||||||
|
*,
|
||||||
|
_ispytest: bool = False,
|
||||||
|
) -> None:
|
||||||
check_ispytest(_ispytest)
|
check_ispytest(_ispytest)
|
||||||
#: Fixture for which this request is being performed.
|
#: Fixture for which this request is being performed.
|
||||||
self.fixturename: Optional[str] = None
|
self.fixturename: Final = fixturename
|
||||||
self._pyfuncitem = pyfuncitem
|
self._pyfuncitem: Final = pyfuncitem
|
||||||
self._fixturemanager = pyfuncitem.session._fixturemanager
|
|
||||||
self._scope = Scope.Function
|
|
||||||
# The FixtureDefs for each fixture name requested by this item.
|
# The FixtureDefs for each fixture name requested by this item.
|
||||||
# Starts from the statically-known fixturedefs resolved during
|
# Starts from the statically-known fixturedefs resolved during
|
||||||
# collection. Dynamically requested fixtures (using
|
# collection. Dynamically requested fixtures (using
|
||||||
# `request.getfixturevalue("foo")`) are added dynamically.
|
# `request.getfixturevalue("foo")`) are added dynamically.
|
||||||
self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy()
|
self._arg2fixturedefs: Final = arg2fixturedefs
|
||||||
# A fixture may override another fixture with the same name, e.g. a fixture
|
# A fixture may override another fixture with the same name, e.g. a fixture
|
||||||
# in a module can override a fixture in a conftest, a fixture in a class can
|
# in a module can override a fixture in a conftest, a fixture in a class can
|
||||||
# override a fixture in the module, and so on.
|
# override a fixture in the module, and so on.
|
||||||
|
@ -369,10 +377,10 @@ class FixtureRequest:
|
||||||
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
|
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
|
||||||
# furthest to closest, so we use negative indexing -1, -2, ... to go from
|
# furthest to closest, so we use negative indexing -1, -2, ... to go from
|
||||||
# last to first.
|
# last to first.
|
||||||
self._arg2index: Dict[str, int] = {}
|
self._arg2index: Final = arg2index
|
||||||
# The evaluated argnames so far, mapping to the FixtureDef they resolved
|
# The evaluated argnames so far, mapping to the FixtureDef they resolved
|
||||||
# to.
|
# to.
|
||||||
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
|
self._fixture_defs: Final = fixture_defs
|
||||||
# Notes on the type of `param`:
|
# Notes on the type of `param`:
|
||||||
# -`request.param` is only defined in parametrized fixtures, and will raise
|
# -`request.param` is only defined in parametrized fixtures, and will raise
|
||||||
# AttributeError otherwise. Python typing has no notion of "undefined", so
|
# AttributeError otherwise. Python typing has no notion of "undefined", so
|
||||||
|
@ -383,6 +391,15 @@ class FixtureRequest:
|
||||||
# for now just using Any.
|
# for now just using Any.
|
||||||
self.param: Any
|
self.param: Any
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _fixturemanager(self) -> "FixtureManager":
|
||||||
|
return self._pyfuncitem.session._fixturemanager
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _scope(self) -> Scope:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scope(self) -> _ScopeName:
|
def scope(self) -> _ScopeName:
|
||||||
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
||||||
|
@ -391,30 +408,15 @@ class FixtureRequest:
|
||||||
@property
|
@property
|
||||||
def fixturenames(self) -> List[str]:
|
def fixturenames(self) -> List[str]:
|
||||||
"""Names of all active fixtures in this request."""
|
"""Names of all active fixtures in this request."""
|
||||||
result = list(self._pyfuncitem._fixtureinfo.names_closure)
|
result = list(self._pyfuncitem.fixturenames)
|
||||||
result.extend(set(self._fixture_defs).difference(result))
|
result.extend(set(self._fixture_defs).difference(result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
def node(self):
|
def node(self):
|
||||||
"""Underlying collection node (depends on current request scope)."""
|
"""Underlying collection node (depends on current request scope)."""
|
||||||
scope = self._scope
|
raise NotImplementedError()
|
||||||
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 SubRequest (a subclass).
|
|
||||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
|
||||||
else:
|
|
||||||
node = get_scope_node(self._pyfuncitem, scope)
|
|
||||||
if node is None and scope is Scope.Class:
|
|
||||||
# Fallback to function item itself.
|
|
||||||
node = self._pyfuncitem
|
|
||||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
|
||||||
scope, self._pyfuncitem
|
|
||||||
)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
||||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||||
|
@ -500,11 +502,11 @@ class FixtureRequest:
|
||||||
"""Pytest session object."""
|
"""Pytest session object."""
|
||||||
return self._pyfuncitem.session # type: ignore[no-any-return]
|
return self._pyfuncitem.session # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||||
"""Add finalizer/teardown function to be called without arguments after
|
"""Add finalizer/teardown function to be called without arguments after
|
||||||
the last test within the requesting test context finished execution."""
|
the last test within the requesting test context finished execution."""
|
||||||
# XXX usually this method is shadowed by fixturedef specific ones.
|
raise NotImplementedError()
|
||||||
self.node.addfinalizer(finalizer)
|
|
||||||
|
|
||||||
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
||||||
"""Apply a marker to a single test function invocation.
|
"""Apply a marker to a single test function invocation.
|
||||||
|
@ -525,13 +527,6 @@ class FixtureRequest:
|
||||||
"""
|
"""
|
||||||
raise self._fixturemanager.FixtureLookupError(None, self, msg)
|
raise self._fixturemanager.FixtureLookupError(None, self, msg)
|
||||||
|
|
||||||
def _fillfixtures(self) -> None:
|
|
||||||
item = self._pyfuncitem
|
|
||||||
fixturenames = getattr(item, "fixturenames", self.fixturenames)
|
|
||||||
for argname in fixturenames:
|
|
||||||
if argname not in item.funcargs:
|
|
||||||
item.funcargs[argname] = self.getfixturevalue(argname)
|
|
||||||
|
|
||||||
def getfixturevalue(self, argname: str) -> Any:
|
def getfixturevalue(self, argname: str) -> Any:
|
||||||
"""Dynamically run a named fixture function.
|
"""Dynamically run a named fixture function.
|
||||||
|
|
||||||
|
@ -665,6 +660,97 @@ class FixtureRequest:
|
||||||
finalizer = functools.partial(fixturedef.finish, request=subrequest)
|
finalizer = functools.partial(fixturedef.finish, request=subrequest)
|
||||||
subrequest.node.addfinalizer(finalizer)
|
subrequest.node.addfinalizer(finalizer)
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
class TopRequest(FixtureRequest):
|
||||||
|
"""The type of the ``request`` fixture in a test function."""
|
||||||
|
|
||||||
|
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
|
||||||
|
super().__init__(
|
||||||
|
fixturename=None,
|
||||||
|
pyfuncitem=pyfuncitem,
|
||||||
|
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
|
||||||
|
arg2index={},
|
||||||
|
fixture_defs={},
|
||||||
|
_ispytest=_ispytest,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _scope(self) -> Scope:
|
||||||
|
return Scope.Function
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node(self):
|
||||||
|
return self._pyfuncitem
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "<FixtureRequest for %r>" % (self.node)
|
||||||
|
|
||||||
|
def _fillfixtures(self) -> None:
|
||||||
|
item = self._pyfuncitem
|
||||||
|
for argname in item.fixturenames:
|
||||||
|
if argname not in item.funcargs:
|
||||||
|
item.funcargs[argname] = self.getfixturevalue(argname)
|
||||||
|
|
||||||
|
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||||
|
self.node.addfinalizer(finalizer)
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
class SubRequest(FixtureRequest):
|
||||||
|
"""The type of the ``request`` fixture in a fixture function requested
|
||||||
|
(transitively) by a test function."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
request: FixtureRequest,
|
||||||
|
scope: Scope,
|
||||||
|
param: Any,
|
||||||
|
param_index: int,
|
||||||
|
fixturedef: "FixtureDef[object]",
|
||||||
|
*,
|
||||||
|
_ispytest: bool = False,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
pyfuncitem=request._pyfuncitem,
|
||||||
|
fixturename=fixturedef.argname,
|
||||||
|
fixture_defs=request._fixture_defs,
|
||||||
|
arg2fixturedefs=request._arg2fixturedefs,
|
||||||
|
arg2index=request._arg2index,
|
||||||
|
_ispytest=_ispytest,
|
||||||
|
)
|
||||||
|
self._parent_request: Final[FixtureRequest] = request
|
||||||
|
self._scope_field: Final = scope
|
||||||
|
self._fixturedef: Final = fixturedef
|
||||||
|
if param is not NOTSET:
|
||||||
|
self.param = param
|
||||||
|
self.param_index: Final = param_index
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _scope(self) -> Scope:
|
||||||
|
return self._scope_field
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node(self):
|
||||||
|
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:
|
||||||
|
node = get_scope_package(self._pyfuncitem, self._fixturedef)
|
||||||
|
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 _check_scope(
|
def _check_scope(
|
||||||
self,
|
self,
|
||||||
argname: str,
|
argname: str,
|
||||||
|
@ -699,44 +785,7 @@ class FixtureRequest:
|
||||||
)
|
)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return "<FixtureRequest for %r>" % (self.node)
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
class SubRequest(FixtureRequest):
|
|
||||||
"""A sub request for handling getting a fixture from a test function/fixture."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
request: "FixtureRequest",
|
|
||||||
scope: Scope,
|
|
||||||
param: Any,
|
|
||||||
param_index: int,
|
|
||||||
fixturedef: "FixtureDef[object]",
|
|
||||||
*,
|
|
||||||
_ispytest: bool = False,
|
|
||||||
) -> None:
|
|
||||||
check_ispytest(_ispytest)
|
|
||||||
self._parent_request = request
|
|
||||||
self.fixturename = fixturedef.argname
|
|
||||||
if param is not NOTSET:
|
|
||||||
self.param = param
|
|
||||||
self.param_index = param_index
|
|
||||||
self._scope = scope
|
|
||||||
self._fixturedef = fixturedef
|
|
||||||
self._pyfuncitem = request._pyfuncitem
|
|
||||||
self._fixture_defs = request._fixture_defs
|
|
||||||
self._arg2fixturedefs = request._arg2fixturedefs
|
|
||||||
self._arg2index = request._arg2index
|
|
||||||
self._fixturemanager = request._fixturemanager
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
|
|
||||||
|
|
||||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||||
"""Add finalizer/teardown function to be called without arguments after
|
|
||||||
the last test within the requesting test context finished execution."""
|
|
||||||
self._fixturedef.addfinalizer(finalizer)
|
self._fixturedef.addfinalizer(finalizer)
|
||||||
|
|
||||||
def _schedule_finalizers(
|
def _schedule_finalizers(
|
||||||
|
@ -745,7 +794,10 @@ class SubRequest(FixtureRequest):
|
||||||
# If the executing fixturedef was not explicitly requested in the argument list (via
|
# If the executing fixturedef was not explicitly requested in the argument list (via
|
||||||
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
|
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
|
||||||
# first.
|
# first.
|
||||||
if fixturedef.argname not in self.fixturenames:
|
if (
|
||||||
|
fixturedef.argname not in self._fixture_defs
|
||||||
|
and fixturedef.argname not in self._pyfuncitem.fixturenames
|
||||||
|
):
|
||||||
fixturedef.addfinalizer(
|
fixturedef.addfinalizer(
|
||||||
functools.partial(self._fixturedef.finish, request=self)
|
functools.partial(self._fixturedef.finish, request=self)
|
||||||
)
|
)
|
||||||
|
@ -1333,7 +1385,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
|
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
|
||||||
"""Return all direct parametrization arguments of a node, so we don't
|
"""Return all direct parametrization arguments of a node, so we don't
|
||||||
mistake them for fixtures.
|
mistake them for fixtures.
|
||||||
|
|
||||||
|
@ -1342,17 +1394,22 @@ def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
|
||||||
These things are done later as well when dealing with parametrization
|
These things are done later as well when dealing with parametrization
|
||||||
so this could be improved.
|
so this could be improved.
|
||||||
"""
|
"""
|
||||||
parametrize_argnames: List[str] = []
|
parametrize_argnames: Set[str] = set()
|
||||||
for marker in node.iter_markers(name="parametrize"):
|
for marker in node.iter_markers(name="parametrize"):
|
||||||
if not marker.kwargs.get("indirect", False):
|
if not marker.kwargs.get("indirect", False):
|
||||||
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
||||||
*marker.args, **marker.kwargs
|
*marker.args, **marker.kwargs
|
||||||
)
|
)
|
||||||
parametrize_argnames.extend(p_argnames)
|
parametrize_argnames.update(p_argnames)
|
||||||
|
|
||||||
return parametrize_argnames
|
return parametrize_argnames
|
||||||
|
|
||||||
|
|
||||||
|
def deduplicate_names(*seqs: Iterable[str]) -> Tuple[str, ...]:
|
||||||
|
"""De-duplicate the sequence of names while keeping the original order."""
|
||||||
|
# Ideally we would use a set, but it does not preserve insertion order.
|
||||||
|
return tuple(dict.fromkeys(name for seq in seqs for name in seq))
|
||||||
|
|
||||||
|
|
||||||
class FixtureManager:
|
class FixtureManager:
|
||||||
"""pytest fixture definitions and information is stored and managed
|
"""pytest fixture definitions and information is stored and managed
|
||||||
from this class.
|
from this class.
|
||||||
|
@ -1405,13 +1462,12 @@ class FixtureManager:
|
||||||
def getfixtureinfo(
|
def getfixtureinfo(
|
||||||
self,
|
self,
|
||||||
node: nodes.Item,
|
node: nodes.Item,
|
||||||
func: Callable[..., object],
|
func: Optional[Callable[..., object]],
|
||||||
cls: Optional[type],
|
cls: Optional[type],
|
||||||
funcargs: bool = True,
|
|
||||||
) -> FuncFixtureInfo:
|
) -> FuncFixtureInfo:
|
||||||
"""Calculate the :class:`FuncFixtureInfo` for an item.
|
"""Calculate the :class:`FuncFixtureInfo` for an item.
|
||||||
|
|
||||||
If ``funcargs`` is false, or if the item sets an attribute
|
If ``func`` is None, or if the item sets an attribute
|
||||||
``nofuncargs = True``, then ``func`` is not examined at all.
|
``nofuncargs = True``, then ``func`` is not examined at all.
|
||||||
|
|
||||||
:param node:
|
:param node:
|
||||||
|
@ -1420,21 +1476,23 @@ class FixtureManager:
|
||||||
The item's function.
|
The item's function.
|
||||||
:param cls:
|
:param cls:
|
||||||
If the function is a method, the method's class.
|
If the function is a method, the method's class.
|
||||||
:param funcargs:
|
|
||||||
Whether to look into func's parameters as fixture requests.
|
|
||||||
"""
|
"""
|
||||||
if funcargs and not getattr(node, "nofuncargs", False):
|
if func is not None and not getattr(node, "nofuncargs", False):
|
||||||
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
||||||
else:
|
else:
|
||||||
argnames = ()
|
argnames = ()
|
||||||
|
usefixturesnames = self._getusefixturesnames(node)
|
||||||
|
autousenames = self._getautousenames(node.nodeid)
|
||||||
|
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
|
||||||
|
|
||||||
usefixtures = tuple(
|
direct_parametrize_args = _get_direct_parametrize_args(node)
|
||||||
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
|
|
||||||
)
|
names_closure, arg2fixturedefs = self.getfixtureclosure(
|
||||||
initialnames = usefixtures + argnames
|
parentnode=node,
|
||||||
initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure(
|
initialnames=initialnames,
|
||||||
initialnames, node, ignore_args=_get_direct_parametrize_args(node)
|
ignore_args=direct_parametrize_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||||
|
|
||||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
||||||
|
@ -1466,12 +1524,17 @@ class FixtureManager:
|
||||||
if basenames:
|
if basenames:
|
||||||
yield from basenames
|
yield from basenames
|
||||||
|
|
||||||
|
def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
|
||||||
|
"""Return the names of usefixtures fixtures applicable to node."""
|
||||||
|
for mark in node.iter_markers(name="usefixtures"):
|
||||||
|
yield from mark.args
|
||||||
|
|
||||||
def getfixtureclosure(
|
def getfixtureclosure(
|
||||||
self,
|
self,
|
||||||
fixturenames: Tuple[str, ...],
|
|
||||||
parentnode: nodes.Node,
|
parentnode: nodes.Node,
|
||||||
ignore_args: Sequence[str] = (),
|
initialnames: Tuple[str, ...],
|
||||||
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
|
ignore_args: AbstractSet[str],
|
||||||
|
) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
|
||||||
# Collect the closure of all fixtures, starting with the given
|
# Collect the closure of all fixtures, starting with the given
|
||||||
# fixturenames as the initial set. As we have to visit all
|
# fixturenames as the initial set. As we have to visit all
|
||||||
# factory definitions anyway, we also return an arg2fixturedefs
|
# factory definitions anyway, we also return an arg2fixturedefs
|
||||||
|
@ -1480,19 +1543,7 @@ class FixtureManager:
|
||||||
# (discovering matching fixtures for a given name/node is expensive).
|
# (discovering matching fixtures for a given name/node is expensive).
|
||||||
|
|
||||||
parentid = parentnode.nodeid
|
parentid = parentnode.nodeid
|
||||||
fixturenames_closure = list(self._getautousenames(parentid))
|
fixturenames_closure = list(initialnames)
|
||||||
|
|
||||||
def merge(otherlist: Iterable[str]) -> None:
|
|
||||||
for arg in otherlist:
|
|
||||||
if arg not in fixturenames_closure:
|
|
||||||
fixturenames_closure.append(arg)
|
|
||||||
|
|
||||||
merge(fixturenames)
|
|
||||||
|
|
||||||
# At this point, fixturenames_closure contains what we call "initialnames",
|
|
||||||
# which is a set of fixturenames the function immediately requests. We
|
|
||||||
# need to return it as well, so save this.
|
|
||||||
initialnames = tuple(fixturenames_closure)
|
|
||||||
|
|
||||||
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
|
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
|
||||||
lastlen = -1
|
lastlen = -1
|
||||||
|
@ -1506,7 +1557,9 @@ class FixtureManager:
|
||||||
fixturedefs = self.getfixturedefs(argname, parentid)
|
fixturedefs = self.getfixturedefs(argname, parentid)
|
||||||
if fixturedefs:
|
if fixturedefs:
|
||||||
arg2fixturedefs[argname] = fixturedefs
|
arg2fixturedefs[argname] = fixturedefs
|
||||||
merge(fixturedefs[-1].argnames)
|
for arg in fixturedefs[-1].argnames:
|
||||||
|
if arg not in fixturenames_closure:
|
||||||
|
fixturenames_closure.append(arg)
|
||||||
|
|
||||||
def sort_by_scope(arg_name: str) -> Scope:
|
def sort_by_scope(arg_name: str) -> Scope:
|
||||||
try:
|
try:
|
||||||
|
@ -1517,7 +1570,7 @@ class FixtureManager:
|
||||||
return fixturedefs[-1]._scope
|
return fixturedefs[-1]._scope
|
||||||
|
|
||||||
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
|
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
|
||||||
return initialnames, fixturenames_closure, arg2fixturedefs
|
return fixturenames_closure, arg2fixturedefs
|
||||||
|
|
||||||
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
|
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
|
||||||
"""Generate new tests based on parametrized fixtures used by the given metafunc"""
|
"""Generate new tests based on parametrized fixtures used by the given metafunc"""
|
||||||
|
|
|
@ -12,6 +12,7 @@ from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import PrintHelp
|
from _pytest.config import PrintHelp
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
|
|
||||||
class HelpAction(Action):
|
class HelpAction(Action):
|
||||||
|
@ -161,7 +162,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||||
def showhelp(config: Config) -> None:
|
def showhelp(config: Config) -> None:
|
||||||
import textwrap
|
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 = reporter._tw
|
||||||
tw.write(config._parser.optparser.format_help())
|
tw.write(config._parser.optparser.format_help())
|
||||||
tw.line()
|
tw.line()
|
||||||
|
|
|
@ -55,7 +55,7 @@ hookspec = HookspecMarker("pytest")
|
||||||
@hookspec(historic=True)
|
@hookspec(historic=True)
|
||||||
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
||||||
"""Called at plugin registration time to allow adding new hooks via a call to
|
"""Called at plugin registration time to allow adding new hooks via a call to
|
||||||
``pluginmanager.add_hookspecs(module_or_class, prefix)``.
|
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
|
||||||
|
|
||||||
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
||||||
|
|
||||||
|
@ -96,8 +96,8 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||||
<pytest.Parser.addini>`.
|
<pytest.Parser.addini>`.
|
||||||
|
|
||||||
:param pytest.PytestPluginManager pluginmanager:
|
:param pytest.PytestPluginManager pluginmanager:
|
||||||
The pytest plugin manager, which can be used to install :py:func:`hookspec`'s
|
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
|
||||||
or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks
|
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
|
||||||
to change how command line options are added.
|
to change how command line options are added.
|
||||||
|
|
||||||
Options can later be accessed through the
|
Options can later be accessed through the
|
||||||
|
@ -858,8 +858,8 @@ def pytest_warning_recorded(
|
||||||
"""Process a warning captured by the internal pytest warnings plugin.
|
"""Process a warning captured by the internal pytest warnings plugin.
|
||||||
|
|
||||||
:param warning_message:
|
:param warning_message:
|
||||||
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
|
The captured warning. This is the same object produced by :class:`warnings.catch_warnings`,
|
||||||
the same attributes as the parameters of :py:func:`warnings.showwarning`.
|
and contains the same attributes as the parameters of :py:func:`warnings.showwarning`.
|
||||||
|
|
||||||
:param when:
|
:param when:
|
||||||
Indicates when the warning was captured. Possible values:
|
Indicates when the warning was captured. Possible values:
|
||||||
|
@ -940,10 +940,10 @@ def pytest_exception_interact(
|
||||||
interactively handled.
|
interactively handled.
|
||||||
|
|
||||||
May be called during collection (see :hook:`pytest_make_collect_report`),
|
May be called during collection (see :hook:`pytest_make_collect_report`),
|
||||||
in which case ``report`` is a :class:`CollectReport`.
|
in which case ``report`` is a :class:`~pytest.CollectReport`.
|
||||||
|
|
||||||
May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
|
May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
|
||||||
in which case ``report`` is a :class:`TestReport`.
|
in which case ``report`` is a :class:`~pytest.TestReport`.
|
||||||
|
|
||||||
This hook is not called if the exception that was raised is an internal
|
This hook is not called if the exception that was raised is an internal
|
||||||
exception like ``skip.Exception``.
|
exception like ``skip.Exception``.
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue