Compare commits
79 Commits
7.2.0.dev0
...
7.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f12087fe0 | ||
|
|
bc3021cdfd | ||
|
|
591d476f14 | ||
|
|
6ca733e8f1 | ||
|
|
ac37b1b113 | ||
|
|
c891e402ac | ||
|
|
e2753a2b8b | ||
|
|
b5a154c1d9 | ||
|
|
0fae45bb6e | ||
|
|
37d434f5fc | ||
|
|
6684110408 | ||
|
|
ed8255c811 | ||
|
|
453bcb1b83 | ||
|
|
a24c78b7af | ||
|
|
4bf8aff5b7 | ||
|
|
462a6f0de6 | ||
|
|
c588a4720e | ||
|
|
048a10bd96 | ||
|
|
3c35477230 | ||
|
|
5eb4d6977c | ||
|
|
e37fbe5685 | ||
|
|
737b220516 | ||
|
|
7fa3972963 | ||
|
|
b304499925 | ||
|
|
f17525df26 | ||
|
|
0a7be971d2 | ||
|
|
c17908cdb3 | ||
|
|
ab549bba8a | ||
|
|
4b1707ff70 | ||
|
|
28e5b3b8b7 | ||
|
|
e854d05328 | ||
|
|
48e64feac7 | ||
|
|
b66899d322 | ||
|
|
b1b1bd03a8 | ||
|
|
6639d6c7d8 | ||
|
|
6a1b8e4b28 | ||
|
|
5c49dea989 | ||
|
|
a1635ca49a | ||
|
|
46c06e7560 | ||
|
|
94bcd2ce0f | ||
|
|
4967a0d084 | ||
|
|
efa02cffdd | ||
|
|
b5a6e30dae | ||
|
|
d0ae12ca76 | ||
|
|
0c5fc61610 | ||
|
|
41cb93b549 | ||
|
|
7381ce2d83 | ||
|
|
5d151c0e7c | ||
|
|
f988e070c6 | ||
|
|
1a04121f2f | ||
|
|
8542eb47c4 | ||
|
|
4f79cea72d | ||
|
|
c76989352f | ||
|
|
74f4aad708 | ||
|
|
5171327e3b | ||
|
|
9274fa5610 | ||
|
|
161841d38e | ||
|
|
e62daed8c4 | ||
|
|
764f90351a | ||
|
|
50bf3625c9 | ||
|
|
0002597ddd | ||
|
|
41e424b172 | ||
|
|
839ca90c0e | ||
|
|
8f1a8800c8 | ||
|
|
045713ac2e | ||
|
|
f094355401 | ||
|
|
378baab126 | ||
|
|
74b9f46e40 | ||
|
|
bf913eb4e7 | ||
|
|
813e1e5b54 | ||
|
|
3587d6b526 | ||
|
|
76e108d06c | ||
|
|
5f70fcba2e | ||
|
|
39028fac00 | ||
|
|
ac0870ebad | ||
|
|
871533322b | ||
|
|
df2c59c07e | ||
|
|
9bfa02ea07 | ||
|
|
85897eddc6 |
51
.github/workflows/backport.yml
vendored
51
.github/workflows/backport.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: backport
|
||||
|
||||
on:
|
||||
# Note that `pull_request_target` has security implications:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
|
||||
# In particular:
|
||||
# - Only allow triggers that can be used only be trusted users
|
||||
# - Don't execute any code from the target branch
|
||||
# - Don't use cache
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: startsWith(github.event.label.name, 'backport ') && github.event.pull_request.merged
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Create backport PR
|
||||
run: |
|
||||
set -eux
|
||||
|
||||
git config --global user.name "pytest bot"
|
||||
git config --global user.email "pytestbot@gmail.com"
|
||||
|
||||
label='${{ github.event.label.name }}'
|
||||
target_branch="${label#backport }"
|
||||
backport_branch=backport-${{ github.event.number }}-to-"${target_branch}"
|
||||
subject="[$target_branch] $(gh pr view --json title -q .title ${{ github.event.number }})"
|
||||
|
||||
git checkout origin/"${target_branch}" -b "${backport_branch}"
|
||||
git cherry-pick -x --mainline 1 ${{ github.event.pull_request.merge_commit_sha }}
|
||||
git commit --amend --message "$subject"
|
||||
git push --set-upstream origin --force-with-lease "${backport_branch}"
|
||||
gh pr create \
|
||||
--base "${target_branch}" \
|
||||
--title "${subject}" \
|
||||
--body "Backport of PR #${{ github.event.number }} to $target_branch branch. PR created by backport workflow."
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
23
.github/workflows/main.yml
vendored
23
.github/workflows/main.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- "[0-9]+.[0-9]+.x"
|
||||
- "test-me-*"
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
@@ -32,6 +31,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: [
|
||||
"windows-py36",
|
||||
"windows-py37",
|
||||
"windows-py37-pluggy",
|
||||
"windows-py38",
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
"windows-py310",
|
||||
"windows-py311",
|
||||
|
||||
"ubuntu-py36",
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
@@ -50,8 +51,6 @@ jobs:
|
||||
|
||||
"macos-py37",
|
||||
"macos-py38",
|
||||
"macos-py39",
|
||||
"macos-py310",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
@@ -59,6 +58,10 @@ jobs:
|
||||
]
|
||||
|
||||
include:
|
||||
- name: "windows-py36"
|
||||
python: "3.6"
|
||||
os: windows-latest
|
||||
tox_env: "py36-xdist"
|
||||
- name: "windows-py37"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
@@ -85,6 +88,10 @@ jobs:
|
||||
os: windows-latest
|
||||
tox_env: "py311"
|
||||
|
||||
- name: "ubuntu-py36"
|
||||
python: "3.6"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py36-xdist"
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -128,17 +135,9 @@ jobs:
|
||||
os: macos-latest
|
||||
tox_env: "py38-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py39"
|
||||
python: "3.9"
|
||||
os: macos-latest
|
||||
tox_env: "py39-xdist"
|
||||
- name: "macos-py310"
|
||||
python: "3.10"
|
||||
os: macos-latest
|
||||
tox_env: "py310-xdist"
|
||||
|
||||
- name: "plugins"
|
||||
python: "3.9"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "plugins"
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.1.0
|
||||
rev: 21.11b1
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v1.12.1
|
||||
rev: v1.12.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==20.8b1]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.1.0
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -20,32 +20,24 @@ repos:
|
||||
- id: debug-statements
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v1.4
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
additional_dependencies:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-typing-imports==1.9.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v2.7.1
|
||||
rev: v2.6.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
args: ['--application-directories=.:src', --py36-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.0
|
||||
rev: v2.29.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
args: [--py36-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.20.0
|
||||
hooks:
|
||||
@@ -56,7 +48,7 @@ repos:
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.931
|
||||
rev: v0.910-1
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@@ -187,7 +187,6 @@ Kevin Cox
|
||||
Kevin J. Foley
|
||||
Kian-Meng Ang
|
||||
Kodi B. Arfer
|
||||
Kojo Idrissa
|
||||
Kostis Anagnostopoulos
|
||||
Kristoffer Nordström
|
||||
Kyle Altendorf
|
||||
|
||||
@@ -391,13 +391,6 @@ actual latest release). The procedure for this is:
|
||||
request, as described above. An exception to this is if the bug fix is not
|
||||
applicable to ``main`` anymore.
|
||||
|
||||
Automatic method:
|
||||
|
||||
Add a ``backport 1.2.x`` label to the PR you want to backport. This will create
|
||||
a backport PR against the ``1.2.x`` branch.
|
||||
|
||||
Manual method:
|
||||
|
||||
#. ``git checkout origin/1.2.x -b backport-XXXX`` # use the main PR number here
|
||||
|
||||
#. Locate the merge commit on the PR, in the *merged* message, for example:
|
||||
|
||||
@@ -100,7 +100,7 @@ Features
|
||||
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
|
||||
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
|
||||
|
||||
- Python 3.7+ or PyPy3
|
||||
- Python 3.6+ and PyPy3
|
||||
|
||||
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ breaking changes or new features.
|
||||
|
||||
For a new minor release, first create a new maintenance branch from ``main``::
|
||||
|
||||
git fetch upstream
|
||||
git fetch --all
|
||||
git branch 7.1.x upstream/main
|
||||
git push upstream 7.1.x
|
||||
|
||||
@@ -63,7 +63,7 @@ Major releases
|
||||
|
||||
1. Create a new maintenance branch from ``main``::
|
||||
|
||||
git fetch upstream
|
||||
git fetch --all
|
||||
git branch 8.0.x upstream/main
|
||||
git push upstream 8.0.x
|
||||
|
||||
@@ -136,31 +136,29 @@ Both automatic and manual processes described above follow the same steps from t
|
||||
#. After all tests pass and the PR has been approved, tag the release commit
|
||||
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
|
||||
|
||||
git fetch upstream
|
||||
git fetch --all
|
||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
||||
git push upstream MAJOR.MINOR.PATCH
|
||||
git push git@github.com:pytest-dev/pytest.git MAJOR.MINOR.PATCH
|
||||
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
|
||||
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
||||
#. Merge the PR.
|
||||
|
||||
#. Cherry-pick the CHANGELOG / announce files to the ``main`` branch::
|
||||
|
||||
git fetch upstream
|
||||
git fetch --all --prune
|
||||
git checkout upstream/main -b cherry-pick-release
|
||||
git cherry-pick -x -m1 upstream/MAJOR.MINOR.x
|
||||
|
||||
#. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step.
|
||||
|
||||
#. For major and minor releases (or the first prerelease of it), tag the release cherry-pick merge commit in main with
|
||||
#. For major and minor releases, tag the release cherry-pick merge commit in main with
|
||||
a dev tag for the next feature release::
|
||||
|
||||
git checkout main
|
||||
git pull
|
||||
git tag MAJOR.{MINOR+1}.0.dev0
|
||||
git push upstream MAJOR.{MINOR+1}.0.dev0
|
||||
|
||||
#. For major and minor releases, change the default version in the `Read the Docs Settings <https://readthedocs.org/dashboard/pytest/advanced/>`_ to the new branch.
|
||||
git push git@github.com:pytest-dev/pytest.git MAJOR.{MINOR+1}.0.dev0
|
||||
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.1.0
|
||||
release-7.0.1
|
||||
release-7.0.0
|
||||
release-7.0.0rc1
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
pytest-7.1.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.1.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Akuli
|
||||
* Andrew Svetlov
|
||||
* Anthony Sottile
|
||||
* Brett Holman
|
||||
* Bruno Oliveira
|
||||
* Chris NeJame
|
||||
* Dan Alvizu
|
||||
* Elijah DeLee
|
||||
* Emmanuel Arias
|
||||
* Fabian Egli
|
||||
* Florian Bruhin
|
||||
* Gabor Szabo
|
||||
* Hasan Ramezani
|
||||
* Hugo van Kemenade
|
||||
* Kian Meng, Ang
|
||||
* Kojo Idrissa
|
||||
* Masaru Tsuchiyama
|
||||
* Olga Matoula
|
||||
* P. L. Lim
|
||||
* Ran Benita
|
||||
* Tobias Deiminger
|
||||
* Yuval Shimon
|
||||
* eduardo naufel schettino
|
||||
* Éric
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -65,7 +65,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1334
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1365
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -134,7 +134,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:487
|
||||
caplog -- .../_pytest/logging.py:483
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
|
||||
@@ -28,98 +28,6 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.1.0 (2022-03-13)
|
||||
=========================
|
||||
|
||||
Breaking Changes
|
||||
----------------
|
||||
|
||||
- `#8838 <https://github.com/pytest-dev/pytest/issues/8838>`_: As per our policy, the following features have been deprecated in the 6.X series and are now
|
||||
removed:
|
||||
|
||||
* ``pytest._fillfuncargs`` function.
|
||||
|
||||
* ``pytest_warning_captured`` hook - use ``pytest_warning_recorded`` instead.
|
||||
|
||||
* ``-k -foobar`` syntax - use ``-k 'not foobar'`` instead.
|
||||
|
||||
* ``-k foobar:`` syntax.
|
||||
|
||||
* ``pytest.collect`` module - import from ``pytest`` directly.
|
||||
|
||||
For more information consult
|
||||
`Deprecations and Removals <https://docs.pytest.org/en/latest/deprecations.html>`__ in the docs.
|
||||
|
||||
|
||||
- `#9437 <https://github.com/pytest-dev/pytest/issues/9437>`_: Dropped support for Python 3.6, which reached `end-of-life <https://devguide.python.org/#status-of-python-branches>`__ at 2021-12-23.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#5192 <https://github.com/pytest-dev/pytest/issues/5192>`_: Fixed test output for some data types where ``-v`` would show less information.
|
||||
|
||||
Also, when showing diffs for sequences, ``-q`` would produce full diffs instead of the expected diff.
|
||||
|
||||
|
||||
- `#9362 <https://github.com/pytest-dev/pytest/issues/9362>`_: pytest now avoids specialized assert formatting when it is detected that the default ``__eq__`` is overridden in ``attrs`` or ``dataclasses``.
|
||||
|
||||
|
||||
- `#9536 <https://github.com/pytest-dev/pytest/issues/9536>`_: When ``-vv`` is given on command line, show skipping and xfail reasons in full instead of truncating them to fit the terminal width.
|
||||
|
||||
|
||||
- `#9644 <https://github.com/pytest-dev/pytest/issues/9644>`_: More information about the location of resources that led Python to raise :class:`ResourceWarning` can now
|
||||
be obtained by enabling :mod:`tracemalloc`.
|
||||
|
||||
See :ref:`resource-warnings` for more information.
|
||||
|
||||
|
||||
- `#9678 <https://github.com/pytest-dev/pytest/issues/9678>`_: More types are now accepted in the ``ids`` argument to ``@pytest.mark.parametrize``.
|
||||
Previously only `str`, `float`, `int` and `bool` were accepted;
|
||||
now `bytes`, `complex`, `re.Pattern`, `Enum` and anything with a `__name__` are also accepted.
|
||||
|
||||
|
||||
- `#9692 <https://github.com/pytest-dev/pytest/issues/9692>`_: :func:`pytest.approx` now raises a :class:`TypeError` when given an unordered sequence (such as :class:`set`).
|
||||
|
||||
Note that this implies that custom classes which only implement ``__iter__`` and ``__len__`` are no longer supported as they don't guarantee order.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#8242 <https://github.com/pytest-dev/pytest/issues/8242>`_: The deprecation of raising :class:`unittest.SkipTest` to skip collection of
|
||||
tests during the pytest collection phase is reverted - this is now a supported
|
||||
feature again.
|
||||
|
||||
|
||||
- `#9493 <https://github.com/pytest-dev/pytest/issues/9493>`_: Symbolic link components are no longer resolved in conftest paths.
|
||||
This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice.
|
||||
For example, given
|
||||
|
||||
tests/real/conftest.py
|
||||
tests/real/test_it.py
|
||||
tests/link -> tests/real
|
||||
|
||||
running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``.
|
||||
This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details).
|
||||
|
||||
|
||||
- `#9626 <https://github.com/pytest-dev/pytest/issues/9626>`_: Fixed count of selected tests on terminal collection summary when there were errors or skipped modules.
|
||||
|
||||
If there were errors or skipped modules on collection, pytest would mistakenly subtract those from the selected count.
|
||||
|
||||
|
||||
- `#9645 <https://github.com/pytest-dev/pytest/issues/9645>`_: Fixed regression where ``--import-mode=importlib`` used together with :envvar:`PYTHONPATH` or :confval:`pythonpath` would cause import errors in test suites.
|
||||
|
||||
|
||||
- `#9708 <https://github.com/pytest-dev/pytest/issues/9708>`_: :fixture:`pytester` now requests a :fixture:`monkeypatch` fixture instead of creating one internally. This solves some issues with tests that involve pytest environment variables.
|
||||
|
||||
|
||||
- `#9730 <https://github.com/pytest-dev/pytest/issues/9730>`_: Malformed ``pyproject.toml`` files now produce a clearer error message.
|
||||
|
||||
|
||||
pytest 7.0.1 (2022-02-11)
|
||||
=========================
|
||||
|
||||
@@ -303,8 +211,6 @@ Deprecations
|
||||
:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` /
|
||||
:func:`unittest.skip` in unittest test cases is fully supported.
|
||||
|
||||
.. note:: This deprecation has been reverted in pytest 7.1.0.
|
||||
|
||||
|
||||
- `#8315 <https://github.com/pytest-dev/pytest/issues/8315>`_: Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
|
||||
scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
|
||||
|
||||
@@ -382,6 +382,7 @@ texinfo_documents = [
|
||||
]
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
"pluggy": ("https://pluggy.readthedocs.io/en/stable", None),
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
@@ -389,6 +390,10 @@ intersphinx_mapping = {
|
||||
"pip": ("https://pip.pypa.io/en/stable", None),
|
||||
"tox": ("https://tox.wiki/en/stable", None),
|
||||
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
|
||||
"django": (
|
||||
"http://docs.djangoproject.com/en/stable",
|
||||
"http://docs.djangoproject.com/en/stable/_objects",
|
||||
),
|
||||
"setuptools": ("https://setuptools.pypa.io/en/stable", None),
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Deprecated Features
|
||||
-------------------
|
||||
|
||||
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
|
||||
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
|
||||
.. _instance-collector-deprecation:
|
||||
|
||||
@@ -241,6 +241,19 @@ scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
|
||||
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
||||
|
||||
|
||||
Raising ``unittest.SkipTest`` during collection
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Raising :class:`unittest.SkipTest` to skip collection of tests during the
|
||||
pytest collection phase is deprecated. Use :func:`pytest.skip` instead.
|
||||
|
||||
Note: This deprecation only relates to using `unittest.SkipTest` during test
|
||||
collection. You are probably not doing that. Ordinary usage of
|
||||
:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` /
|
||||
:func:`unittest.skip` in unittest test cases is fully supported.
|
||||
|
||||
Using ``pytest.warns(None)``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -275,42 +288,29 @@ The ``yield_fixture`` function/decorator
|
||||
It has been so for a very long time, so can be search/replaced safely.
|
||||
|
||||
|
||||
Removed Features
|
||||
----------------
|
||||
The ``pytest_warning_captured`` hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
.. deprecated:: 6.0
|
||||
|
||||
This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``.
|
||||
|
||||
Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter
|
||||
by a ``nodeid`` parameter.
|
||||
|
||||
The ``pytest.collect`` module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.0
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
The ``pytest.collect`` module is no longer part of the public API, all its names
|
||||
should now be imported from ``pytest`` directly instead.
|
||||
|
||||
|
||||
|
||||
The ``pytest_warning_captured`` hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.0
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``.
|
||||
|
||||
Use the ``pytest_warning_recorded`` hook instead, which replaces the ``item`` parameter
|
||||
by a ``nodeid`` parameter.
|
||||
|
||||
|
||||
|
||||
The ``pytest._fillfuncargs`` function
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.0
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
This function was kept for backward compatibility with an older plugin.
|
||||
|
||||
@@ -319,6 +319,12 @@ it, use `function._request._fillfixtures()` instead, though note this is not
|
||||
a public API and may break in the future.
|
||||
|
||||
|
||||
Removed Features
|
||||
----------------
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
``--no-print-logs`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
> assert [0, 1, 2] == [0, 1, 3]
|
||||
E assert [0, 1, 2] == [0, 1, 3]
|
||||
E At index 2 diff: 2 != 3
|
||||
E Use -v to get more diff
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:63: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||
@@ -168,7 +168,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
> assert a == b
|
||||
E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...]
|
||||
E At index 100 diff: 1 != 2
|
||||
E Use -v to get more diff
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:68: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||
@@ -215,7 +215,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
> assert [1, 2] == [1, 2, 3]
|
||||
E assert [1, 2] == [1, 2, 3]
|
||||
E Right contains one more item: 3
|
||||
E Use -v to get more diff
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:77: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||
|
||||
@@ -9,7 +9,7 @@ Get Started
|
||||
Install ``pytest``
|
||||
----------------------------------------
|
||||
|
||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
||||
``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3.
|
||||
|
||||
1. Run the following command in your command line:
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.1.0
|
||||
pytest 7.0.1
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ if you run this module:
|
||||
E '1'
|
||||
E Extra items in the right set:
|
||||
E '5'
|
||||
E Use -v to get more diff
|
||||
E Use -v to get the full diff
|
||||
|
||||
test_assert2.py:4: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
|
||||
@@ -5,7 +5,7 @@ How to set up bash completion
|
||||
=============================
|
||||
|
||||
When using bash as your shell, ``pytest`` can use argcomplete
|
||||
(https://kislyuk.github.io/argcomplete/) for auto-completion.
|
||||
(https://argcomplete.readthedocs.io/) for auto-completion.
|
||||
For this ``argcomplete`` needs to be installed **and** enabled.
|
||||
|
||||
Install argcomplete using:
|
||||
|
||||
@@ -358,7 +358,7 @@ Additional use cases of warnings in tests
|
||||
|
||||
Here are some use cases involving warnings that often come up in tests, and suggestions on how to deal with them:
|
||||
|
||||
- To ensure that **at least one** warning is emitted, use:
|
||||
- To ensure that **any** warning is emitted, use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -441,18 +441,3 @@ Please read our :ref:`backwards-compatibility` to learn how we proceed about dep
|
||||
features.
|
||||
|
||||
The full list of warnings is listed in :ref:`the reference documentation <warnings ref>`.
|
||||
|
||||
|
||||
.. _`resource-warnings`:
|
||||
|
||||
Resource Warnings
|
||||
-----------------
|
||||
|
||||
Additional information of the source of a :class:`ResourceWarning` can be obtained when captured by pytest if
|
||||
:mod:`tracemalloc` module is enabled.
|
||||
|
||||
One convenient way to enable :mod:`tracemalloc` when running tests is to set the :envvar:`PYTHONTRACEMALLOC` to a large
|
||||
enough number of frames (say ``20``, but that number is application dependent).
|
||||
|
||||
For more information, consult the `Python Development Mode <https://docs.python.org/3/library/devmode.html>`__
|
||||
section in the Python documentation.
|
||||
|
||||
@@ -84,7 +84,7 @@ Executing pytest normally gives us this output (we are skipping the header to fo
|
||||
> assert fruits1 == fruits2
|
||||
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
|
||||
E At index 2 diff: 'grapes' != 'orange'
|
||||
E Use -v to get more diff
|
||||
E Use -v to get the full diff
|
||||
|
||||
test_verbosity_example.py:8: AssertionError
|
||||
____________________________ test_numbers_fail _____________________________
|
||||
@@ -99,7 +99,7 @@ Executing pytest normally gives us this output (we are skipping the header to fo
|
||||
E {'1': 1, '2': 2, '3': 3, '4': 4}
|
||||
E Right contains 4 more items:
|
||||
E {'10': 10, '20': 20, '30': 30, '40': 40}
|
||||
E Use -v to get more diff
|
||||
E Use -v to get the full diff
|
||||
|
||||
test_verbosity_example.py:14: AssertionError
|
||||
___________________________ test_long_text_fail ____________________________
|
||||
|
||||
@@ -21,7 +21,7 @@ there is no need to activate it.
|
||||
Here is a little annotated list for some popular plugins:
|
||||
|
||||
* :pypi:`pytest-django`: write tests
|
||||
for `django <https://docs.djangoproject.com/>`_ apps, using pytest integration.
|
||||
for :std:doc:`django <django:index>` apps, using pytest integration.
|
||||
|
||||
* :pypi:`pytest-twisted`: write tests
|
||||
for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and
|
||||
@@ -51,6 +51,9 @@ Here is a little annotated list for some popular plugins:
|
||||
* :pypi:`pytest-flakes`:
|
||||
check source code with pyflakes.
|
||||
|
||||
* :pypi:`oejskit`:
|
||||
a plugin to run javascript unittests in live browsers.
|
||||
|
||||
To see a complete list of all plugins with their latest testing
|
||||
status against different pytest and Python versions, please visit
|
||||
:ref:`plugin-list`.
|
||||
|
||||
@@ -84,14 +84,14 @@ It is also possible to skip the whole module using
|
||||
|
||||
If you wish to skip something conditionally then you can use ``skipif`` instead.
|
||||
Here is an example of marking a test function to be skipped
|
||||
when run on an interpreter earlier than Python3.10:
|
||||
when run on an interpreter earlier than Python3.6:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
|
||||
def test_function():
|
||||
...
|
||||
|
||||
|
||||
@@ -121,10 +121,6 @@ the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||
``NUM`` will be incremented with each test run. Moreover, entries older
|
||||
than 3 temporary directories will be removed.
|
||||
|
||||
The number of entries currently cannot be changed, but using the ``--basetemp``
|
||||
option will remove the directory before every run, effectively meaning the temporary directories
|
||||
of only the most recent run will be kept.
|
||||
|
||||
You can override the default temporary directory setting like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
:orphan:
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `PyConDE <https://2022.pycon.de/program/W93DBJ/>`__, April 11th 2022 (3h), Berlin, Germany
|
||||
- `PyConIT <https://pycon.it/en/talk/pytest-simple-rapid-and-fun-testing-with-python>`__, June 3rd 2022 (4h), Florence, Italy
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
..
|
||||
- `Europython <https://ep2022.europython.eu/>`__, July 11th to 17th (3h), Dublin, Ireland
|
||||
- `CH Open Workshoptage <https://workshoptage.ch/>`__ (German), September 6th to 8th (1 day), Bern, Switzerland
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `Professional Testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, February 1st to 3rd, 2022, Leipzig (Germany) and remote.
|
||||
|
||||
Also see `previous talks and blogposts <talks.html>`_.
|
||||
|
||||
.. _features:
|
||||
|
||||
@@ -23,7 +18,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can
|
||||
scale to support complex functional testing for applications and libraries.
|
||||
|
||||
|
||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
||||
**Pythons**: ``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3.
|
||||
|
||||
**PyPI package name**: :pypi:`pytest`
|
||||
|
||||
@@ -84,7 +79,7 @@ Features
|
||||
|
||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
||||
|
||||
- Python 3.7+ or PyPy 3
|
||||
- Python 3.6+ and PyPy 3
|
||||
|
||||
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -808,6 +808,8 @@ Session related reporting hooks:
|
||||
.. autofunction:: pytest_fixture_setup
|
||||
.. hook:: pytest_fixture_post_finalizer
|
||||
.. autofunction:: pytest_fixture_post_finalizer
|
||||
.. hook:: pytest_warning_captured
|
||||
.. autofunction:: pytest_warning_captured
|
||||
.. hook:: pytest_warning_recorded
|
||||
.. autofunction:: pytest_warning_recorded
|
||||
|
||||
|
||||
@@ -11,14 +11,9 @@ Books
|
||||
- `Python Testing with pytest, by Brian Okken (2017)
|
||||
<https://pragprog.com/book/bopytest/python-testing-with-pytest>`_.
|
||||
|
||||
- `Python Testing with pytest, Second Edition, by Brian Okken (2022)
|
||||
<https://pragprog.com/titles/bopytest2/python-testing-with-pytest-second-edition>`_.
|
||||
|
||||
Talks and blog postings
|
||||
---------------------------------------------
|
||||
|
||||
- `pytest: Simple, rapid and fun testing with Python, <https://youtu.be/cSJ-X3TbQ1c?t=15752>`_ (@ 4:22:32), Florian Bruhin, WeAreDevelopers World Congress 2021
|
||||
|
||||
- Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020
|
||||
|
||||
- Webinar: `Simplify Your Tests with Fixtures <https://blog.jetbrains.com/pycharm/2020/08/webinar-recording-simplify-your-tests-with-fixtures-with-oliver-bestwalter/>`_, Oliver Bestwalter, via JetBrains, 2020
|
||||
|
||||
@@ -28,6 +28,8 @@ filterwarnings = [
|
||||
"default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*",
|
||||
# distutils is deprecated in 3.10, scheduled for removal in 3.12
|
||||
"ignore:The distutils package is deprecated:DeprecationWarning",
|
||||
# produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)."
|
||||
"ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))",
|
||||
# produced by pytest-xdist
|
||||
"ignore:.*type argument to addoption.*:DeprecationWarning",
|
||||
# produced on execnet (pytest-xdist)
|
||||
@@ -111,4 +113,4 @@ template = "changelog/_template.rst"
|
||||
showcontent = true
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py37']
|
||||
target-version = ['py36']
|
||||
|
||||
@@ -17,6 +17,7 @@ classifiers =
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
@@ -50,7 +51,7 @@ install_requires =
|
||||
atomicwrites>=1.0;sys_platform=="win32"
|
||||
colorama;sys_platform=="win32"
|
||||
importlib-metadata>=0.12;python_version<"3.8"
|
||||
python_requires = >=3.7
|
||||
python_requires = >=3.6
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
|
||||
@@ -108,6 +108,7 @@ if os.environ.get("_ARGCOMPLETE"):
|
||||
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||
argcomplete.autocomplete(parser, always_complete_options=False)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
@@ -100,6 +100,9 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
spec is None
|
||||
# this is a namespace package (without `__init__.py`)
|
||||
# there's nothing to rewrite there
|
||||
# python3.6: `namespace`
|
||||
# python3.7+: `None`
|
||||
or spec.origin == "namespace"
|
||||
or spec.origin is None
|
||||
# we can only rewrite source files
|
||||
or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
|
||||
@@ -292,8 +295,9 @@ def _write_pyc_fp(
|
||||
# import. However, there's little reason to deviate.
|
||||
fp.write(importlib.util.MAGIC_NUMBER)
|
||||
# https://www.python.org/dev/peps/pep-0552/
|
||||
flags = b"\x00\x00\x00\x00"
|
||||
fp.write(flags)
|
||||
if sys.version_info >= (3, 7):
|
||||
flags = b"\x00\x00\x00\x00"
|
||||
fp.write(flags)
|
||||
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
|
||||
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
|
||||
size = source_stat.st_size & 0xFFFFFFFF
|
||||
@@ -322,6 +326,7 @@ if sys.platform == "win32":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def _write_pyc(
|
||||
@@ -374,29 +379,31 @@ def _read_pyc(
|
||||
except OSError:
|
||||
return None
|
||||
with fp:
|
||||
# https://www.python.org/dev/peps/pep-0552/
|
||||
has_flags = sys.version_info >= (3, 7)
|
||||
try:
|
||||
stat_result = os.stat(source)
|
||||
mtime = int(stat_result.st_mtime)
|
||||
size = stat_result.st_size
|
||||
data = fp.read(16)
|
||||
data = fp.read(16 if has_flags else 12)
|
||||
except OSError as e:
|
||||
trace(f"_read_pyc({source}): OSError {e}")
|
||||
return None
|
||||
# Check for invalid or out of date pyc file.
|
||||
if len(data) != (16):
|
||||
if len(data) != (16 if has_flags else 12):
|
||||
trace("_read_pyc(%s): invalid pyc (too short)" % source)
|
||||
return None
|
||||
if data[:4] != importlib.util.MAGIC_NUMBER:
|
||||
trace("_read_pyc(%s): invalid pyc (bad magic number)" % source)
|
||||
return None
|
||||
if data[4:8] != b"\x00\x00\x00\x00":
|
||||
if has_flags and data[4:8] != b"\x00\x00\x00\x00":
|
||||
trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source)
|
||||
return None
|
||||
mtime_data = data[8:12]
|
||||
mtime_data = data[8 if has_flags else 4 : 12 if has_flags else 8]
|
||||
if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
|
||||
trace("_read_pyc(%s): out of date" % source)
|
||||
return None
|
||||
size_data = data[12:16]
|
||||
size_data = data[12 if has_flags else 8 : 16 if has_flags else 12]
|
||||
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
|
||||
trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
|
||||
return None
|
||||
|
||||
@@ -135,27 +135,6 @@ def isiterable(obj: Any) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def has_default_eq(
|
||||
obj: object,
|
||||
) -> bool:
|
||||
"""Check if an instance of an object contains the default eq
|
||||
|
||||
First, we check if the object's __eq__ attribute has __code__,
|
||||
if so, we check the equally of the method code filename (__code__.co_filename)
|
||||
to the default one generated by the dataclass and attr module
|
||||
for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
|
||||
"""
|
||||
# inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
|
||||
if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
|
||||
code_filename = obj.__eq__.__code__.co_filename
|
||||
|
||||
if isattrs(obj):
|
||||
return "attrs generated eq" in code_filename
|
||||
|
||||
return code_filename == "<string>" # data class
|
||||
return True
|
||||
|
||||
|
||||
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
|
||||
"""Return specialised explanations for some operators/operands."""
|
||||
verbose = config.getoption("verbose")
|
||||
@@ -223,6 +202,8 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||
explanation = _compare_eq_set(left, right, verbose)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
elif verbose > 0:
|
||||
explanation = _compare_eq_verbose(left, right)
|
||||
|
||||
if isiterable(left) and isiterable(right):
|
||||
expl = _compare_eq_iterable(left, right, verbose)
|
||||
@@ -279,6 +260,18 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
|
||||
keepends = True
|
||||
left_lines = repr(left).splitlines(keepends)
|
||||
right_lines = repr(right).splitlines(keepends)
|
||||
|
||||
explanation: List[str] = []
|
||||
explanation += ["+" + line for line in left_lines]
|
||||
explanation += ["-" + line for line in right_lines]
|
||||
|
||||
return explanation
|
||||
|
||||
|
||||
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
|
||||
"""Move opening/closing parenthesis/bracket to own lines."""
|
||||
opening = lines[0][:1]
|
||||
@@ -294,8 +287,8 @@ def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
|
||||
def _compare_eq_iterable(
|
||||
left: Iterable[Any], right: Iterable[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
if verbose <= 0 and not running_on_ci():
|
||||
return ["Use -v to get more diff"]
|
||||
if not verbose and not running_on_ci():
|
||||
return ["Use -v to get the full diff"]
|
||||
# dynamic import to speedup pytest
|
||||
import difflib
|
||||
|
||||
@@ -434,8 +427,6 @@ def _compare_eq_dict(
|
||||
|
||||
|
||||
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
|
||||
if not has_default_eq(left):
|
||||
return []
|
||||
if isdatacls(left):
|
||||
all_fields = left.__dataclass_fields__
|
||||
fields_to_check = [field for field, info in all_fields.items() if info.compare]
|
||||
|
||||
@@ -68,8 +68,8 @@ def _colorama_workaround() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
"""Workaround for Windows Unicode console handling.
|
||||
def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
"""Workaround for Windows Unicode console handling on Python>=3.6.
|
||||
|
||||
Python 3.6 implemented Unicode console handling for Windows. This works
|
||||
by reading/writing to the raw console handle using
|
||||
@@ -112,7 +112,7 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
buffering = -1
|
||||
|
||||
return io.TextIOWrapper(
|
||||
open(os.dup(f.fileno()), mode, buffering),
|
||||
open(os.dup(f.fileno()), mode, buffering), # type: ignore[arg-type]
|
||||
f.encoding,
|
||||
f.errors,
|
||||
f.newlines,
|
||||
@@ -128,7 +128,7 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
def pytest_load_initial_conftests(early_config: Config):
|
||||
ns = early_config.known_args_namespace
|
||||
if ns.capture == "fd":
|
||||
_windowsconsoleio_workaround(sys.stdout)
|
||||
_py36_windowsconsoleio_workaround(sys.stdout)
|
||||
_colorama_workaround()
|
||||
pluginmanager = early_config.pluginmanager
|
||||
capman = CaptureManager(ns.capture)
|
||||
|
||||
@@ -4,6 +4,7 @@ import functools
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from inspect import Parameter
|
||||
from inspect import signature
|
||||
from pathlib import Path
|
||||
@@ -185,6 +186,17 @@ def getfuncargnames(
|
||||
return arg_names
|
||||
|
||||
|
||||
if sys.version_info < (3, 7):
|
||||
|
||||
@contextmanager
|
||||
def nullcontext():
|
||||
yield
|
||||
|
||||
|
||||
else:
|
||||
from contextlib import nullcontext as nullcontext # noqa: F401
|
||||
|
||||
|
||||
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
|
||||
# Note: this code intentionally mirrors the code at the beginning of
|
||||
# getfuncargnames, to get the arguments which were excluded from its result
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Command line options, ini-file and conftest.py processing."""
|
||||
import argparse
|
||||
import collections.abc
|
||||
import contextlib
|
||||
import copy
|
||||
import enum
|
||||
import inspect
|
||||
@@ -352,17 +353,13 @@ class PytestPluginManager(PluginManager):
|
||||
import _pytest.assertion
|
||||
|
||||
super().__init__("pytest")
|
||||
|
||||
# -- State related to local conftest plugins.
|
||||
# All loaded conftest modules.
|
||||
# The objects are module objects, only used generically.
|
||||
self._conftest_plugins: Set[types.ModuleType] = set()
|
||||
# All conftest modules applicable for a directory.
|
||||
# This includes the directory's own conftest modules as well
|
||||
# as those of its parent directories.
|
||||
|
||||
# State related to local conftest plugins.
|
||||
self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
|
||||
# Cutoff directory above which conftests are no longer discovered.
|
||||
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
|
||||
self._confcutdir: Optional[Path] = None
|
||||
# If set, conftest loading is skipped.
|
||||
self._noconftest = False
|
||||
|
||||
# _getconftestmodules()'s call to _get_directory() causes a stat
|
||||
@@ -531,19 +528,6 @@ class PytestPluginManager(PluginManager):
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(current, namespace.importmode, rootpath)
|
||||
|
||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||
"""Whether a path is within the confcutdir.
|
||||
|
||||
When false, should not load conftest.
|
||||
"""
|
||||
if self._confcutdir is None:
|
||||
return True
|
||||
try:
|
||||
path.relative_to(self._confcutdir)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _try_load_conftest(
|
||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> None:
|
||||
@@ -556,7 +540,7 @@ class PytestPluginManager(PluginManager):
|
||||
|
||||
def _getconftestmodules(
|
||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> Sequence[types.ModuleType]:
|
||||
) -> List[types.ModuleType]:
|
||||
if self._noconftest:
|
||||
return []
|
||||
|
||||
@@ -572,12 +556,14 @@ class PytestPluginManager(PluginManager):
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir.
|
||||
clist = []
|
||||
confcutdir_parents = self._confcutdir.parents if self._confcutdir else []
|
||||
for parent in reversed((directory, *directory.parents)):
|
||||
if self._is_in_confcutdir(parent):
|
||||
conftestpath = parent / "conftest.py"
|
||||
if conftestpath.is_file():
|
||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
clist.append(mod)
|
||||
if parent in confcutdir_parents:
|
||||
continue
|
||||
conftestpath = parent / "conftest.py"
|
||||
if conftestpath.is_file():
|
||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
return clist
|
||||
|
||||
@@ -599,9 +585,15 @@ class PytestPluginManager(PluginManager):
|
||||
def _importconftest(
|
||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> types.ModuleType:
|
||||
existing = self.get_plugin(str(conftestpath))
|
||||
if existing is not None:
|
||||
return cast(types.ModuleType, existing)
|
||||
# Use a resolved Path object as key to avoid loading the same conftest
|
||||
# twice with build systems that create build directories containing
|
||||
# symlinks to actual files.
|
||||
# Using Path().resolve() is better than py.path.realpath because
|
||||
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
|
||||
key = conftestpath.resolve()
|
||||
|
||||
with contextlib.suppress(KeyError):
|
||||
return self._conftestpath2mod[key]
|
||||
|
||||
pkgpath = resolve_package_path(conftestpath)
|
||||
if pkgpath is None:
|
||||
@@ -617,10 +609,11 @@ class PytestPluginManager(PluginManager):
|
||||
self._check_non_top_pytest_plugins(mod, conftestpath)
|
||||
|
||||
self._conftest_plugins.add(mod)
|
||||
self._conftestpath2mod[key] = mod
|
||||
dirpath = conftestpath.parent
|
||||
if dirpath in self._dirpath2confmods:
|
||||
for path, mods in self._dirpath2confmods.items():
|
||||
if dirpath in path.parents or path == dirpath:
|
||||
if path and dirpath in path.parents or path == dirpath:
|
||||
assert mod not in mods
|
||||
mods.append(mod)
|
||||
self.trace(f"loading conftestmodule {mod!r}")
|
||||
@@ -1348,6 +1341,14 @@ class Config:
|
||||
if records:
|
||||
frame = sys._getframe(stacklevel - 1)
|
||||
location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
|
||||
self.hook.pytest_warning_captured.call_historic(
|
||||
kwargs=dict(
|
||||
warning_message=records[0],
|
||||
when="config",
|
||||
item=None,
|
||||
location=location,
|
||||
)
|
||||
)
|
||||
self.hook.pytest_warning_recorded.call_historic(
|
||||
kwargs=dict(
|
||||
warning_message=records[0],
|
||||
@@ -1447,7 +1448,6 @@ class Config:
|
||||
)
|
||||
except KeyError:
|
||||
return None
|
||||
assert mod.__file__ is not None
|
||||
modpath = Path(mod.__file__).parent
|
||||
values: List[Path] = []
|
||||
for relroot in relroots:
|
||||
@@ -1593,7 +1593,7 @@ def _strtobool(val: str) -> bool:
|
||||
@lru_cache(maxsize=50)
|
||||
def parse_warning_filter(
|
||||
arg: str, *, escape: bool
|
||||
) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]:
|
||||
) -> Tuple[str, str, Type[Warning], str, int]:
|
||||
"""Parse a warnings filter string.
|
||||
|
||||
This is copied from warnings._setoption with the following changes:
|
||||
@@ -1635,7 +1635,7 @@ def parse_warning_filter(
|
||||
parts.append("")
|
||||
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
|
||||
try:
|
||||
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||
except warnings._OptionError as e:
|
||||
raise UsageError(error_template.format(error=str(e)))
|
||||
try:
|
||||
|
||||
@@ -70,7 +70,7 @@ def load_config_dict_from_file(
|
||||
try:
|
||||
config = tomli.loads(toml_text)
|
||||
except tomli.TOMLDecodeError as exc:
|
||||
raise UsageError(f"{filepath}: {exc}") from exc
|
||||
raise UsageError(str(exc)) from exc
|
||||
|
||||
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
|
||||
if result is not None:
|
||||
|
||||
@@ -11,6 +11,7 @@ in case of warnings which need to format their messages.
|
||||
from warnings import warn
|
||||
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestRemovedIn7Warning
|
||||
from _pytest.warning_types import PytestRemovedIn8Warning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
@@ -23,6 +24,18 @@ DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
}
|
||||
|
||||
|
||||
FILLFUNCARGS = UnformattedWarning(
|
||||
PytestRemovedIn7Warning,
|
||||
"{name} is deprecated, use "
|
||||
"function._request._fillfixtures() instead if you cannot avoid reaching into internals.",
|
||||
)
|
||||
|
||||
PYTEST_COLLECT_MODULE = UnformattedWarning(
|
||||
PytestRemovedIn7Warning,
|
||||
"pytest.collect.{name} was moved to pytest.{name}\n"
|
||||
"Please update to the new name.",
|
||||
)
|
||||
|
||||
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
|
||||
# * If you're in the future: "could have been".
|
||||
YIELD_FIXTURE = PytestDeprecationWarning(
|
||||
@@ -30,6 +43,20 @@ YIELD_FIXTURE = PytestDeprecationWarning(
|
||||
"Use @pytest.fixture instead; they are the same."
|
||||
)
|
||||
|
||||
MINUS_K_DASH = PytestRemovedIn7Warning(
|
||||
"The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead."
|
||||
)
|
||||
|
||||
MINUS_K_COLON = PytestRemovedIn7Warning(
|
||||
"The `-k 'expr:'` syntax to -k is deprecated.\n"
|
||||
"Please open an issue if you use this and want a replacement."
|
||||
)
|
||||
|
||||
WARNING_CAPTURED_HOOK = PytestRemovedIn7Warning(
|
||||
"The pytest_warning_captured is deprecated and will be removed in a future release.\n"
|
||||
"Please use pytest_warning_recorded instead."
|
||||
)
|
||||
|
||||
WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning(
|
||||
"The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n"
|
||||
"Please use pytest_load_initial_conftests hook instead."
|
||||
@@ -47,6 +74,11 @@ STRICT_OPTION = PytestRemovedIn8Warning(
|
||||
# This deprecation is never really meant to be removed.
|
||||
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
|
||||
|
||||
UNITTEST_SKIP_DURING_COLLECTION = PytestRemovedIn8Warning(
|
||||
"Raising unittest.SkipTest to skip tests during collection is deprecated. "
|
||||
"Use pytest.skip() instead."
|
||||
)
|
||||
|
||||
ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
|
||||
'pytest now uses argparse. "%default" should be changed to "%(default)s"',
|
||||
)
|
||||
|
||||
@@ -656,7 +656,7 @@ def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
||||
precision = 0 if fraction is None else len(fraction)
|
||||
if exponent is not None:
|
||||
precision -= int(exponent)
|
||||
if float(w.group()) == approx(float(g.group()), abs=10**-precision):
|
||||
if float(w.group()) == approx(float(g.group()), abs=10 ** -precision):
|
||||
# They're close enough. Replace the text we actually
|
||||
# got with the text we want, so that it will match when we
|
||||
# check the string literally.
|
||||
|
||||
@@ -52,6 +52,7 @@ from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import FILLFUNCARGS
|
||||
from _pytest.deprecated import YIELD_FIXTURE
|
||||
from _pytest.mark import Mark
|
||||
from _pytest.mark import ParameterSet
|
||||
@@ -72,6 +73,7 @@ if TYPE_CHECKING:
|
||||
from _pytest.scope import _ScopeName
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import CallSpec2
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Metafunc
|
||||
|
||||
|
||||
@@ -350,6 +352,41 @@ def reorder_items_atscope(
|
||||
return items_done
|
||||
|
||||
|
||||
def _fillfuncargs(function: "Function") -> None:
|
||||
"""Fill missing fixtures for a test function, old public API (deprecated)."""
|
||||
warnings.warn(FILLFUNCARGS.format(name="pytest._fillfuncargs()"), stacklevel=2)
|
||||
_fill_fixtures_impl(function)
|
||||
|
||||
|
||||
def fillfixtures(function: "Function") -> None:
|
||||
"""Fill missing fixtures for a test function (deprecated)."""
|
||||
warnings.warn(
|
||||
FILLFUNCARGS.format(name="_pytest.fixtures.fillfixtures()"), stacklevel=2
|
||||
)
|
||||
_fill_fixtures_impl(function)
|
||||
|
||||
|
||||
def _fill_fixtures_impl(function: "Function") -> None:
|
||||
"""Internal implementation to fill fixtures on the given function object."""
|
||||
try:
|
||||
request = function._request
|
||||
except AttributeError:
|
||||
# XXX this special code path is only expected to execute
|
||||
# with the oejskit plugin. It uses classes with funcargs
|
||||
# and we thus have to work a bit to allow this.
|
||||
fm = function.session._fixturemanager
|
||||
assert function.parent is not None
|
||||
fi = fm.getfixtureinfo(function.parent, function.obj, None)
|
||||
function._fixtureinfo = fi
|
||||
request = function._request = FixtureRequest(function, _ispytest=True)
|
||||
fm.session._setupstate.setup(function)
|
||||
request._fillfixtures()
|
||||
# Prune out funcargs for jstests.
|
||||
function.funcargs = {name: function.funcargs[name] for name in fi.argnames}
|
||||
else:
|
||||
request._fillfixtures()
|
||||
|
||||
|
||||
def get_direct_param_fixture_func(request):
|
||||
return request.param
|
||||
|
||||
@@ -597,17 +634,8 @@ class FixtureRequest:
|
||||
funcitem = self._pyfuncitem
|
||||
scope = fixturedef._scope
|
||||
try:
|
||||
callspec = funcitem.callspec
|
||||
except AttributeError:
|
||||
callspec = None
|
||||
if callspec is not None and argname in callspec.params:
|
||||
param = callspec.params[argname]
|
||||
param_index = callspec.indices[argname]
|
||||
# If a parametrize invocation set a scope it will override
|
||||
# the static scope defined with the fixture function.
|
||||
with suppress(KeyError):
|
||||
scope = callspec._arg2scope[argname]
|
||||
else:
|
||||
param = funcitem.callspec.getparam(argname)
|
||||
except (AttributeError, ValueError):
|
||||
param = NOTSET
|
||||
param_index = 0
|
||||
has_params = fixturedef.params is not None
|
||||
@@ -647,6 +675,12 @@ class FixtureRequest:
|
||||
)
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
else:
|
||||
param_index = funcitem.callspec.indices[argname]
|
||||
# If a parametrize invocation set a scope it will override
|
||||
# the static scope defined with the fixture function.
|
||||
with suppress(KeyError):
|
||||
scope = funcitem.callspec._arg2scope[argname]
|
||||
|
||||
subrequest = SubRequest(
|
||||
self, scope, param, param_index, fixturedef, _ispytest=True
|
||||
@@ -930,7 +964,7 @@ def _eval_scope_callable(
|
||||
|
||||
@final
|
||||
class FixtureDef(Generic[FixtureValue]):
|
||||
"""A container for a fixture definition."""
|
||||
"""A container for a factory definition."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -942,56 +976,33 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
params: Optional[Sequence[object]],
|
||||
unittest: bool = False,
|
||||
ids: Optional[
|
||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||
Union[
|
||||
Tuple[Union[None, str, float, int, bool], ...],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = None,
|
||||
) -> None:
|
||||
self._fixturemanager = fixturemanager
|
||||
# The "base" node ID for the fixture.
|
||||
#
|
||||
# This is a node ID prefix. A fixture is only available to a node (e.g.
|
||||
# a `Function` item) if the fixture's baseid is a parent of the node's
|
||||
# nodeid (see the `iterparentnodeids` function for what constitutes a
|
||||
# "parent" and a "prefix" in this context).
|
||||
#
|
||||
# For a fixture found in a Collector's object (e.g. a `Module`s module,
|
||||
# a `Class`'s class), the baseid is the Collector's nodeid.
|
||||
#
|
||||
# For a fixture found in a conftest plugin, the baseid is the conftest's
|
||||
# directory path relative to the rootdir.
|
||||
#
|
||||
# For other plugins, the baseid is the empty string (always matches).
|
||||
self.baseid = baseid or ""
|
||||
# Whether the fixture was found from a node or a conftest in the
|
||||
# collection tree. Will be false for fixtures defined in non-conftest
|
||||
# plugins.
|
||||
self.has_location = baseid is not None
|
||||
# The fixture factory function.
|
||||
self.func = func
|
||||
# The name by which the fixture may be requested.
|
||||
self.argname = argname
|
||||
if scope is None:
|
||||
scope = Scope.Function
|
||||
elif callable(scope):
|
||||
scope = _eval_scope_callable(scope, argname, fixturemanager.config)
|
||||
|
||||
if isinstance(scope, str):
|
||||
scope = Scope.from_user(
|
||||
scope, descr=f"Fixture '{func.__name__}'", where=baseid
|
||||
)
|
||||
self._scope = scope
|
||||
# If the fixture is directly parametrized, the parameter values.
|
||||
self.params: Optional[Sequence[object]] = params
|
||||
# If the fixture is directly parametrized, a tuple of explicit IDs to
|
||||
# assign to the parameter values, or a callable to generate an ID given
|
||||
# a parameter value.
|
||||
self.ids = ids
|
||||
# The names requested by the fixtures.
|
||||
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
|
||||
# Whether the fixture was collected from a unittest TestCase class.
|
||||
# Note that it really only makes sense to define autouse fixtures in
|
||||
# unittest TestCases.
|
||||
self.argnames: Tuple[str, ...] = getfuncargnames(
|
||||
func, name=argname, is_method=unittest
|
||||
)
|
||||
self.unittest = unittest
|
||||
# If the fixture was executed, the current value of the fixture.
|
||||
# Can change if the fixture is executed with different parameters.
|
||||
self.ids = ids
|
||||
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
|
||||
self._finalizers: List[Callable[[], object]] = []
|
||||
|
||||
@@ -1018,8 +1029,8 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
if exc:
|
||||
raise exc
|
||||
finally:
|
||||
ihook = request.node.ihook
|
||||
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.path)
|
||||
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
# Even if finalization fails, we invalidate the cached fixture
|
||||
# value and remove all finalizers because they may be bound methods
|
||||
# which will keep instances alive.
|
||||
@@ -1053,8 +1064,8 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
self.finish(request)
|
||||
assert self.cached_result is None
|
||||
|
||||
ihook = request.node.ihook
|
||||
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.path)
|
||||
result = hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
return result
|
||||
|
||||
def cache_key(self, request: SubRequest) -> object:
|
||||
@@ -1119,8 +1130,18 @@ def pytest_fixture_setup(
|
||||
|
||||
|
||||
def _ensure_immutable_ids(
|
||||
ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
|
||||
) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
],
|
||||
) -> Optional[
|
||||
Union[
|
||||
Tuple[Union[None, str, float, int, bool], ...],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
]:
|
||||
if ids is None:
|
||||
return None
|
||||
if callable(ids):
|
||||
@@ -1164,8 +1185,9 @@ class FixtureFunctionMarker:
|
||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
|
||||
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
|
||||
autouse: bool = False
|
||||
ids: Optional[
|
||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||
ids: Union[
|
||||
Tuple[Union[None, str, float, int, bool], ...],
|
||||
Callable[[Any], Optional[object]],
|
||||
] = attr.ib(
|
||||
default=None,
|
||||
converter=_ensure_immutable_ids,
|
||||
@@ -1206,7 +1228,10 @@ def fixture(
|
||||
params: Optional[Iterable[object]] = ...,
|
||||
autouse: bool = ...,
|
||||
ids: Optional[
|
||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = ...,
|
||||
name: Optional[str] = ...,
|
||||
) -> FixtureFunction:
|
||||
@@ -1221,7 +1246,10 @@ def fixture(
|
||||
params: Optional[Iterable[object]] = ...,
|
||||
autouse: bool = ...,
|
||||
ids: Optional[
|
||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = ...,
|
||||
name: Optional[str] = None,
|
||||
) -> FixtureFunctionMarker:
|
||||
@@ -1235,7 +1263,10 @@ def fixture(
|
||||
params: Optional[Iterable[object]] = None,
|
||||
autouse: bool = False,
|
||||
ids: Optional[
|
||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = None,
|
||||
name: Optional[str] = None,
|
||||
) -> Union[FixtureFunctionMarker, FixtureFunction]:
|
||||
@@ -1277,7 +1308,7 @@ def fixture(
|
||||
the fixture.
|
||||
|
||||
:param ids:
|
||||
Sequence of ids each corresponding to the params so that they are
|
||||
List of string ids each corresponding to the params so that they are
|
||||
part of the test id. If no ids are provided they will be generated
|
||||
automatically from the params.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Union
|
||||
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
from _pytest.deprecated import WARNING_CAPTURED_HOOK
|
||||
from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -33,10 +34,10 @@ if TYPE_CHECKING:
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.outcomes import Exit
|
||||
from _pytest.python import Class
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Metafunc
|
||||
from _pytest.python import Module
|
||||
from _pytest.python import PyCollector
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.runner import CallInfo
|
||||
@@ -359,7 +360,7 @@ def pytest_pycollect_makemodule(
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pycollect_makeitem(
|
||||
collector: Union["Module", "Class"], name: str, obj: object
|
||||
collector: "PyCollector", name: str, obj: object
|
||||
) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]:
|
||||
"""Return a custom item/collector for a Python object in a module, or None.
|
||||
|
||||
@@ -776,6 +777,41 @@ def pytest_terminal_summary(
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
|
||||
def pytest_warning_captured(
|
||||
warning_message: "warnings.WarningMessage",
|
||||
when: "Literal['config', 'collect', 'runtest']",
|
||||
item: Optional["Item"],
|
||||
location: Optional[Tuple[str, int, str]],
|
||||
) -> None:
|
||||
"""(**Deprecated**) Process a warning captured by the internal pytest warnings plugin.
|
||||
|
||||
.. deprecated:: 6.0
|
||||
|
||||
This hook is considered deprecated and will be removed in a future pytest version.
|
||||
Use :func:`pytest_warning_recorded` instead.
|
||||
|
||||
:param warnings.WarningMessage warning_message:
|
||||
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
|
||||
the same attributes as the parameters of :py:func:`warnings.showwarning`.
|
||||
|
||||
:param str when:
|
||||
Indicates when the warning was captured. Possible values:
|
||||
|
||||
* ``"config"``: during pytest configuration/initialization stage.
|
||||
* ``"collect"``: during test collection.
|
||||
* ``"runtest"``: during test execution.
|
||||
|
||||
:param pytest.Item|None item:
|
||||
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
|
||||
|
||||
:param tuple location:
|
||||
When available, holds information about the execution context of the captured
|
||||
warning (filename, linenumber, function). ``function`` evaluates to <module>
|
||||
when the execution context is at the module level.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_warning_recorded(
|
||||
warning_message: "warnings.WarningMessage",
|
||||
|
||||
@@ -92,7 +92,7 @@ class _NodeReporter:
|
||||
self.xml = xml
|
||||
self.add_stats = self.xml.add_stats
|
||||
self.family = self.xml.family
|
||||
self.duration = 0.0
|
||||
self.duration = 0
|
||||
self.properties: List[Tuple[str, str]] = []
|
||||
self.nodes: List[ET.Element] = []
|
||||
self.attrs: Dict[str, str] = {}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Access and control log capturing."""
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from contextlib import nullcontext
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import AbstractSet
|
||||
@@ -14,7 +13,6 @@ from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -22,6 +20,7 @@ from _pytest import nodes
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.capture import CaptureManager
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import nullcontext
|
||||
from _pytest.config import _strtobool
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import create_terminal_writer
|
||||
@@ -35,11 +34,6 @@ from _pytest.main import Session
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
logging_StreamHandler = logging.StreamHandler[StringIO]
|
||||
else:
|
||||
logging_StreamHandler = logging.StreamHandler
|
||||
|
||||
|
||||
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
|
||||
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
|
||||
@@ -328,9 +322,11 @@ class catching_logs:
|
||||
root_logger.removeHandler(self.handler)
|
||||
|
||||
|
||||
class LogCaptureHandler(logging_StreamHandler):
|
||||
class LogCaptureHandler(logging.StreamHandler):
|
||||
"""A logging handler that stores log records and the log text."""
|
||||
|
||||
stream: StringIO
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Create a new log handler."""
|
||||
super().__init__(StringIO())
|
||||
@@ -625,11 +621,20 @@ class LoggingPlugin:
|
||||
if not fpath.parent.exists():
|
||||
fpath.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# https://github.com/python/mypy/issues/11193
|
||||
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
|
||||
old_stream = self.log_file_handler.setStream(stream)
|
||||
stream = fpath.open(mode="w", encoding="UTF-8")
|
||||
if sys.version_info >= (3, 7):
|
||||
old_stream = self.log_file_handler.setStream(stream)
|
||||
else:
|
||||
old_stream = self.log_file_handler.stream
|
||||
self.log_file_handler.acquire()
|
||||
try:
|
||||
self.log_file_handler.flush()
|
||||
self.log_file_handler.stream = stream
|
||||
finally:
|
||||
self.log_file_handler.release()
|
||||
if old_stream:
|
||||
old_stream.close()
|
||||
# https://github.com/python/typeshed/pull/5663
|
||||
old_stream.close() # type:ignore[attr-defined]
|
||||
|
||||
def _log_cli_enabled(self):
|
||||
"""Return whether live logging is enabled."""
|
||||
@@ -753,7 +758,7 @@ class _FileHandler(logging.FileHandler):
|
||||
pass
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging_StreamHandler):
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
"""A logging StreamHandler used by the live logging feature: it will
|
||||
write a newline before the first log message in each test.
|
||||
|
||||
|
||||
@@ -689,8 +689,9 @@ class Session(nodes.FSCollector):
|
||||
# No point in finding packages when collecting doctests.
|
||||
if not self.config.getoption("doctestmodules", False):
|
||||
pm = self.config.pluginmanager
|
||||
confcutdir = pm._confcutdir
|
||||
for parent in (argpath, *argpath.parents):
|
||||
if not pm._is_in_confcutdir(argpath):
|
||||
if confcutdir and parent in confcutdir.parents:
|
||||
break
|
||||
|
||||
if parent.is_dir():
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Generic mechanism for marking and selecting python functions."""
|
||||
import warnings
|
||||
from typing import AbstractSet
|
||||
from typing import Collection
|
||||
from typing import List
|
||||
@@ -22,6 +23,8 @@ from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import MINUS_K_COLON
|
||||
from _pytest.deprecated import MINUS_K_DASH
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -186,14 +189,27 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
|
||||
if not keywordexpr:
|
||||
return
|
||||
|
||||
if keywordexpr.startswith("-"):
|
||||
# To be removed in pytest 8.0.0.
|
||||
warnings.warn(MINUS_K_DASH, stacklevel=2)
|
||||
keywordexpr = "not " + keywordexpr[1:]
|
||||
selectuntil = False
|
||||
if keywordexpr[-1:] == ":":
|
||||
# To be removed in pytest 8.0.0.
|
||||
warnings.warn(MINUS_K_COLON, stacklevel=2)
|
||||
selectuntil = True
|
||||
keywordexpr = keywordexpr[:-1]
|
||||
|
||||
expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'")
|
||||
|
||||
remaining = []
|
||||
deselected = []
|
||||
for colitem in items:
|
||||
if not expr.evaluate(KeywordMatcher.from_item(colitem)):
|
||||
if keywordexpr and not expr.evaluate(KeywordMatcher.from_item(colitem)):
|
||||
deselected.append(colitem)
|
||||
else:
|
||||
if selectuntil:
|
||||
keywordexpr = None
|
||||
remaining.append(colitem)
|
||||
|
||||
if deselected:
|
||||
|
||||
@@ -72,11 +72,16 @@ def get_empty_parameterset_mark(
|
||||
return mark
|
||||
|
||||
|
||||
class ParameterSet(NamedTuple):
|
||||
values: Sequence[Union[object, NotSetType]]
|
||||
marks: Collection[Union["MarkDecorator", "Mark"]]
|
||||
id: Optional[str]
|
||||
|
||||
class ParameterSet(
|
||||
NamedTuple(
|
||||
"ParameterSet",
|
||||
[
|
||||
("values", Sequence[Union[object, NotSetType]]),
|
||||
("marks", Collection[Union["MarkDecorator", "Mark"]]),
|
||||
("id", Optional[str]),
|
||||
],
|
||||
)
|
||||
):
|
||||
@classmethod
|
||||
def param(
|
||||
cls,
|
||||
|
||||
@@ -55,7 +55,7 @@ def resolve(name: str) -> object:
|
||||
parts = name.split(".")
|
||||
|
||||
used = parts.pop(0)
|
||||
found: object = __import__(used)
|
||||
found = __import__(used)
|
||||
for part in parts:
|
||||
used += "." + part
|
||||
try:
|
||||
|
||||
@@ -539,9 +539,6 @@ def import_path(
|
||||
ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
|
||||
if ignore != "1":
|
||||
module_file = mod.__file__
|
||||
if module_file is None:
|
||||
raise ImportPathMismatchError(module_name, module_file, path)
|
||||
|
||||
if module_file.endswith((".pyc", ".pyo")):
|
||||
module_file = module_file[:-1]
|
||||
if module_file.endswith(os.path.sep + "__init__.py"):
|
||||
@@ -565,6 +562,7 @@ if sys.platform.startswith("win"):
|
||||
def _is_same(f1: str, f2: str) -> bool:
|
||||
return Path(f1) == Path(f2) or os.path.samefile(f1, f2)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def _is_same(f1: str, f2: str) -> bool:
|
||||
@@ -603,20 +601,11 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
|
||||
module_parts = module_name.split(".")
|
||||
while module_name:
|
||||
if module_name not in modules:
|
||||
try:
|
||||
# If sys.meta_path is empty, calling import_module will issue
|
||||
# a warning and raise ModuleNotFoundError. To avoid the
|
||||
# warning, we check sys.meta_path explicitly and raise the error
|
||||
# ourselves to fall back to creating a dummy module.
|
||||
if not sys.meta_path:
|
||||
raise ModuleNotFoundError
|
||||
importlib.import_module(module_name)
|
||||
except ModuleNotFoundError:
|
||||
module = ModuleType(
|
||||
module_name,
|
||||
doc="Empty module created by pytest's importmode=importlib.",
|
||||
)
|
||||
modules[module_name] = module
|
||||
module = ModuleType(
|
||||
module_name,
|
||||
doc="Empty module created by pytest's importmode=importlib.",
|
||||
)
|
||||
modules[module_name] = module
|
||||
module_parts.pop(-1)
|
||||
module_name = ".".join(module_parts)
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class LsofFdLeakChecker:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True,
|
||||
text=True,
|
||||
universal_newlines=True,
|
||||
).stdout
|
||||
|
||||
def isopen(line: str) -> bool:
|
||||
@@ -477,9 +477,7 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
|
||||
|
||||
|
||||
@fixture
|
||||
def pytester(
|
||||
request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
|
||||
) -> "Pytester":
|
||||
def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester":
|
||||
"""
|
||||
Facilities to write tests/configuration files, execute pytest in isolation, and match
|
||||
against expected output, perfect for black-box testing of pytest plugins.
|
||||
@@ -490,7 +488,7 @@ def pytester(
|
||||
It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
|
||||
fixture but provides methods which aid in testing pytest itself.
|
||||
"""
|
||||
return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True)
|
||||
return Pytester(request, tmp_path_factory, _ispytest=True)
|
||||
|
||||
|
||||
@fixture
|
||||
@@ -685,7 +683,6 @@ class Pytester:
|
||||
self,
|
||||
request: FixtureRequest,
|
||||
tmp_path_factory: TempPathFactory,
|
||||
monkeypatch: MonkeyPatch,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
@@ -709,7 +706,7 @@ class Pytester:
|
||||
self._method = self._request.config.getoption("--runpytest")
|
||||
self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
|
||||
|
||||
self._monkeypatch = mp = monkeypatch
|
||||
self._monkeypatch = mp = MonkeyPatch()
|
||||
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
|
||||
# Ensure no unexpected caching via tox.
|
||||
mp.delenv("TOX_ENV_DIR", raising=False)
|
||||
@@ -741,6 +738,7 @@ class Pytester:
|
||||
self._sys_modules_snapshot.restore()
|
||||
self._sys_path_snapshot.restore()
|
||||
self._cwd_snapshot.restore()
|
||||
self._monkeypatch.undo()
|
||||
|
||||
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
|
||||
# Some zope modules used by twisted-related tests keep internal state
|
||||
|
||||
@@ -224,15 +224,11 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
|
||||
|
||||
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_pycollect_makeitem(
|
||||
collector: Union["Module", "Class"], name: str, obj: object
|
||||
) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]:
|
||||
assert isinstance(collector, (Class, Module)), type(collector)
|
||||
def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
|
||||
# Nothing was collected elsewhere, let's do it here.
|
||||
if safe_isclass(obj):
|
||||
if collector.istestclass(obj, name):
|
||||
klass: Class = Class.from_parent(collector, name=name, obj=obj)
|
||||
return klass
|
||||
return Class.from_parent(collector, name=name, obj=obj)
|
||||
elif collector.istestfunction(obj, name):
|
||||
# mock seems to store unbound methods (issue473), normalize it.
|
||||
obj = getattr(obj, "__func__", obj)
|
||||
@@ -251,16 +247,15 @@ def pytest_pycollect_makeitem(
|
||||
)
|
||||
elif getattr(obj, "__test__", True):
|
||||
if is_generator(obj):
|
||||
res: Function = Function.from_parent(collector, name=name)
|
||||
res = Function.from_parent(collector, name=name)
|
||||
reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
|
||||
name=name
|
||||
)
|
||||
res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
|
||||
res.warn(PytestCollectionWarning(reason))
|
||||
return res
|
||||
else:
|
||||
return list(collector._genfunctions(name, obj))
|
||||
return None
|
||||
res = list(collector._genfunctions(name, obj))
|
||||
return res
|
||||
|
||||
|
||||
class PyobjMixin(nodes.Node):
|
||||
@@ -303,9 +298,6 @@ class PyobjMixin(nodes.Node):
|
||||
# used to avoid Function marker duplication
|
||||
if self._ALLOW_MARKERS:
|
||||
self.own_markers.extend(get_unpacked_marks(self.obj))
|
||||
# This assumes that `obj` is called before there is a chance
|
||||
# to add custom keys to `self.keywords`, so no fear of overriding.
|
||||
self.keywords.update((mark.name, mark) for mark in self.own_markers)
|
||||
return obj
|
||||
|
||||
@obj.setter
|
||||
@@ -343,7 +335,6 @@ class PyobjMixin(nodes.Node):
|
||||
if isinstance(compat_co_firstlineno, int):
|
||||
# nose compatibility
|
||||
file_path = sys.modules[obj.__module__].__file__
|
||||
assert file_path is not None
|
||||
if file_path.endswith(".pyc"):
|
||||
file_path = file_path[:-1]
|
||||
path: Union["os.PathLike[str]", str] = file_path
|
||||
@@ -428,7 +419,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
for basecls in self.obj.__mro__:
|
||||
dicts.append(basecls.__dict__)
|
||||
|
||||
# In each class, nodes should be definition ordered.
|
||||
# In each class, nodes should be definition ordered. Since Python 3.6,
|
||||
# __dict__ is definition ordered.
|
||||
seen: Set[str] = set()
|
||||
dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = []
|
||||
@@ -905,7 +896,11 @@ class InstanceDummy:
|
||||
only to ignore it; this dummy class keeps them working. This will be removed
|
||||
in pytest 8."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Note: module __getattr__ only works on Python>=3.7. Unfortunately
|
||||
# we can't provide this deprecation warning on Python 3.6.
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "Instance":
|
||||
warnings.warn(INSTANCE_COLLECTOR, 2)
|
||||
@@ -927,159 +922,6 @@ def hasnew(obj: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, auto_attribs=True, slots=True)
|
||||
class IdMaker:
|
||||
"""Make IDs for a parametrization."""
|
||||
|
||||
# The argnames of the parametrization.
|
||||
argnames: Sequence[str]
|
||||
# The ParameterSets of the parametrization.
|
||||
parametersets: Sequence[ParameterSet]
|
||||
# Optionally, a user-provided callable to make IDs for parameters in a
|
||||
# ParameterSet.
|
||||
idfn: Optional[Callable[[Any], Optional[object]]]
|
||||
# Optionally, explicit IDs for ParameterSets by index.
|
||||
ids: Optional[Sequence[Optional[object]]]
|
||||
# Optionally, the pytest config.
|
||||
# Used for controlling ASCII escaping, and for calling the
|
||||
# :hook:`pytest_make_parametrize_id` hook.
|
||||
config: Optional[Config]
|
||||
# Optionally, the ID of the node being parametrized.
|
||||
# Used only for clearer error messages.
|
||||
nodeid: Optional[str]
|
||||
# Optionally, the ID of the function being parametrized.
|
||||
# Used only for clearer error messages.
|
||||
func_name: Optional[str]
|
||||
|
||||
def make_unique_parameterset_ids(self) -> List[str]:
|
||||
"""Make a unique identifier for each ParameterSet, that may be used to
|
||||
identify the parametrization in a node ID.
|
||||
|
||||
Format is <prm_1_token>-...-<prm_n_token>[counter], where prm_x_token is
|
||||
- user-provided id, if given
|
||||
- else an id derived from the value, applicable for certain types
|
||||
- else <argname><parameterset index>
|
||||
The counter suffix is appended only in case a string wouldn't be unique
|
||||
otherwise.
|
||||
"""
|
||||
resolved_ids = list(self._resolve_ids())
|
||||
# All IDs must be unique!
|
||||
if len(resolved_ids) != len(set(resolved_ids)):
|
||||
# Record the number of occurrences of each ID.
|
||||
id_counts = Counter(resolved_ids)
|
||||
# Map the ID to its next suffix.
|
||||
id_suffixes: Dict[str, int] = defaultdict(int)
|
||||
# Suffix non-unique IDs to make them unique.
|
||||
for index, id in enumerate(resolved_ids):
|
||||
if id_counts[id] > 1:
|
||||
resolved_ids[index] = f"{id}{id_suffixes[id]}"
|
||||
id_suffixes[id] += 1
|
||||
return resolved_ids
|
||||
|
||||
def _resolve_ids(self) -> Iterable[str]:
|
||||
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
|
||||
for idx, parameterset in enumerate(self.parametersets):
|
||||
if parameterset.id is not None:
|
||||
# ID provided directly - pytest.param(..., id="...")
|
||||
yield parameterset.id
|
||||
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
|
||||
# ID provided in the IDs list - parametrize(..., ids=[...]).
|
||||
yield self._idval_from_value_required(self.ids[idx], idx)
|
||||
else:
|
||||
# ID not provided - generate it.
|
||||
yield "-".join(
|
||||
self._idval(val, argname, idx)
|
||||
for val, argname in zip(parameterset.values, self.argnames)
|
||||
)
|
||||
|
||||
def _idval(self, val: object, argname: str, idx: int) -> str:
|
||||
"""Make an ID for a parameter in a ParameterSet."""
|
||||
idval = self._idval_from_function(val, argname, idx)
|
||||
if idval is not None:
|
||||
return idval
|
||||
idval = self._idval_from_hook(val, argname)
|
||||
if idval is not None:
|
||||
return idval
|
||||
idval = self._idval_from_value(val)
|
||||
if idval is not None:
|
||||
return idval
|
||||
return self._idval_from_argname(argname, idx)
|
||||
|
||||
def _idval_from_function(
|
||||
self, val: object, argname: str, idx: int
|
||||
) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet using the
|
||||
user-provided id callable, if given."""
|
||||
if self.idfn is None:
|
||||
return None
|
||||
try:
|
||||
id = self.idfn(val)
|
||||
except Exception as e:
|
||||
prefix = f"{self.nodeid}: " if self.nodeid is not None else ""
|
||||
msg = "error raised while trying to determine id of parameter '{}' at position {}"
|
||||
msg = prefix + msg.format(argname, idx)
|
||||
raise ValueError(msg) from e
|
||||
if id is None:
|
||||
return None
|
||||
return self._idval_from_value(id)
|
||||
|
||||
def _idval_from_hook(self, val: object, argname: str) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet by calling the
|
||||
:hook:`pytest_make_parametrize_id` hook."""
|
||||
if self.config:
|
||||
id: Optional[str] = self.config.hook.pytest_make_parametrize_id(
|
||||
config=self.config, val=val, argname=argname
|
||||
)
|
||||
return id
|
||||
return None
|
||||
|
||||
def _idval_from_value(self, val: object) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet from its value,
|
||||
if the value type is supported."""
|
||||
if isinstance(val, STRING_TYPES):
|
||||
return _ascii_escaped_by_config(val, self.config)
|
||||
elif val is None or isinstance(val, (float, int, bool, complex)):
|
||||
return str(val)
|
||||
elif isinstance(val, Pattern):
|
||||
return ascii_escaped(val.pattern)
|
||||
elif val is NOTSET:
|
||||
# Fallback to default. Note that NOTSET is an enum.Enum.
|
||||
pass
|
||||
elif isinstance(val, enum.Enum):
|
||||
return str(val)
|
||||
elif isinstance(getattr(val, "__name__", None), str):
|
||||
# Name of a class, function, module, etc.
|
||||
name: str = getattr(val, "__name__")
|
||||
return name
|
||||
return None
|
||||
|
||||
def _idval_from_value_required(self, val: object, idx: int) -> str:
|
||||
"""Like _idval_from_value(), but fails if the type is not supported."""
|
||||
id = self._idval_from_value(val)
|
||||
if id is not None:
|
||||
return id
|
||||
|
||||
# Fail.
|
||||
if self.func_name is not None:
|
||||
prefix = f"In {self.func_name}: "
|
||||
elif self.nodeid is not None:
|
||||
prefix = f"In {self.nodeid}: "
|
||||
else:
|
||||
prefix = ""
|
||||
msg = (
|
||||
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
|
||||
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
@staticmethod
|
||||
def _idval_from_argname(argname: str, idx: int) -> str:
|
||||
"""Make an ID for a parameter in a ParameterSet from the argument name
|
||||
and the index of the ParameterSet."""
|
||||
return str(argname) + str(idx)
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, slots=True, auto_attribs=True)
|
||||
class CallSpec2:
|
||||
@@ -1202,7 +1044,10 @@ class Metafunc:
|
||||
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
|
||||
indirect: Union[bool, Sequence[str]] = False,
|
||||
ids: Optional[
|
||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = None,
|
||||
scope: "Optional[_ScopeName]" = None,
|
||||
*,
|
||||
@@ -1269,7 +1114,7 @@ class Metafunc:
|
||||
It will also override any fixture-function defined scope, allowing
|
||||
to set a dynamic scope using test context or configuration.
|
||||
"""
|
||||
argnames, parametersets = ParameterSet._for_parametrize(
|
||||
argnames, parameters = ParameterSet._for_parametrize(
|
||||
argnames,
|
||||
argvalues,
|
||||
self.function,
|
||||
@@ -1301,8 +1146,8 @@ class Metafunc:
|
||||
if generated_ids is not None:
|
||||
ids = generated_ids
|
||||
|
||||
ids = self._resolve_parameter_set_ids(
|
||||
argnames, ids, parametersets, nodeid=self.definition.nodeid
|
||||
ids = self._resolve_arg_ids(
|
||||
argnames, ids, parameters, nodeid=self.definition.nodeid
|
||||
)
|
||||
|
||||
# Store used (possibly generated) ids with parametrize Marks.
|
||||
@@ -1314,9 +1159,7 @@ class Metafunc:
|
||||
# of all calls.
|
||||
newcalls = []
|
||||
for callspec in self._calls or [CallSpec2()]:
|
||||
for param_index, (param_id, param_set) in enumerate(
|
||||
zip(ids, parametersets)
|
||||
):
|
||||
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
|
||||
newcallspec = callspec.setmulti(
|
||||
valtypes=arg_values_types,
|
||||
argnames=argnames,
|
||||
@@ -1329,29 +1172,27 @@ class Metafunc:
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def _resolve_parameter_set_ids(
|
||||
def _resolve_arg_ids(
|
||||
self,
|
||||
argnames: Sequence[str],
|
||||
ids: Optional[
|
||||
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
],
|
||||
parametersets: Sequence[ParameterSet],
|
||||
parameters: Sequence[ParameterSet],
|
||||
nodeid: str,
|
||||
) -> List[str]:
|
||||
"""Resolve the actual ids for the given parameter sets.
|
||||
"""Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
|
||||
to ``parametrize``.
|
||||
|
||||
:param argnames:
|
||||
Argument names passed to ``parametrize()``.
|
||||
:param ids:
|
||||
The `ids` parameter of the ``parametrize()`` call (see docs).
|
||||
:param parametersets:
|
||||
The parameter sets, each containing a set of values corresponding
|
||||
to ``argnames``.
|
||||
:param nodeid str:
|
||||
The nodeid of the definition item that generated this
|
||||
parametrization.
|
||||
:returns:
|
||||
List with ids for each parameter set given.
|
||||
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||
:param ids: The ids parameter of the parametrized call (see docs).
|
||||
:param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``.
|
||||
:param str str: The nodeid of the item that generated this parametrized call.
|
||||
:rtype: List[str]
|
||||
:returns: The list of ids for each argname given.
|
||||
"""
|
||||
if ids is None:
|
||||
idfn = None
|
||||
@@ -1361,24 +1202,15 @@ class Metafunc:
|
||||
ids_ = None
|
||||
else:
|
||||
idfn = None
|
||||
ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
|
||||
id_maker = IdMaker(
|
||||
argnames,
|
||||
parametersets,
|
||||
idfn,
|
||||
ids_,
|
||||
self.config,
|
||||
nodeid=nodeid,
|
||||
func_name=self.function.__name__,
|
||||
)
|
||||
return id_maker.make_unique_parameterset_ids()
|
||||
ids_ = self._validate_ids(ids, parameters, self.function.__name__)
|
||||
return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid)
|
||||
|
||||
def _validate_ids(
|
||||
self,
|
||||
ids: Iterable[Optional[object]],
|
||||
parametersets: Sequence[ParameterSet],
|
||||
ids: Iterable[Union[None, str, float, int, bool]],
|
||||
parameters: Sequence[ParameterSet],
|
||||
func_name: str,
|
||||
) -> List[Optional[object]]:
|
||||
) -> List[Union[None, str]]:
|
||||
try:
|
||||
num_ids = len(ids) # type: ignore[arg-type]
|
||||
except TypeError:
|
||||
@@ -1386,14 +1218,29 @@ class Metafunc:
|
||||
iter(ids)
|
||||
except TypeError as e:
|
||||
raise TypeError("ids must be a callable or an iterable") from e
|
||||
num_ids = len(parametersets)
|
||||
num_ids = len(parameters)
|
||||
|
||||
# num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
|
||||
if num_ids != len(parametersets) and num_ids != 0:
|
||||
if num_ids != len(parameters) and num_ids != 0:
|
||||
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
|
||||
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
|
||||
fail(msg.format(func_name, len(parameters), num_ids), pytrace=False)
|
||||
|
||||
return list(itertools.islice(ids, num_ids))
|
||||
new_ids = []
|
||||
for idx, id_value in enumerate(itertools.islice(ids, num_ids)):
|
||||
if id_value is None or isinstance(id_value, str):
|
||||
new_ids.append(id_value)
|
||||
elif isinstance(id_value, (float, int, bool)):
|
||||
new_ids.append(str(id_value))
|
||||
else:
|
||||
msg = ( # type: ignore[unreachable]
|
||||
"In {}: ids must be list of string/float/int/bool, "
|
||||
"found: {} (type: {!r}) at index {}"
|
||||
)
|
||||
fail(
|
||||
msg.format(func_name, saferepr(id_value), type(id_value), idx),
|
||||
pytrace=False,
|
||||
)
|
||||
return new_ids
|
||||
|
||||
def _resolve_arg_value_types(
|
||||
self,
|
||||
@@ -1513,6 +1360,105 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
|
||||
return val if escape_option else ascii_escaped(val) # type: ignore
|
||||
|
||||
|
||||
def _idval(
|
||||
val: object,
|
||||
argname: str,
|
||||
idx: int,
|
||||
idfn: Optional[Callable[[Any], Optional[object]]],
|
||||
nodeid: Optional[str],
|
||||
config: Optional[Config],
|
||||
) -> str:
|
||||
if idfn:
|
||||
try:
|
||||
generated_id = idfn(val)
|
||||
if generated_id is not None:
|
||||
val = generated_id
|
||||
except Exception as e:
|
||||
prefix = f"{nodeid}: " if nodeid is not None else ""
|
||||
msg = "error raised while trying to determine id of parameter '{}' at position {}"
|
||||
msg = prefix + msg.format(argname, idx)
|
||||
raise ValueError(msg) from e
|
||||
elif config:
|
||||
hook_id: Optional[str] = config.hook.pytest_make_parametrize_id(
|
||||
config=config, val=val, argname=argname
|
||||
)
|
||||
if hook_id:
|
||||
return hook_id
|
||||
|
||||
if isinstance(val, STRING_TYPES):
|
||||
return _ascii_escaped_by_config(val, config)
|
||||
elif val is None or isinstance(val, (float, int, bool, complex)):
|
||||
return str(val)
|
||||
elif isinstance(val, Pattern):
|
||||
return ascii_escaped(val.pattern)
|
||||
elif val is NOTSET:
|
||||
# Fallback to default. Note that NOTSET is an enum.Enum.
|
||||
pass
|
||||
elif isinstance(val, enum.Enum):
|
||||
return str(val)
|
||||
elif isinstance(getattr(val, "__name__", None), str):
|
||||
# Name of a class, function, module, etc.
|
||||
name: str = getattr(val, "__name__")
|
||||
return name
|
||||
return str(argname) + str(idx)
|
||||
|
||||
|
||||
def _idvalset(
|
||||
idx: int,
|
||||
parameterset: ParameterSet,
|
||||
argnames: Iterable[str],
|
||||
idfn: Optional[Callable[[Any], Optional[object]]],
|
||||
ids: Optional[List[Union[None, str]]],
|
||||
nodeid: Optional[str],
|
||||
config: Optional[Config],
|
||||
) -> str:
|
||||
if parameterset.id is not None:
|
||||
return parameterset.id
|
||||
id = None if ids is None or idx >= len(ids) else ids[idx]
|
||||
if id is None:
|
||||
this_id = [
|
||||
_idval(val, argname, idx, idfn, nodeid=nodeid, config=config)
|
||||
for val, argname in zip(parameterset.values, argnames)
|
||||
]
|
||||
return "-".join(this_id)
|
||||
else:
|
||||
return _ascii_escaped_by_config(id, config)
|
||||
|
||||
|
||||
def idmaker(
|
||||
argnames: Iterable[str],
|
||||
parametersets: Iterable[ParameterSet],
|
||||
idfn: Optional[Callable[[Any], Optional[object]]] = None,
|
||||
ids: Optional[List[Union[None, str]]] = None,
|
||||
config: Optional[Config] = None,
|
||||
nodeid: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
resolved_ids = [
|
||||
_idvalset(
|
||||
valindex, parameterset, argnames, idfn, ids, config=config, nodeid=nodeid
|
||||
)
|
||||
for valindex, parameterset in enumerate(parametersets)
|
||||
]
|
||||
|
||||
# All IDs must be unique!
|
||||
unique_ids = set(resolved_ids)
|
||||
if len(unique_ids) != len(resolved_ids):
|
||||
|
||||
# Record the number of occurrences of each test ID.
|
||||
test_id_counts = Counter(resolved_ids)
|
||||
|
||||
# Map the test ID to its next suffix.
|
||||
test_id_suffixes: Dict[str, int] = defaultdict(int)
|
||||
|
||||
# Suffix non-unique IDs to make them unique.
|
||||
for index, test_id in enumerate(resolved_ids):
|
||||
if test_id_counts[test_id] > 1:
|
||||
resolved_ids[index] = f"{test_id}{test_id_suffixes[test_id]}"
|
||||
test_id_suffixes[test_id] += 1
|
||||
|
||||
return resolved_ids
|
||||
|
||||
|
||||
def _pretty_fixture_path(func) -> str:
|
||||
cwd = Path.cwd()
|
||||
loc = Path(getlocation(func, str(cwd)))
|
||||
@@ -1684,7 +1630,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||
config: Optional[Config] = None,
|
||||
callspec: Optional[CallSpec2] = None,
|
||||
callobj=NOTSET,
|
||||
keywords: Optional[Mapping[str, Any]] = None,
|
||||
keywords=None,
|
||||
session: Optional[Session] = None,
|
||||
fixtureinfo: Optional[FuncFixtureInfo] = None,
|
||||
originalname: Optional[str] = None,
|
||||
@@ -1705,20 +1651,31 @@ class Function(PyobjMixin, nodes.Item):
|
||||
# Note: when FunctionDefinition is introduced, we should change ``originalname``
|
||||
# to a readonly property that returns FunctionDefinition.name.
|
||||
|
||||
self.keywords.update(self.obj.__dict__)
|
||||
self.own_markers.extend(get_unpacked_marks(self.obj))
|
||||
if callspec:
|
||||
self.callspec = callspec
|
||||
self.own_markers.extend(callspec.marks)
|
||||
# this is total hostile and a mess
|
||||
# keywords are broken by design by now
|
||||
# this will be redeemed later
|
||||
for mark in callspec.marks:
|
||||
# feel free to cry, this was broken for years before
|
||||
# and keywords can't fix it per design
|
||||
self.keywords[mark.name] = mark
|
||||
self.own_markers.extend(normalize_mark_list(callspec.marks))
|
||||
if keywords:
|
||||
self.keywords.update(keywords)
|
||||
|
||||
# todo: this is a hell of a hack
|
||||
# https://github.com/pytest-dev/pytest/issues/4569
|
||||
# Note: the order of the updates is important here; indicates what
|
||||
# takes priority (ctor argument over function attributes over markers).
|
||||
# Take own_markers only; NodeKeywords handles parent traversal on its own.
|
||||
self.keywords.update((mark.name, mark) for mark in self.own_markers)
|
||||
self.keywords.update(self.obj.__dict__)
|
||||
if keywords:
|
||||
self.keywords.update(keywords)
|
||||
|
||||
self.keywords.update(
|
||||
{
|
||||
mark.name: True
|
||||
for mark in self.iter_markers()
|
||||
if mark.name not in self.keywords
|
||||
}
|
||||
)
|
||||
|
||||
if fixtureinfo is None:
|
||||
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import math
|
||||
import pprint
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Sized
|
||||
from decimal import Decimal
|
||||
from numbers import Complex
|
||||
@@ -9,6 +8,7 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
@@ -131,6 +131,7 @@ class ApproxBase:
|
||||
# a numeric type. For this reason, the default is to do nothing. The
|
||||
# classes that deal with sequences should reimplement this method to
|
||||
# raise if there are any non-numeric elements in the sequence.
|
||||
pass
|
||||
|
||||
|
||||
def _recursive_list_map(f, x):
|
||||
@@ -306,12 +307,12 @@ class ApproxMapping(ApproxBase):
|
||||
raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
|
||||
|
||||
|
||||
class ApproxSequenceLike(ApproxBase):
|
||||
class ApproxSequencelike(ApproxBase):
|
||||
"""Perform approximate comparisons where the expected value is a sequence of numbers."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
seq_type = type(self.expected)
|
||||
if seq_type not in (tuple, list):
|
||||
if seq_type not in (tuple, list, set):
|
||||
seq_type = list
|
||||
return "approx({!r})".format(
|
||||
seq_type(self._approx_scalar(x) for x in self.expected)
|
||||
@@ -515,7 +516,7 @@ class ApproxDecimal(ApproxScalar):
|
||||
|
||||
|
||||
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
"""Assert that two numbers (or two ordered sequences of numbers) are equal to each other
|
||||
"""Assert that two numbers (or two sets of numbers) are equal to each other
|
||||
within some tolerance.
|
||||
|
||||
Due to the :std:doc:`tutorial/floatingpoint`, numbers that we
|
||||
@@ -547,11 +548,16 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
>>> 0.1 + 0.2 == approx(0.3)
|
||||
True
|
||||
|
||||
The same syntax also works for ordered sequences of numbers::
|
||||
The same syntax also works for sequences of numbers::
|
||||
|
||||
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
|
||||
True
|
||||
|
||||
Dictionary *values*::
|
||||
|
||||
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
|
||||
True
|
||||
|
||||
``numpy`` arrays::
|
||||
|
||||
>>> import numpy as np # doctest: +SKIP
|
||||
@@ -564,20 +570,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
|
||||
True
|
||||
|
||||
Only ordered sequences are supported, because ``approx`` needs
|
||||
to infer the relative position of the sequences without ambiguity. This means
|
||||
``sets`` and other unordered sequences are not supported.
|
||||
|
||||
Finally, dictionary *values* can also be compared::
|
||||
|
||||
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
|
||||
True
|
||||
|
||||
The comparision will be true if both mappings have the same keys and their
|
||||
respective values match the expected tolerances.
|
||||
|
||||
**Tolerances**
|
||||
|
||||
By default, ``approx`` considers numbers within a relative tolerance of
|
||||
``1e-6`` (i.e. one part in a million) of its expected value to be equal.
|
||||
This treatment would lead to surprising results if the expected value was
|
||||
@@ -717,19 +709,12 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
expected = _as_numpy_array(expected)
|
||||
cls = ApproxNumpy
|
||||
elif (
|
||||
hasattr(expected, "__getitem__")
|
||||
isinstance(expected, Iterable)
|
||||
and isinstance(expected, Sized)
|
||||
# Type ignored because the error is wrong -- not unreachable.
|
||||
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
|
||||
):
|
||||
cls = ApproxSequenceLike
|
||||
elif (
|
||||
isinstance(expected, Collection)
|
||||
# Type ignored because the error is wrong -- not unreachable.
|
||||
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
|
||||
):
|
||||
msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
|
||||
raise TypeError(msg)
|
||||
cls = ApproxSequencelike
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
@@ -255,7 +254,7 @@ class TestReport(BaseReport):
|
||||
self,
|
||||
nodeid: str,
|
||||
location: Tuple[str, Optional[int], str],
|
||||
keywords: Mapping[str, Any],
|
||||
keywords,
|
||||
outcome: "Literal['passed', 'failed', 'skipped']",
|
||||
longrepr: Union[
|
||||
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import bdb
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
@@ -27,6 +28,7 @@ from _pytest._code.code import TerminalRepr
|
||||
from _pytest.compat import final
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import UNITTEST_SKIP_DURING_COLLECTION
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.nodes import Node
|
||||
@@ -377,6 +379,11 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||
# Type ignored because unittest is loaded dynamically.
|
||||
skip_exceptions.append(unittest.SkipTest) # type: ignore
|
||||
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
|
||||
if unittest is not None and isinstance(
|
||||
call.excinfo.value, unittest.SkipTest # type: ignore[attr-defined]
|
||||
):
|
||||
warnings.warn(UNITTEST_SKIP_DURING_COLLECTION, stacklevel=2)
|
||||
|
||||
outcome = "skipped"
|
||||
r_ = collector._repr_failure_py(call.excinfo, "line")
|
||||
assert isinstance(r_, ExceptionChainRepr), repr(r_)
|
||||
|
||||
@@ -542,21 +542,15 @@ class TerminalReporter:
|
||||
if not running_xdist:
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
if rep.skipped or hasattr(report, "wasxfail"):
|
||||
available_width = (
|
||||
(self._tw.fullwidth - self._tw.width_of_current_line)
|
||||
- len(" [100%]")
|
||||
- 1
|
||||
)
|
||||
reason = _get_raw_skip_reason(rep)
|
||||
if self.config.option.verbose < 2:
|
||||
available_width = (
|
||||
(self._tw.fullwidth - self._tw.width_of_current_line)
|
||||
- len(" [100%]")
|
||||
- 1
|
||||
)
|
||||
formatted_reason = _format_trimmed(
|
||||
" ({})", reason, available_width
|
||||
)
|
||||
else:
|
||||
formatted_reason = f" ({reason})"
|
||||
|
||||
if reason and formatted_reason is not None:
|
||||
self._tw.write(formatted_reason)
|
||||
reason_ = _format_trimmed(" ({})", reason, available_width)
|
||||
if reason and reason_ is not None:
|
||||
self._tw.write(reason_)
|
||||
if self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
@@ -663,7 +657,7 @@ class TerminalReporter:
|
||||
errors = len(self.stats.get("error", []))
|
||||
skipped = len(self.stats.get("skipped", []))
|
||||
deselected = len(self.stats.get("deselected", []))
|
||||
selected = self._numcollected - deselected
|
||||
selected = self._numcollected - errors - skipped - deselected
|
||||
line = "collected " if final else "collecting "
|
||||
line += (
|
||||
str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
|
||||
@@ -674,7 +668,7 @@ class TerminalReporter:
|
||||
line += " / %d deselected" % deselected
|
||||
if skipped:
|
||||
line += " / %d skipped" % skipped
|
||||
if self._numcollected > selected:
|
||||
if self._numcollected > selected > 0:
|
||||
line += " / %d selected" % selected
|
||||
if self.isatty:
|
||||
self.rewrite(line, bold=True, erase=True)
|
||||
|
||||
@@ -27,7 +27,7 @@ from _pytest.outcomes import skip
|
||||
from _pytest.outcomes import xfail
|
||||
from _pytest.python import Class
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Module
|
||||
from _pytest.python import PyCollector
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.scope import Scope
|
||||
|
||||
@@ -42,7 +42,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(
|
||||
collector: Union[Module, Class], name: str, obj: object
|
||||
collector: PyCollector, name: str, obj: object
|
||||
) -> Optional["UnitTestCase"]:
|
||||
# Has unittest been imported and is obj a subclass of its TestCase?
|
||||
try:
|
||||
|
||||
@@ -48,6 +48,13 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
@final
|
||||
class PytestRemovedIn7Warning(PytestDeprecationWarning):
|
||||
"""Warning class for features that will be removed in pytest 7."""
|
||||
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
@final
|
||||
class PytestRemovedIn8Warning(PytestDeprecationWarning):
|
||||
"""Warning class for features that will be removed in pytest 8."""
|
||||
|
||||
@@ -49,6 +49,8 @@ def catch_warnings_for_item(
|
||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||
|
||||
warnings.filterwarnings("error", category=pytest.PytestRemovedIn7Warning)
|
||||
|
||||
apply_warning_filters(config_filters, cmdline_filters)
|
||||
|
||||
# apply filters from "filterwarnings" marks
|
||||
@@ -61,6 +63,14 @@ def catch_warnings_for_item(
|
||||
yield
|
||||
|
||||
for warning_message in log:
|
||||
ihook.pytest_warning_captured.call_historic(
|
||||
kwargs=dict(
|
||||
warning_message=warning_message,
|
||||
when=when,
|
||||
item=item,
|
||||
location=None,
|
||||
)
|
||||
)
|
||||
ihook.pytest_warning_recorded.call_historic(
|
||||
kwargs=dict(
|
||||
warning_message=warning_message,
|
||||
@@ -81,23 +91,6 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
|
||||
warning_message.lineno,
|
||||
warning_message.line,
|
||||
)
|
||||
if warning_message.source is not None:
|
||||
try:
|
||||
import tracemalloc
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
tb = tracemalloc.get_object_traceback(warning_message.source)
|
||||
if tb is not None:
|
||||
formatted_tb = "\n".join(tb.format())
|
||||
# Use a leading new line to better separate the (large) output
|
||||
# from the traceback to the previous warning text.
|
||||
msg += f"\nObject allocated at:\n{formatted_tb}"
|
||||
else:
|
||||
# No need for a leading new line.
|
||||
url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
|
||||
msg += "Enable tracemalloc to get traceback where the object was allocated.\n"
|
||||
msg += f"See {url} for more info."
|
||||
return msg
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
"""pytest: unit and functional testing with Python."""
|
||||
from . import collect
|
||||
from _pytest import __version__
|
||||
from _pytest import version_tuple
|
||||
from _pytest._code import ExceptionInfo
|
||||
@@ -18,6 +19,7 @@ from _pytest.config import UsageError
|
||||
from _pytest.config.argparsing import OptionGroup
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.debugging import pytestPDB as __pytestPDB
|
||||
from _pytest.fixtures import _fillfuncargs
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureLookupError
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
@@ -68,6 +70,7 @@ from _pytest.warning_types import PytestCollectionWarning
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestExperimentalApiWarning
|
||||
from _pytest.warning_types import PytestRemovedIn7Warning
|
||||
from _pytest.warning_types import PytestRemovedIn8Warning
|
||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||
from _pytest.warning_types import PytestUnhandledThreadExceptionWarning
|
||||
@@ -80,12 +83,14 @@ set_trace = __pytestPDB.set_trace
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"_fillfuncargs",
|
||||
"approx",
|
||||
"Cache",
|
||||
"CallInfo",
|
||||
"CaptureFixture",
|
||||
"Class",
|
||||
"cmdline",
|
||||
"collect",
|
||||
"Collector",
|
||||
"CollectReport",
|
||||
"Config",
|
||||
@@ -126,6 +131,7 @@ __all__ = [
|
||||
"PytestConfigWarning",
|
||||
"PytestDeprecationWarning",
|
||||
"PytestExperimentalApiWarning",
|
||||
"PytestRemovedIn7Warning",
|
||||
"PytestRemovedIn8Warning",
|
||||
"Pytester",
|
||||
"PytestPluginManager",
|
||||
|
||||
38
src/pytest/collect.py
Normal file
38
src/pytest/collect.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import sys
|
||||
import warnings
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from _pytest.deprecated import PYTEST_COLLECT_MODULE
|
||||
|
||||
COLLECT_FAKEMODULE_ATTRIBUTES = [
|
||||
"Collector",
|
||||
"Module",
|
||||
"Function",
|
||||
"Session",
|
||||
"Item",
|
||||
"Class",
|
||||
"File",
|
||||
"_fillfuncargs",
|
||||
]
|
||||
|
||||
|
||||
class FakeCollectModule(ModuleType):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("pytest.collect")
|
||||
self.__all__ = list(COLLECT_FAKEMODULE_ATTRIBUTES)
|
||||
self.__pytest = pytest
|
||||
|
||||
def __dir__(self) -> List[str]:
|
||||
return dir(super()) + self.__all__
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name not in self.__all__:
|
||||
raise AttributeError(name)
|
||||
warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2)
|
||||
return getattr(pytest, name)
|
||||
|
||||
|
||||
sys.modules["pytest.collect"] = FakeCollectModule()
|
||||
@@ -1238,6 +1238,8 @@ def test_pdb_can_be_rewritten(pytester: Pytester) -> None:
|
||||
" def check():",
|
||||
"> assert 1 == 2",
|
||||
"E assert 1 == 2",
|
||||
"E +1",
|
||||
"E -2",
|
||||
"",
|
||||
"pdb.py:2: AssertionError",
|
||||
"*= 1 failed in *",
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# flake8: noqa
|
||||
# disable flake check on this file because some constructs are strange
|
||||
# or redundant on purpose and can't be disable on a line-by-line basis
|
||||
import ast
|
||||
import inspect
|
||||
import linecache
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from types import CodeType
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from _pytest._code import Code
|
||||
@@ -329,7 +332,8 @@ def test_findsource(monkeypatch) -> None:
|
||||
lines = ["if 1:\n", " def x():\n", " pass\n"]
|
||||
co = compile("".join(lines), filename, "exec")
|
||||
|
||||
monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename))
|
||||
# Type ignored because linecache.cache is private.
|
||||
monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename)) # type: ignore[attr-defined]
|
||||
|
||||
src, lineno = findsource(co)
|
||||
assert src is not None
|
||||
|
||||
@@ -2,6 +2,7 @@ import re
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from _pytest import deprecated
|
||||
@@ -10,6 +11,13 @@ from _pytest.pytester import Pytester
|
||||
from pytest import PytestDeprecationWarning
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore
|
||||
# false positive due to dynamic attribute
|
||||
def test_pytest_collect_module_deprecated(attribute) -> None:
|
||||
with pytest.warns(DeprecationWarning, match=attribute):
|
||||
getattr(pytest.collect, attribute)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS))
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_external_plugins_integrated(pytester: Pytester, plugin) -> None:
|
||||
@@ -20,6 +28,54 @@ def test_external_plugins_integrated(pytester: Pytester, plugin) -> None:
|
||||
pytester.parseconfig("-p", plugin)
|
||||
|
||||
|
||||
def test_fillfuncargs_is_deprecated() -> None:
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
match=re.escape(
|
||||
"pytest._fillfuncargs() is deprecated, use "
|
||||
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
|
||||
),
|
||||
):
|
||||
pytest._fillfuncargs(mock.Mock())
|
||||
|
||||
|
||||
def test_fillfixtures_is_deprecated() -> None:
|
||||
import _pytest.fixtures
|
||||
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
match=re.escape(
|
||||
"_pytest.fixtures.fillfixtures() is deprecated, use "
|
||||
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
|
||||
),
|
||||
):
|
||||
_pytest.fixtures.fillfixtures(mock.Mock())
|
||||
|
||||
|
||||
def test_minus_k_dash_is_deprecated(pytester: Pytester) -> None:
|
||||
threepass = pytester.makepyfile(
|
||||
test_threepass="""
|
||||
def test_one(): assert 1
|
||||
def test_two(): assert 1
|
||||
def test_three(): assert 1
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-k=-test_two", threepass)
|
||||
result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"])
|
||||
|
||||
|
||||
def test_minus_k_colon_is_deprecated(pytester: Pytester) -> None:
|
||||
threepass = pytester.makepyfile(
|
||||
test_threepass="""
|
||||
def test_one(): assert 1
|
||||
def test_two(): assert 1
|
||||
def test_three(): assert 1
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-k", "test_two:", threepass)
|
||||
result.stdout.fnmatch_lines(["*The `-k 'expr:'` syntax*deprecated*"])
|
||||
|
||||
|
||||
def test_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None:
|
||||
module = pytester.getmodulecol(
|
||||
"""
|
||||
@@ -86,6 +142,23 @@ def test_private_is_deprecated() -> None:
|
||||
PrivateInit(10, _ispytest=True)
|
||||
|
||||
|
||||
def test_raising_unittest_skiptest_during_collection_is_deprecated(
|
||||
pytester: Pytester,
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
raise unittest.SkipTest()
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*PytestRemovedIn8Warning: Raising unittest.SkipTest*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
|
||||
def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request):
|
||||
path = legacy_path(tmp_path)
|
||||
@@ -219,6 +292,10 @@ def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 7),
|
||||
reason="This deprecation can only be emitted on python>=3.7",
|
||||
)
|
||||
def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
||||
|
||||
def test_dataclasses() -> None:
|
||||
@dataclass
|
||||
class SimpleDataObject:
|
||||
field_a: int = field()
|
||||
field_b: str = field()
|
||||
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
return super().__eq__(__o)
|
||||
|
||||
left = SimpleDataObject(1, "b")
|
||||
right = SimpleDataObject(1, "c")
|
||||
|
||||
assert left == right
|
||||
@@ -56,7 +56,7 @@ def test_terminalwriter_not_unicode() -> None:
|
||||
file = io.TextIOWrapper(buffer, encoding="cp1252")
|
||||
tw = terminalwriter.TerminalWriter(file)
|
||||
tw.write("hello 🌀 wôrld אבג", flush=True)
|
||||
assert buffer.getvalue() == rb"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2"
|
||||
assert buffer.getvalue() == br"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2"
|
||||
|
||||
|
||||
win32 = int(sys.platform == "win32")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[pytest]
|
||||
addopts = --strict-markers
|
||||
asyncio_mode = strict
|
||||
filterwarnings =
|
||||
error::pytest.PytestWarning
|
||||
ignore:.*.fspath is deprecated and will be replaced by .*.path.*:pytest.PytestDeprecationWarning
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
anyio[curio,trio]==3.5.0
|
||||
django==4.0.3
|
||||
pytest-asyncio==0.18.2
|
||||
anyio[curio,trio]==3.4.0
|
||||
django==3.2.9
|
||||
pytest-asyncio==0.16.0
|
||||
pytest-bdd==5.0.0
|
||||
pytest-cov==3.0.0
|
||||
pytest-django==4.5.2
|
||||
pytest-django==4.5.1
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==3.1.1
|
||||
pytest-mock==3.7.0
|
||||
pytest-mock==3.6.1
|
||||
pytest-rerunfailures==10.2
|
||||
pytest-sugar==0.9.4
|
||||
pytest-trio==0.7.0
|
||||
pytest-twisted==1.13.4
|
||||
twisted==22.1.0
|
||||
twisted==21.7.0
|
||||
pytest-xvfb==2.0.0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import operator
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from fractions import Fraction
|
||||
@@ -771,7 +772,7 @@ class TestApprox:
|
||||
def test_expected_value_type_error(self, x, name):
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match=rf"pytest.approx\(\) does not support nested {name}:",
|
||||
match=fr"pytest.approx\(\) does not support nested {name}:",
|
||||
):
|
||||
approx(x)
|
||||
|
||||
@@ -809,6 +810,7 @@ class TestApprox:
|
||||
assert 1.0 != approx([None])
|
||||
assert None != approx([1.0]) # noqa: E711
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires ordered dicts")
|
||||
def test_nonnumeric_dict_repr(self):
|
||||
"""Dicts with non-numerics and infinites have no tolerances"""
|
||||
x1 = {"foo": 1.0000005, "bar": None, "foobar": inf}
|
||||
@@ -858,21 +860,13 @@ class TestApprox:
|
||||
assert approx(expected, rel=5e-7, abs=0) == actual
|
||||
assert approx(expected, rel=5e-8, abs=0) != actual
|
||||
|
||||
def test_generic_ordered_sequence(self):
|
||||
class MySequence:
|
||||
def __getitem__(self, i):
|
||||
return [1, 2, 3, 4][i]
|
||||
def test_generic_sized_iterable_object(self):
|
||||
class MySizedIterable:
|
||||
def __iter__(self):
|
||||
return iter([1, 2, 3, 4])
|
||||
|
||||
def __len__(self):
|
||||
return 4
|
||||
|
||||
expected = MySequence()
|
||||
assert [1, 2, 3, 4] == approx(expected, abs=1e-4)
|
||||
|
||||
expected_repr = "approx([1 ± 1.0e-06, 2 ± 2.0e-06, 3 ± 3.0e-06, 4 ± 4.0e-06])"
|
||||
assert repr(approx(expected)) == expected_repr
|
||||
|
||||
def test_allow_ordered_sequences_only(self) -> None:
|
||||
"""pytest.approx() should raise an error on unordered sequences (#9692)."""
|
||||
with pytest.raises(TypeError, match="only supports ordered sequences"):
|
||||
assert {1, 2, 3} == approx({1, 2, 3})
|
||||
expected = MySizedIterable()
|
||||
assert [1, 2, 3, 4] == approx(expected)
|
||||
|
||||
@@ -103,6 +103,10 @@ def test_getfuncargnames_staticmethod_partial():
|
||||
|
||||
@pytest.mark.pytester_example_path("fixtures/fill_fixtures")
|
||||
class TestFillFixtures:
|
||||
def test_fillfuncargs_exposed(self):
|
||||
# used by oejskit, kept for compatibility
|
||||
assert pytest._fillfuncargs == fixtures._fillfuncargs
|
||||
|
||||
def test_funcarg_lookupfails(self, pytester: Pytester) -> None:
|
||||
pytester.copy_example()
|
||||
result = pytester.runpytest() # "--collect-only")
|
||||
|
||||
@@ -1,10 +1,84 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from _pytest import runner
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest.fixtures import getfixturemarker
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.python import Function
|
||||
|
||||
|
||||
class TestOEJSKITSpecials:
|
||||
def test_funcarg_non_pycollectobj(
|
||||
self, pytester: Pytester, recwarn
|
||||
) -> None: # rough jstests usage
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if name == "MyClass":
|
||||
return MyCollector.from_parent(collector, name=name)
|
||||
class MyCollector(pytest.Collector):
|
||||
def reportinfo(self):
|
||||
return self.path, 3, "xyz"
|
||||
"""
|
||||
)
|
||||
modcol = pytester.getmodulecol(
|
||||
"""
|
||||
import pytest
|
||||
@pytest.fixture
|
||||
def arg1(request):
|
||||
return 42
|
||||
class MyClass(object):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
# this hook finds funcarg factories
|
||||
rep = runner.collect_one_node(collector=modcol)
|
||||
# TODO: Don't treat as Any.
|
||||
clscol: Any = rep.result[0]
|
||||
clscol.obj = lambda arg1: None
|
||||
clscol.funcargs = {}
|
||||
pytest._fillfuncargs(clscol)
|
||||
assert clscol.funcargs["arg1"] == 42
|
||||
|
||||
def test_autouse_fixture(
|
||||
self, pytester: Pytester, recwarn
|
||||
) -> None: # rough jstests usage
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if name == "MyClass":
|
||||
return MyCollector.from_parent(collector, name=name)
|
||||
class MyCollector(pytest.Collector):
|
||||
def reportinfo(self):
|
||||
return self.path, 3, "xyz"
|
||||
"""
|
||||
)
|
||||
modcol = pytester.getmodulecol(
|
||||
"""
|
||||
import pytest
|
||||
@pytest.fixture(autouse=True)
|
||||
def hello():
|
||||
pass
|
||||
@pytest.fixture
|
||||
def arg1(request):
|
||||
return 42
|
||||
class MyClass(object):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
# this hook finds funcarg factories
|
||||
rep = runner.collect_one_node(modcol)
|
||||
# TODO: Don't treat as Any.
|
||||
clscol: Any = rep.result[0]
|
||||
clscol.obj = lambda: None
|
||||
clscol.funcargs = {}
|
||||
pytest._fillfuncargs(clscol)
|
||||
assert not clscol.funcargs
|
||||
|
||||
|
||||
def test_wrapped_getfslineno() -> None:
|
||||
def func():
|
||||
pass
|
||||
|
||||
@@ -24,7 +24,8 @@ from _pytest.compat import getfuncargnames
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.python import IdMaker
|
||||
from _pytest.python import _idval
|
||||
from _pytest.python import idmaker
|
||||
from _pytest.scope import Scope
|
||||
|
||||
|
||||
@@ -106,8 +107,8 @@ class TestMetafunc:
|
||||
with pytest.raises(
|
||||
fail.Exception,
|
||||
match=(
|
||||
r"In func: ids contains unsupported value Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2. "
|
||||
r"Supported types are: .*"
|
||||
r"In func: ids must be list of string/float/int/bool, found:"
|
||||
r" Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2"
|
||||
),
|
||||
):
|
||||
metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type]
|
||||
@@ -285,7 +286,7 @@ class TestMetafunc:
|
||||
deadline=400.0
|
||||
) # very close to std deadline and CI boxes are not reliable in CPU power
|
||||
def test_idval_hypothesis(self, value) -> None:
|
||||
escaped = IdMaker([], [], None, None, None, None, None)._idval(value, "a", 6)
|
||||
escaped = _idval(value, "a", 6, None, nodeid=None, config=None)
|
||||
assert isinstance(escaped, str)
|
||||
escaped.encode("ascii")
|
||||
|
||||
@@ -307,10 +308,7 @@ class TestMetafunc:
|
||||
),
|
||||
]
|
||||
for val, expected in values:
|
||||
assert (
|
||||
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
|
||||
== expected
|
||||
)
|
||||
assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
|
||||
|
||||
def test_unicode_idval_with_config(self) -> None:
|
||||
"""Unit test for expected behavior to obtain ids with
|
||||
@@ -338,7 +336,7 @@ class TestMetafunc:
|
||||
("ação", MockConfig({option: False}), "a\\xe7\\xe3o"),
|
||||
]
|
||||
for val, config, expected in values:
|
||||
actual = IdMaker([], [], None, None, config, None, None)._idval(val, "a", 6)
|
||||
actual = _idval(val, "a", 6, None, nodeid=None, config=config)
|
||||
assert actual == expected
|
||||
|
||||
def test_bytes_idval(self) -> None:
|
||||
@@ -351,10 +349,7 @@ class TestMetafunc:
|
||||
("αρά".encode(), r"\xce\xb1\xcf\x81\xce\xac"),
|
||||
]
|
||||
for val, expected in values:
|
||||
assert (
|
||||
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
|
||||
== expected
|
||||
)
|
||||
assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected
|
||||
|
||||
def test_class_or_function_idval(self) -> None:
|
||||
"""Unit test for the expected behavior to obtain ids for parametrized
|
||||
@@ -368,10 +363,7 @@ class TestMetafunc:
|
||||
|
||||
values = [(TestClass, "TestClass"), (test_function, "test_function")]
|
||||
for val, expected in values:
|
||||
assert (
|
||||
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
|
||||
== expected
|
||||
)
|
||||
assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
|
||||
|
||||
def test_notset_idval(self) -> None:
|
||||
"""Test that a NOTSET value (used by an empty parameterset) generates
|
||||
@@ -379,47 +371,29 @@ class TestMetafunc:
|
||||
|
||||
Regression test for #7686.
|
||||
"""
|
||||
assert (
|
||||
IdMaker([], [], None, None, None, None, None)._idval(NOTSET, "a", 0) == "a0"
|
||||
)
|
||||
assert _idval(NOTSET, "a", 0, None, nodeid=None, config=None) == "a0"
|
||||
|
||||
def test_idmaker_autoname(self) -> None:
|
||||
"""#250"""
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[pytest.param("string", 1.0), pytest.param("st-ring", 2.0)],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
result = idmaker(
|
||||
("a", "b"), [pytest.param("string", 1.0), pytest.param("st-ring", 2.0)]
|
||||
)
|
||||
assert result == ["string-1.0", "st-ring-2.0"]
|
||||
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[pytest.param(object(), 1.0), pytest.param(object(), object())],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
result = idmaker(
|
||||
("a", "b"), [pytest.param(object(), 1.0), pytest.param(object(), object())]
|
||||
)
|
||||
assert result == ["a0-1.0", "a1-b1"]
|
||||
# unicode mixing, issue250
|
||||
result = IdMaker(
|
||||
("a", "b"), [pytest.param({}, b"\xc3\xb4")], None, None, None, None, None
|
||||
).make_unique_parameterset_ids()
|
||||
result = idmaker(("a", "b"), [pytest.param({}, b"\xc3\xb4")])
|
||||
assert result == ["a0-\\xc3\\xb4"]
|
||||
|
||||
def test_idmaker_with_bytes_regex(self) -> None:
|
||||
result = IdMaker(
|
||||
("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None, None
|
||||
).make_unique_parameterset_ids()
|
||||
result = idmaker(("a"), [pytest.param(re.compile(b"foo"), 1.0)])
|
||||
assert result == ["foo"]
|
||||
|
||||
def test_idmaker_native_strings(self) -> None:
|
||||
result = IdMaker(
|
||||
result = idmaker(
|
||||
("a", "b"),
|
||||
[
|
||||
pytest.param(1.0, -1.1),
|
||||
@@ -436,12 +410,7 @@ class TestMetafunc:
|
||||
pytest.param(b"\xc3\xb4", "other"),
|
||||
pytest.param(1.0j, -2.0j),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
)
|
||||
assert result == [
|
||||
"1.0--1.1",
|
||||
"2--202",
|
||||
@@ -459,7 +428,7 @@ class TestMetafunc:
|
||||
]
|
||||
|
||||
def test_idmaker_non_printable_characters(self) -> None:
|
||||
result = IdMaker(
|
||||
result = idmaker(
|
||||
("s", "n"),
|
||||
[
|
||||
pytest.param("\x00", 1),
|
||||
@@ -469,35 +438,23 @@ class TestMetafunc:
|
||||
pytest.param("\t", 5),
|
||||
pytest.param(b"\t", 6),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
)
|
||||
assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"]
|
||||
|
||||
def test_idmaker_manual_ids_must_be_printable(self) -> None:
|
||||
result = IdMaker(
|
||||
result = idmaker(
|
||||
("s",),
|
||||
[
|
||||
pytest.param("x00", id="hello \x00"),
|
||||
pytest.param("x05", id="hello \x05"),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
)
|
||||
assert result == ["hello \\x00", "hello \\x05"]
|
||||
|
||||
def test_idmaker_enum(self) -> None:
|
||||
enum = pytest.importorskip("enum")
|
||||
e = enum.Enum("Foo", "one, two")
|
||||
result = IdMaker(
|
||||
("a", "b"), [pytest.param(e.one, e.two)], None, None, None, None, None
|
||||
).make_unique_parameterset_ids()
|
||||
result = idmaker(("a", "b"), [pytest.param(e.one, e.two)])
|
||||
assert result == ["Foo.one-Foo.two"]
|
||||
|
||||
def test_idmaker_idfn(self) -> None:
|
||||
@@ -508,19 +465,15 @@ class TestMetafunc:
|
||||
return repr(val)
|
||||
return None
|
||||
|
||||
result = IdMaker(
|
||||
result = idmaker(
|
||||
("a", "b"),
|
||||
[
|
||||
pytest.param(10.0, IndexError()),
|
||||
pytest.param(20, KeyError()),
|
||||
pytest.param("three", [1, 2, 3]),
|
||||
],
|
||||
ids,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
idfn=ids,
|
||||
)
|
||||
assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"]
|
||||
|
||||
def test_idmaker_idfn_unique_names(self) -> None:
|
||||
@@ -529,19 +482,15 @@ class TestMetafunc:
|
||||
def ids(val: object) -> str:
|
||||
return "a"
|
||||
|
||||
result = IdMaker(
|
||||
result = idmaker(
|
||||
("a", "b"),
|
||||
[
|
||||
pytest.param(10.0, IndexError()),
|
||||
pytest.param(20, KeyError()),
|
||||
pytest.param("three", [1, 2, 3]),
|
||||
],
|
||||
ids,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
idfn=ids,
|
||||
)
|
||||
assert result == ["a-a0", "a-a1", "a-a2"]
|
||||
|
||||
def test_idmaker_with_idfn_and_config(self) -> None:
|
||||
@@ -571,15 +520,12 @@ class TestMetafunc:
|
||||
(MockConfig({option: False}), "a\\xe7\\xe3o"),
|
||||
]
|
||||
for config, expected in values:
|
||||
result = IdMaker(
|
||||
result = idmaker(
|
||||
("a",),
|
||||
[pytest.param("string")],
|
||||
lambda _: "ação",
|
||||
None,
|
||||
config,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
idfn=lambda _: "ação",
|
||||
config=config,
|
||||
)
|
||||
assert result == [expected]
|
||||
|
||||
def test_idmaker_with_ids_and_config(self) -> None:
|
||||
@@ -609,9 +555,12 @@ class TestMetafunc:
|
||||
(MockConfig({option: False}), "a\\xe7\\xe3o"),
|
||||
]
|
||||
for config, expected in values:
|
||||
result = IdMaker(
|
||||
("a",), [pytest.param("string")], None, ["ação"], config, None, None
|
||||
).make_unique_parameterset_ids()
|
||||
result = idmaker(
|
||||
("a",),
|
||||
[pytest.param("string")],
|
||||
ids=["ação"],
|
||||
config=config,
|
||||
)
|
||||
assert result == [expected]
|
||||
|
||||
def test_parametrize_ids_exception(self, pytester: Pytester) -> None:
|
||||
@@ -668,39 +617,23 @@ class TestMetafunc:
|
||||
)
|
||||
|
||||
def test_idmaker_with_ids(self) -> None:
|
||||
result = IdMaker(
|
||||
("a", "b"),
|
||||
[pytest.param(1, 2), pytest.param(3, 4)],
|
||||
None,
|
||||
["a", None],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
result = idmaker(
|
||||
("a", "b"), [pytest.param(1, 2), pytest.param(3, 4)], ids=["a", None]
|
||||
)
|
||||
assert result == ["a", "3-4"]
|
||||
|
||||
def test_idmaker_with_paramset_id(self) -> None:
|
||||
result = IdMaker(
|
||||
result = idmaker(
|
||||
("a", "b"),
|
||||
[pytest.param(1, 2, id="me"), pytest.param(3, 4, id="you")],
|
||||
None,
|
||||
["a", None],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
ids=["a", None],
|
||||
)
|
||||
assert result == ["me", "you"]
|
||||
|
||||
def test_idmaker_with_ids_unique_names(self) -> None:
|
||||
result = IdMaker(
|
||||
("a"),
|
||||
list(map(pytest.param, [1, 2, 3, 4, 5])),
|
||||
None,
|
||||
["a", "a", "b", "c", "b"],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).make_unique_parameterset_ids()
|
||||
result = idmaker(
|
||||
("a"), map(pytest.param, [1, 2, 3, 4, 5]), ids=["a", "a", "b", "c", "b"]
|
||||
)
|
||||
assert result == ["a0", "a1", "b0", "c", "b1"]
|
||||
|
||||
def test_parametrize_indirect(self) -> None:
|
||||
@@ -1339,7 +1272,7 @@ class TestMetafuncFunctional:
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, OSError()))
|
||||
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type))
|
||||
def test_ids_numbers(x,expected):
|
||||
assert x * 2 == expected
|
||||
"""
|
||||
@@ -1347,8 +1280,8 @@ class TestMetafuncFunctional:
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"In test_ids_numbers: ids contains unsupported value OSError() (type: <class 'OSError'>) at index 2. "
|
||||
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
|
||||
"In test_ids_numbers: ids must be list of string/float/int/bool,"
|
||||
" found: <class 'type'> (type: <class 'type'>) at index 2"
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class TestImportHookInstallation:
|
||||
"E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}",
|
||||
"E Omitting 1 identical items, use -vv to show",
|
||||
"E Differing items:",
|
||||
"E Use -v to get more diff",
|
||||
"E Use -v to get the full diff",
|
||||
]
|
||||
)
|
||||
# XXX: unstable output.
|
||||
@@ -376,7 +376,7 @@ class TestAssert_reprcompare:
|
||||
assert diff == [
|
||||
"b'spam' == b'eggs'",
|
||||
"At index 0 diff: b's' != b'e'",
|
||||
"Use -v to get more diff",
|
||||
"Use -v to get the full diff",
|
||||
]
|
||||
|
||||
def test_bytes_diff_verbose(self) -> None:
|
||||
@@ -444,19 +444,11 @@ class TestAssert_reprcompare:
|
||||
"""
|
||||
expl = callequal(left, right, verbose=0)
|
||||
assert expl is not None
|
||||
assert expl[-1] == "Use -v to get more diff"
|
||||
assert expl[-1] == "Use -v to get the full diff"
|
||||
verbose_expl = callequal(left, right, verbose=1)
|
||||
assert verbose_expl is not None
|
||||
assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip())
|
||||
|
||||
def test_iterable_quiet(self) -> None:
|
||||
expl = callequal([1, 2], [10, 2], verbose=-1)
|
||||
assert expl == [
|
||||
"[1, 2] == [10, 2]",
|
||||
"At index 0 diff: 1 != 10",
|
||||
"Use -v to get more diff",
|
||||
]
|
||||
|
||||
def test_iterable_full_diff_ci(
|
||||
self, monkeypatch: MonkeyPatch, pytester: Pytester
|
||||
) -> None:
|
||||
@@ -474,7 +466,7 @@ class TestAssert_reprcompare:
|
||||
|
||||
monkeypatch.delenv("CI", raising=False)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["E Use -v to get more diff"])
|
||||
result.stdout.fnmatch_lines(["E Use -v to get the full diff"])
|
||||
|
||||
def test_list_different_lengths(self) -> None:
|
||||
expl = callequal([0, 1], [0, 1, 2])
|
||||
@@ -707,6 +699,32 @@ class TestAssert_reprcompare:
|
||||
assert expl is not None
|
||||
assert len(expl) > 1
|
||||
|
||||
def test_repr_verbose(self) -> None:
|
||||
class Nums:
|
||||
def __init__(self, nums):
|
||||
self.nums = nums
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.nums)
|
||||
|
||||
list_x = list(range(5000))
|
||||
list_y = list(range(5000))
|
||||
list_y[len(list_y) // 2] = 3
|
||||
nums_x = Nums(list_x)
|
||||
nums_y = Nums(list_y)
|
||||
|
||||
assert callequal(nums_x, nums_y) is None
|
||||
|
||||
expl = callequal(nums_x, nums_y, verbose=1)
|
||||
assert expl is not None
|
||||
assert "+" + repr(nums_x) in expl
|
||||
assert "-" + repr(nums_y) in expl
|
||||
|
||||
expl = callequal(nums_x, nums_y, verbose=2)
|
||||
assert expl is not None
|
||||
assert "+" + repr(nums_x) in expl
|
||||
assert "-" + repr(nums_y) in expl
|
||||
|
||||
def test_list_bad_repr(self) -> None:
|
||||
class A:
|
||||
def __repr__(self):
|
||||
@@ -778,6 +796,7 @@ class TestAssert_reprcompare:
|
||||
|
||||
|
||||
class TestAssert_reprcompare_dataclass:
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example("dataclasses/test_compare_dataclasses.py")
|
||||
result = pytester.runpytest(p)
|
||||
@@ -796,6 +815,7 @@ class TestAssert_reprcompare_dataclass:
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_recursive_dataclasses(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py")
|
||||
result = pytester.runpytest(p)
|
||||
@@ -814,6 +834,7 @@ class TestAssert_reprcompare_dataclass:
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py")
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
@@ -833,6 +854,8 @@ class TestAssert_reprcompare_dataclass:
|
||||
"E ",
|
||||
"E Drill down into differing attribute a:",
|
||||
"E a: 10 != 20",
|
||||
"E +10",
|
||||
"E -20",
|
||||
"E ",
|
||||
"E Drill down into differing attribute b:",
|
||||
"E b: 'ten' != 'xxx'",
|
||||
@@ -844,6 +867,7 @@ class TestAssert_reprcompare_dataclass:
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses_verbose(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py")
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
@@ -857,6 +881,7 @@ class TestAssert_reprcompare_dataclass:
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses_with_attribute_comparison_off(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
@@ -866,6 +891,7 @@ class TestAssert_reprcompare_dataclass:
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=0, passed=1)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_comparing_two_different_data_classes(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example(
|
||||
"dataclasses/test_compare_two_different_dataclasses.py"
|
||||
@@ -873,15 +899,6 @@ class TestAssert_reprcompare_dataclass:
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=0, passed=1)
|
||||
|
||||
def test_data_classes_with_custom_eq(self, pytester: Pytester) -> None:
|
||||
p = pytester.copy_example(
|
||||
"dataclasses/test_compare_dataclasses_with_custom_eq.py"
|
||||
)
|
||||
# issue 9362
|
||||
result = pytester.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=1, passed=0)
|
||||
result.stdout.no_re_match_line(".*Differing attributes.*")
|
||||
|
||||
|
||||
class TestAssert_reprcompare_attrsclass:
|
||||
def test_attrs(self) -> None:
|
||||
@@ -965,6 +982,7 @@ class TestAssert_reprcompare_attrsclass:
|
||||
right = SimpleDataObject(1, "b")
|
||||
|
||||
lines = callequal(left, right, verbose=2)
|
||||
print(lines)
|
||||
assert lines is not None
|
||||
assert lines[2].startswith("Matching attributes:")
|
||||
assert "Omitting" not in lines[1]
|
||||
@@ -989,36 +1007,6 @@ class TestAssert_reprcompare_attrsclass:
|
||||
lines = callequal(left, right)
|
||||
assert lines is None
|
||||
|
||||
def test_attrs_with_auto_detect_and_custom_eq(self) -> None:
|
||||
@attr.s(
|
||||
auto_detect=True
|
||||
) # attr.s doesn’t ignore a custom eq if auto_detect=True
|
||||
class SimpleDataObject:
|
||||
field_a = attr.ib()
|
||||
|
||||
def __eq__(self, other): # pragma: no cover
|
||||
return super().__eq__(other)
|
||||
|
||||
left = SimpleDataObject(1)
|
||||
right = SimpleDataObject(2)
|
||||
# issue 9362
|
||||
lines = callequal(left, right, verbose=2)
|
||||
assert lines is None
|
||||
|
||||
def test_attrs_with_custom_eq(self) -> None:
|
||||
@attr.define(slots=False)
|
||||
class SimpleDataObject:
|
||||
field_a = attr.ib()
|
||||
|
||||
def __eq__(self, other): # pragma: no cover
|
||||
return super().__eq__(other)
|
||||
|
||||
left = SimpleDataObject(1)
|
||||
right = SimpleDataObject(2)
|
||||
# issue 9362
|
||||
lines = callequal(left, right, verbose=2)
|
||||
assert lines is None
|
||||
|
||||
|
||||
class TestAssert_reprcompare_namedtuple:
|
||||
def test_namedtuple(self) -> None:
|
||||
@@ -1039,7 +1027,7 @@ class TestAssert_reprcompare_namedtuple:
|
||||
" b: 'b' != 'c'",
|
||||
" - c",
|
||||
" + b",
|
||||
"Use -v to get more diff",
|
||||
"Use -v to get the full diff",
|
||||
]
|
||||
|
||||
def test_comparing_two_different_namedtuple(self) -> None:
|
||||
@@ -1054,7 +1042,7 @@ class TestAssert_reprcompare_namedtuple:
|
||||
assert lines == [
|
||||
"NT1(a=1, b='b') == NT2(a=2, b='b')",
|
||||
"At index 0 diff: 1 != 2",
|
||||
"Use -v to get more diff",
|
||||
"Use -v to get the full diff",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -13,12 +13,10 @@ from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from unittest import mock
|
||||
|
||||
import _pytest._code
|
||||
import pytest
|
||||
@@ -1059,7 +1057,7 @@ class TestAssertionRewriteHookDetails:
|
||||
e = OSError()
|
||||
e.errno = 10
|
||||
raise e
|
||||
yield
|
||||
yield # type:ignore[unreachable]
|
||||
|
||||
monkeypatch.setattr(
|
||||
_pytest.assertion.rewrite, "atomic_write", atomic_write_failed
|
||||
@@ -1147,6 +1145,9 @@ class TestAssertionRewriteHookDetails:
|
||||
_write_pyc(state, co, source_stat, pyc)
|
||||
assert _read_pyc(fn, pyc, state.trace) is not None
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 7), reason="Only the Python 3.7 format for simplicity"
|
||||
)
|
||||
def test_read_pyc_more_invalid(self, tmp_path: Path) -> None:
|
||||
from _pytest.assertion.rewrite import _read_pyc
|
||||
|
||||
@@ -1315,7 +1316,7 @@ class TestIssue2121:
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||
sys.maxsize <= (2 ** 31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||
)
|
||||
@pytest.mark.parametrize("offset", [-1, +1])
|
||||
def test_source_mtime_long_long(pytester: Pytester, offset) -> None:
|
||||
@@ -1334,7 +1335,7 @@ def test_source_mtime_long_long(pytester: Pytester, offset) -> None:
|
||||
# use unsigned long timestamp which overflows signed long,
|
||||
# which was the cause of the bug
|
||||
# +1 offset also tests masking of 0xFFFFFFFF
|
||||
timestamp = 2**32 + offset
|
||||
timestamp = 2 ** 32 + offset
|
||||
os.utime(str(p), (timestamp, timestamp))
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
@@ -1378,7 +1379,7 @@ class TestEarlyRewriteBailout:
|
||||
@pytest.fixture
|
||||
def hook(
|
||||
self, pytestconfig, monkeypatch, pytester: Pytester
|
||||
) -> Generator[AssertionRewritingHook, None, None]:
|
||||
) -> AssertionRewritingHook:
|
||||
"""Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track
|
||||
if PathFinder.find_spec has been called.
|
||||
"""
|
||||
@@ -1399,11 +1400,11 @@ class TestEarlyRewriteBailout:
|
||||
|
||||
hook = AssertionRewritingHook(pytestconfig)
|
||||
# use default patterns, otherwise we inherit pytest's testing config
|
||||
with mock.patch.object(hook, "fnpats", ["test_*.py", "*_test.py"]):
|
||||
monkeypatch.setattr(hook, "_find_spec", spy_find_spec)
|
||||
hook.set_session(StubSession()) # type: ignore[arg-type]
|
||||
pytester.syspathinsert()
|
||||
yield hook
|
||||
hook.fnpats[:] = ["test_*.py", "*_test.py"]
|
||||
monkeypatch.setattr(hook, "_find_spec", spy_find_spec)
|
||||
hook.set_session(StubSession()) # type: ignore[arg-type]
|
||||
pytester.syspathinsert()
|
||||
return hook
|
||||
|
||||
def test_basic(self, pytester: Pytester, hook: AssertionRewritingHook) -> None:
|
||||
"""
|
||||
@@ -1453,9 +1454,9 @@ class TestEarlyRewriteBailout:
|
||||
}
|
||||
)
|
||||
pytester.syspathinsert("tests")
|
||||
with mock.patch.object(hook, "fnpats", ["tests/**.py"]):
|
||||
assert hook.find_spec("file") is not None
|
||||
assert self.find_spec_calls == ["file"]
|
||||
hook.fnpats[:] = ["tests/**.py"]
|
||||
assert hook.find_spec("file") is not None
|
||||
assert self.find_spec_calls == ["file"]
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("win32"), reason="cannot remove cwd on Windows"
|
||||
|
||||
@@ -773,7 +773,7 @@ class TestLastFailed:
|
||||
result = pytester.runpytest("--lf", "--lfnf", "none")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"collected 2 items / 2 deselected / 0 selected",
|
||||
"collected 2 items / 2 deselected",
|
||||
"run-last-failure: no previously failed tests, deselecting all items.",
|
||||
"deselected=2",
|
||||
"* 2 deselected in *",
|
||||
|
||||
@@ -1433,19 +1433,19 @@ def test_error_attribute_issue555(pytester: Pytester) -> None:
|
||||
not sys.platform.startswith("win"),
|
||||
reason="only on windows",
|
||||
)
|
||||
def test_windowsconsoleio_workaround_non_standard_streams() -> None:
|
||||
def test_py36_windowsconsoleio_workaround_non_standard_streams() -> None:
|
||||
"""
|
||||
Ensure _windowsconsoleio_workaround function works with objects that
|
||||
Ensure _py36_windowsconsoleio_workaround function works with objects that
|
||||
do not implement the full ``io``-based stream protocol, for example execnet channels (#2666).
|
||||
"""
|
||||
from _pytest.capture import _windowsconsoleio_workaround
|
||||
from _pytest.capture import _py36_windowsconsoleio_workaround
|
||||
|
||||
class DummyStream:
|
||||
def write(self, s):
|
||||
pass
|
||||
|
||||
stream = cast(TextIO, DummyStream())
|
||||
_windowsconsoleio_workaround(stream)
|
||||
_py36_windowsconsoleio_workaround(stream)
|
||||
|
||||
|
||||
def test_dontreadfrominput_has_encoding(pytester: Pytester) -> None:
|
||||
|
||||
@@ -881,36 +881,6 @@ class TestNodeKeywords:
|
||||
assert item.keywords["kw"] == "method"
|
||||
assert len(item.keywords) == len(set(item.keywords))
|
||||
|
||||
def test_unpacked_marks_added_to_keywords(self, pytester: Pytester) -> None:
|
||||
item = pytester.getitem(
|
||||
"""
|
||||
import pytest
|
||||
pytestmark = pytest.mark.foo
|
||||
class TestClass:
|
||||
pytestmark = pytest.mark.bar
|
||||
def test_method(self): pass
|
||||
test_method.pytestmark = pytest.mark.baz
|
||||
""",
|
||||
"test_method",
|
||||
)
|
||||
assert isinstance(item, pytest.Function)
|
||||
cls = item.getparent(pytest.Class)
|
||||
assert cls is not None
|
||||
mod = item.getparent(pytest.Module)
|
||||
assert mod is not None
|
||||
|
||||
assert item.keywords["foo"] == pytest.mark.foo.mark
|
||||
assert item.keywords["bar"] == pytest.mark.bar.mark
|
||||
assert item.keywords["baz"] == pytest.mark.baz.mark
|
||||
|
||||
assert cls.keywords["foo"] == pytest.mark.foo.mark
|
||||
assert cls.keywords["bar"] == pytest.mark.bar.mark
|
||||
assert "baz" not in cls.keywords
|
||||
|
||||
assert mod.keywords["foo"] == pytest.mark.foo.mark
|
||||
assert "bar" not in mod.keywords
|
||||
assert "baz" not in mod.keywords
|
||||
|
||||
|
||||
COLLECTION_ERROR_PY_FILES = dict(
|
||||
test_01_failure="""
|
||||
@@ -1507,35 +1477,6 @@ class TestImportModeImportlib:
|
||||
]
|
||||
)
|
||||
|
||||
def test_using_python_path(self, pytester: Pytester) -> None:
|
||||
"""
|
||||
Dummy modules created by insert_missing_modules should not get in
|
||||
the way of modules that could be imported via python path (#9645).
|
||||
"""
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
pythonpath = .
|
||||
addopts = --import-mode importlib
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(
|
||||
**{
|
||||
"tests/__init__.py": "",
|
||||
"tests/conftest.py": "",
|
||||
"tests/subpath/__init__.py": "",
|
||||
"tests/subpath/helper.py": "",
|
||||
"tests/subpath/test_something.py": """
|
||||
import tests.subpath.helper
|
||||
|
||||
def test_something():
|
||||
assert True
|
||||
""",
|
||||
}
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines("*1 passed in*")
|
||||
|
||||
|
||||
def test_does_not_crash_on_error_from_decorated_function(pytester: Pytester) -> None:
|
||||
"""Regression test for an issue around bad exception formatting due to
|
||||
|
||||
@@ -135,7 +135,7 @@ def test_is_generator_async_gen_syntax(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
from _pytest.compat import is_generator
|
||||
def test_is_generator():
|
||||
def test_is_generator_py36():
|
||||
async def foo():
|
||||
yield
|
||||
await foo()
|
||||
|
||||
@@ -163,17 +163,7 @@ class TestParseIni:
|
||||
pytester.path.joinpath("pytest.ini").write_text("addopts = -x")
|
||||
result = pytester.runpytest()
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined")
|
||||
|
||||
def test_toml_parse_error(self, pytester: Pytester) -> None:
|
||||
pytester.makepyprojecttoml(
|
||||
"""
|
||||
\\"
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*")
|
||||
result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"])
|
||||
|
||||
@pytest.mark.xfail(reason="probably not needed")
|
||||
def test_confcutdir(self, pytester: Pytester) -> None:
|
||||
|
||||
@@ -114,7 +114,6 @@ class TestConftestValueAccessGlobal:
|
||||
"a", startdir, importmode="prepend", rootpath=Path(basedir)
|
||||
)
|
||||
assert value == 1.5
|
||||
assert mod.__file__ is not None
|
||||
path = Path(mod.__file__)
|
||||
assert path.parent == basedir / "adir" / "b"
|
||||
assert path.stem == "conftest"
|
||||
@@ -146,9 +145,10 @@ def test_issue151_load_all_conftests(pytester: Pytester) -> None:
|
||||
p = pytester.mkdir(name)
|
||||
p.joinpath("conftest.py").touch()
|
||||
|
||||
pm = PytestPluginManager()
|
||||
conftest_setinitial(pm, names)
|
||||
assert len(set(pm.get_plugins()) - {pm}) == len(names)
|
||||
conftest = PytestPluginManager()
|
||||
conftest_setinitial(conftest, names)
|
||||
d = list(conftest._conftestpath2mod.values())
|
||||
assert len(d) == len(names)
|
||||
|
||||
|
||||
def test_conftest_global_import(pytester: Pytester) -> None:
|
||||
@@ -191,20 +191,18 @@ def test_conftestcutdir(pytester: Pytester) -> None:
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
assert len(values) == 0
|
||||
assert not conftest.has_plugin(str(conf))
|
||||
assert Path(conf) not in conftest._conftestpath2mod
|
||||
# but we can still import a conftest directly
|
||||
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
|
||||
values = conftest._getconftestmodules(
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
# and all sub paths get updated properly
|
||||
values = conftest._getconftestmodules(
|
||||
p, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
assert len(values) == 1
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
|
||||
|
||||
@@ -216,7 +214,6 @@ def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None:
|
||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||
)
|
||||
assert len(values) == 1
|
||||
assert values[0].__file__ is not None
|
||||
assert values[0].__file__.startswith(str(conf))
|
||||
|
||||
|
||||
@@ -225,15 +222,15 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None:
|
||||
sub = pytester.mkdir(name)
|
||||
subconftest = sub.joinpath("conftest.py")
|
||||
subconftest.touch()
|
||||
pm = PytestPluginManager()
|
||||
conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path)
|
||||
conftest = PytestPluginManager()
|
||||
conftest_setinitial(conftest, [sub.parent], confcutdir=pytester.path)
|
||||
key = subconftest.resolve()
|
||||
if name not in ("whatever", ".dotdir"):
|
||||
assert pm.has_plugin(str(key))
|
||||
assert len(set(pm.get_plugins()) - {pm}) == 1
|
||||
assert key in conftest._conftestpath2mod
|
||||
assert len(conftest._conftestpath2mod) == 1
|
||||
else:
|
||||
assert not pm.has_plugin(str(key))
|
||||
assert len(set(pm.get_plugins()) - {pm}) == 0
|
||||
assert key not in conftest._conftestpath2mod
|
||||
assert len(conftest._conftestpath2mod) == 0
|
||||
|
||||
|
||||
def test_conftest_confcutdir(pytester: Pytester) -> None:
|
||||
|
||||
@@ -8,6 +8,14 @@ from _pytest.debugging import _validate_usepdb_cls
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.pytester import Pytester
|
||||
|
||||
try:
|
||||
# Type ignored for Python <= 3.6.
|
||||
breakpoint # type: ignore
|
||||
except NameError:
|
||||
SUPPORTS_BREAKPOINT_BUILTIN = False
|
||||
else:
|
||||
SUPPORTS_BREAKPOINT_BUILTIN = True
|
||||
|
||||
|
||||
_ENVIRON_PYTHONBREAKPOINT = os.environ.get("PYTHONBREAKPOINT", "")
|
||||
|
||||
@@ -903,6 +911,14 @@ class TestPDB:
|
||||
|
||||
|
||||
class TestDebuggingBreakpoints:
|
||||
def test_supports_breakpoint_module_global(self) -> None:
|
||||
"""Test that supports breakpoint global marks on Python 3.7+."""
|
||||
if sys.version_info >= (3, 7):
|
||||
assert SUPPORTS_BREAKPOINT_BUILTIN is True
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
|
||||
)
|
||||
@pytest.mark.parametrize("arg", ["--pdb", ""])
|
||||
def test_sys_breakpointhook_configure_and_unconfigure(
|
||||
self, pytester: Pytester, arg: str
|
||||
@@ -936,6 +952,9 @@ class TestDebuggingBreakpoints:
|
||||
result = pytester.runpytest_subprocess(*args)
|
||||
result.stdout.fnmatch_lines(["*1 passed in *"])
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
|
||||
)
|
||||
def test_pdb_custom_cls(self, pytester: Pytester, custom_debugger_hook) -> None:
|
||||
p1 = pytester.makepyfile(
|
||||
"""
|
||||
@@ -950,6 +969,9 @@ class TestDebuggingBreakpoints:
|
||||
assert custom_debugger_hook == ["init", "set_trace"]
|
||||
|
||||
@pytest.mark.parametrize("arg", ["--pdb", ""])
|
||||
@pytest.mark.skipif(
|
||||
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
|
||||
)
|
||||
def test_environ_custom_class(
|
||||
self, pytester: Pytester, custom_debugger_hook, arg: str
|
||||
) -> None:
|
||||
@@ -980,6 +1002,9 @@ class TestDebuggingBreakpoints:
|
||||
result = pytester.runpytest_subprocess(*args)
|
||||
result.stdout.fnmatch_lines(["*1 passed in *"])
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
not _ENVIRON_PYTHONBREAKPOINT == "",
|
||||
reason="Requires breakpoint() default value",
|
||||
@@ -1000,6 +1025,9 @@ class TestDebuggingBreakpoints:
|
||||
assert "reading from stdin while output" not in rest
|
||||
TestPDB.flush(child)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin"
|
||||
)
|
||||
def test_pdb_not_altered(self, pytester: Pytester) -> None:
|
||||
p1 = pytester.makepyfile(
|
||||
"""
|
||||
|
||||
@@ -803,8 +803,8 @@ class TestDoctests:
|
||||
"""
|
||||
p = pytester.makepyfile(
|
||||
setup="""
|
||||
from setuptools import setup, find_packages
|
||||
if __name__ == '__main__':
|
||||
from setuptools import setup, find_packages
|
||||
setup(name='sample',
|
||||
version='0.0',
|
||||
description='description',
|
||||
|
||||
@@ -4,6 +4,8 @@ Tests and examples for correct "+/-" usage in error diffs.
|
||||
See https://github.com/pytest-dev/pytest/issues/3333 for details.
|
||||
|
||||
"""
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from _pytest.pytester import Pytester
|
||||
|
||||
@@ -208,61 +210,68 @@ TESTCASES = [
|
||||
""",
|
||||
id='Test "not in" string',
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
b: str
|
||||
|
||||
def test_this():
|
||||
result = A(1, 'spam')
|
||||
expected = A(2, 'spam')
|
||||
assert result == expected
|
||||
""",
|
||||
"""
|
||||
> assert result == expected
|
||||
E AssertionError: assert A(a=1, b='spam') == A(a=2, b='spam')
|
||||
E Matching attributes:
|
||||
E ['b']
|
||||
E Differing attributes:
|
||||
E ['a']
|
||||
E Drill down into differing attribute a:
|
||||
E a: 1 != 2
|
||||
""",
|
||||
id="Compare data classes",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
import attr
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class A:
|
||||
a: int
|
||||
b: str
|
||||
|
||||
def test_this():
|
||||
result = A(1, 'spam')
|
||||
expected = A(1, 'eggs')
|
||||
assert result == expected
|
||||
""",
|
||||
"""
|
||||
> assert result == expected
|
||||
E AssertionError: assert A(a=1, b='spam') == A(a=1, b='eggs')
|
||||
E Matching attributes:
|
||||
E ['a']
|
||||
E Differing attributes:
|
||||
E ['b']
|
||||
E Drill down into differing attribute b:
|
||||
E b: 'spam' != 'eggs'
|
||||
E - eggs
|
||||
E + spam
|
||||
""",
|
||||
id="Compare attrs classes",
|
||||
),
|
||||
]
|
||||
if sys.version_info[:2] >= (3, 7):
|
||||
TESTCASES.extend(
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
b: str
|
||||
|
||||
def test_this():
|
||||
result = A(1, 'spam')
|
||||
expected = A(2, 'spam')
|
||||
assert result == expected
|
||||
""",
|
||||
"""
|
||||
> assert result == expected
|
||||
E AssertionError: assert A(a=1, b='spam') == A(a=2, b='spam')
|
||||
E Matching attributes:
|
||||
E ['b']
|
||||
E Differing attributes:
|
||||
E ['a']
|
||||
E Drill down into differing attribute a:
|
||||
E a: 1 != 2
|
||||
E +1
|
||||
E -2
|
||||
""",
|
||||
id="Compare data classes",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
import attr
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class A:
|
||||
a: int
|
||||
b: str
|
||||
|
||||
def test_this():
|
||||
result = A(1, 'spam')
|
||||
expected = A(1, 'eggs')
|
||||
assert result == expected
|
||||
""",
|
||||
"""
|
||||
> assert result == expected
|
||||
E AssertionError: assert A(a=1, b='spam') == A(a=1, b='eggs')
|
||||
E Matching attributes:
|
||||
E ['a']
|
||||
E Differing attributes:
|
||||
E ['b']
|
||||
E Drill down into differing attribute b:
|
||||
E b: 'spam' != 'eggs'
|
||||
E - eggs
|
||||
E + spam
|
||||
""",
|
||||
id="Compare attrs classes",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code, expected", TESTCASES)
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_item_fspath(pytester: pytest.Pytester) -> None:
|
||||
items2, hookrec = pytester.inline_genitems(item.nodeid)
|
||||
(item2,) = items2
|
||||
assert item2.name == item.name
|
||||
assert item2.fspath == item.fspath
|
||||
assert item2.fspath == item.fspath # type: ignore[attr-defined]
|
||||
assert item2.path == item.path
|
||||
|
||||
|
||||
|
||||
@@ -823,6 +823,25 @@ class TestKeywordSelection:
|
||||
assert len(dlist) == 1
|
||||
assert dlist[0].items[0].name == "test_1"
|
||||
|
||||
def test_select_starton(self, pytester: Pytester) -> None:
|
||||
threepass = pytester.makepyfile(
|
||||
test_threepass="""
|
||||
def test_one(): assert 1
|
||||
def test_two(): assert 1
|
||||
def test_three(): assert 1
|
||||
"""
|
||||
)
|
||||
reprec = pytester.inline_run(
|
||||
"-Wignore::pytest.PytestRemovedIn7Warning", "-k", "test_two:", threepass
|
||||
)
|
||||
passed, skipped, failed = reprec.listoutcomes()
|
||||
assert len(passed) == 2
|
||||
assert not failed
|
||||
dlist = reprec.getcalls("pytest_deselected")
|
||||
assert len(dlist) == 1
|
||||
item = dlist[0].items[0]
|
||||
assert item.name == "test_one"
|
||||
|
||||
def test_keyword_extra(self, pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
|
||||
@@ -50,24 +50,21 @@ def test_setattr() -> None:
|
||||
|
||||
class TestSetattrWithImportPath:
|
||||
def test_string_expression(self, monkeypatch: MonkeyPatch) -> None:
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("os.path.abspath", lambda x: "hello2")
|
||||
assert os.path.abspath("123") == "hello2"
|
||||
monkeypatch.setattr("os.path.abspath", lambda x: "hello2")
|
||||
assert os.path.abspath("123") == "hello2"
|
||||
|
||||
def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None:
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("_pytest.config.Config", 42)
|
||||
import _pytest
|
||||
monkeypatch.setattr("_pytest.config.Config", 42)
|
||||
import _pytest
|
||||
|
||||
assert _pytest.config.Config == 42 # type: ignore
|
||||
assert _pytest.config.Config == 42 # type: ignore
|
||||
|
||||
def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None:
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("_pytest.config.Config", 42)
|
||||
import _pytest
|
||||
monkeypatch.setattr("_pytest.config.Config", 42)
|
||||
import _pytest
|
||||
|
||||
assert _pytest.config.Config == 42 # type: ignore
|
||||
mp.delattr("_pytest.config.Config")
|
||||
assert _pytest.config.Config == 42 # type: ignore
|
||||
monkeypatch.delattr("_pytest.config.Config")
|
||||
|
||||
def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None:
|
||||
with pytest.raises(TypeError):
|
||||
@@ -83,16 +80,14 @@ class TestSetattrWithImportPath:
|
||||
|
||||
def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None:
|
||||
# https://github.com/pytest-dev/pytest/issues/746
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("os.path.qweqwe", 42, raising=False)
|
||||
assert os.path.qweqwe == 42 # type: ignore
|
||||
monkeypatch.setattr("os.path.qweqwe", 42, raising=False)
|
||||
assert os.path.qweqwe == 42 # type: ignore
|
||||
|
||||
def test_delattr(self, monkeypatch: MonkeyPatch) -> None:
|
||||
with monkeypatch.context() as mp:
|
||||
mp.delattr("os.path.abspath")
|
||||
assert not hasattr(os.path, "abspath")
|
||||
mp.undo()
|
||||
assert os.path.abspath
|
||||
monkeypatch.delattr("os.path.abspath")
|
||||
assert not hasattr(os.path, "abspath")
|
||||
monkeypatch.undo()
|
||||
assert os.path.abspath
|
||||
|
||||
|
||||
def test_delattr() -> None:
|
||||
|
||||
@@ -345,7 +345,7 @@ def test_SkipTest_during_collection(pytester: Pytester) -> None:
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(p)
|
||||
result.assert_outcomes(skipped=1, warnings=0)
|
||||
result.assert_outcomes(skipped=1, warnings=1)
|
||||
|
||||
|
||||
def test_SkipTest_in_test(pytester: Pytester) -> None:
|
||||
|
||||
@@ -295,7 +295,7 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True,
|
||||
text=True,
|
||||
universal_newlines=True,
|
||||
).stdout
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pytest.skip("bash is not available")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import email.message
|
||||
import io
|
||||
from typing import List
|
||||
from typing import Union
|
||||
@@ -99,9 +98,7 @@ class TestPaste:
|
||||
|
||||
def mocked(url, data):
|
||||
calls.append((url, data))
|
||||
raise urllib.error.HTTPError(
|
||||
url, 400, "Bad request", email.message.Message(), io.BytesIO()
|
||||
)
|
||||
raise urllib.error.HTTPError(url, 400, "Bad request", {}, io.BytesIO())
|
||||
|
||||
monkeypatch.setattr(urllib.request, "urlopen", mocked)
|
||||
return calls
|
||||
|
||||
@@ -143,10 +143,6 @@ class TestImportPath:
|
||||
assert obj.x == 42 # type: ignore[attr-defined]
|
||||
assert obj.__name__ == "execfile"
|
||||
|
||||
def test_import_path_missing_file(self, path1: Path) -> None:
|
||||
with pytest.raises(ImportPathMismatchError):
|
||||
import_path(path1 / "sampledir", root=path1)
|
||||
|
||||
def test_renamed_dir_creates_mismatch(
|
||||
self, tmp_path: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
@@ -454,6 +450,7 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N
|
||||
|
||||
|
||||
class TestImportLibMode:
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_importmode_importlib_with_dataclass(self, tmp_path: Path) -> None:
|
||||
"""Ensure that importlib mode works with a module containing dataclasses (#7856)."""
|
||||
fn = tmp_path.joinpath("_src/tests/test_dataclass.py")
|
||||
@@ -562,20 +559,15 @@ class TestImportLibMode:
|
||||
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
|
||||
assert result == "home.foo.test_foo"
|
||||
|
||||
def test_insert_missing_modules(
|
||||
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
# Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and
|
||||
# don't end up being imported.
|
||||
modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")}
|
||||
insert_missing_modules(modules, "xxx.tests.foo")
|
||||
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
|
||||
def test_insert_missing_modules(self) -> None:
|
||||
modules = {"src.tests.foo": ModuleType("src.tests.foo")}
|
||||
insert_missing_modules(modules, "src.tests.foo")
|
||||
assert sorted(modules) == ["src", "src.tests", "src.tests.foo"]
|
||||
|
||||
mod = ModuleType("mod", doc="My Module")
|
||||
modules = {"xxy": mod}
|
||||
insert_missing_modules(modules, "xxy")
|
||||
assert modules == {"xxy": mod}
|
||||
modules = {"src": mod}
|
||||
insert_missing_modules(modules, "src")
|
||||
assert modules == {"src": mod}
|
||||
|
||||
modules = {}
|
||||
insert_missing_modules(modules, "")
|
||||
|
||||
@@ -618,9 +618,14 @@ def test_linematcher_string_api() -> None:
|
||||
|
||||
|
||||
def test_pytest_addopts_before_pytester(request, monkeypatch: MonkeyPatch) -> None:
|
||||
orig = os.environ.get("PYTEST_ADDOPTS", None)
|
||||
monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused")
|
||||
_: Pytester = request.getfixturevalue("pytester")
|
||||
pytester: Pytester = request.getfixturevalue("pytester")
|
||||
assert "PYTEST_ADDOPTS" not in os.environ
|
||||
pytester._finalize()
|
||||
assert os.environ.get("PYTEST_ADDOPTS") == "--orig-unused"
|
||||
monkeypatch.undo()
|
||||
assert os.environ.get("PYTEST_ADDOPTS") == orig
|
||||
|
||||
|
||||
def test_run_stdin(pytester: Pytester) -> None:
|
||||
|
||||
@@ -114,13 +114,13 @@ class TestDeprecatedCall:
|
||||
# Type ignored because `onceregistry` and `filters` are not
|
||||
# documented API.
|
||||
onceregistry = warnings.onceregistry.copy() # type: ignore
|
||||
filters = warnings.filters[:]
|
||||
filters = warnings.filters[:] # type: ignore
|
||||
warn = warnings.warn
|
||||
warn_explicit = warnings.warn_explicit
|
||||
self.test_deprecated_call_raises()
|
||||
self.test_deprecated_call()
|
||||
assert onceregistry == warnings.onceregistry # type: ignore
|
||||
assert filters == warnings.filters
|
||||
assert filters == warnings.filters # type: ignore
|
||||
assert warn is warnings.warn
|
||||
assert warn_explicit is warnings.warn_explicit
|
||||
|
||||
|
||||
@@ -335,54 +335,6 @@ def test_sessionfinish_with_start(pytester: Pytester) -> None:
|
||||
assert res.ret == ExitCode.NO_TESTS_COLLECTED
|
||||
|
||||
|
||||
def test_collection_args_do_not_duplicate_modules(pytester: Pytester) -> None:
|
||||
"""Test that when multiple collection args are specified on the command line
|
||||
for the same module, only a single Module collector is created.
|
||||
|
||||
Regression test for #723, #3358.
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
**{
|
||||
"d/test_it": """
|
||||
def test_1(): pass
|
||||
def test_2(): pass
|
||||
"""
|
||||
}
|
||||
)
|
||||
|
||||
result = pytester.runpytest(
|
||||
"--collect-only",
|
||||
"d/test_it.py::test_1",
|
||||
"d/test_it.py::test_2",
|
||||
)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"<Module d/test_it.py>",
|
||||
" <Function test_1>",
|
||||
" <Function test_2>",
|
||||
],
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
# Different, but related case.
|
||||
result = pytester.runpytest(
|
||||
"--collect-only",
|
||||
"--keep-duplicates",
|
||||
"d",
|
||||
"d",
|
||||
)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"<Module d/test_it.py>",
|
||||
" <Function test_1>",
|
||||
" <Function test_2>",
|
||||
" <Function test_1>",
|
||||
" <Function test_2>",
|
||||
],
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["root", "{relative}/root", "{environment}/root"])
|
||||
def test_rootdir_option_arg(
|
||||
pytester: Pytester, monkeypatch: MonkeyPatch, path: str
|
||||
|
||||
@@ -385,55 +385,21 @@ class TestTerminal:
|
||||
|
||||
def test_10():
|
||||
pytest.xfail("It's 🕙 o'clock")
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="cannot do foobar because baz is missing due to I don't know what"
|
||||
)
|
||||
def test_long_skip():
|
||||
pass
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="cannot do foobar because baz is missing due to I don't know what"
|
||||
)
|
||||
def test_long_xfail():
|
||||
print(1 / 0)
|
||||
"""
|
||||
)
|
||||
|
||||
common_output = [
|
||||
"test_verbose_skip_reason.py::test_1 SKIPPED (123) *",
|
||||
"test_verbose_skip_reason.py::test_2 XPASS (456) *",
|
||||
"test_verbose_skip_reason.py::test_3 XFAIL (789) *",
|
||||
"test_verbose_skip_reason.py::test_4 XFAIL *",
|
||||
"test_verbose_skip_reason.py::test_5 SKIPPED (unconditional skip) *",
|
||||
"test_verbose_skip_reason.py::test_6 XPASS *",
|
||||
"test_verbose_skip_reason.py::test_7 SKIPPED *",
|
||||
"test_verbose_skip_reason.py::test_8 SKIPPED (888 is great) *",
|
||||
"test_verbose_skip_reason.py::test_9 XFAIL *",
|
||||
"test_verbose_skip_reason.py::test_10 XFAIL (It's 🕙 o'clock) *",
|
||||
]
|
||||
|
||||
result = pytester.runpytest("-v")
|
||||
result.stdout.fnmatch_lines(
|
||||
common_output
|
||||
+ [
|
||||
"test_verbose_skip_reason.py::test_long_skip SKIPPED (cannot *...) *",
|
||||
"test_verbose_skip_reason.py::test_long_xfail XFAIL (cannot *...) *",
|
||||
]
|
||||
)
|
||||
|
||||
result = pytester.runpytest("-vv")
|
||||
result.stdout.fnmatch_lines(
|
||||
common_output
|
||||
+ [
|
||||
(
|
||||
"test_verbose_skip_reason.py::test_long_skip SKIPPED"
|
||||
" (cannot do foobar because baz is missing due to I don't know what) *"
|
||||
),
|
||||
(
|
||||
"test_verbose_skip_reason.py::test_long_xfail XFAIL"
|
||||
" (cannot do foobar because baz is missing due to I don't know what) *"
|
||||
),
|
||||
[
|
||||
"test_verbose_skip_reason.py::test_1 SKIPPED (123) *",
|
||||
"test_verbose_skip_reason.py::test_2 XPASS (456) *",
|
||||
"test_verbose_skip_reason.py::test_3 XFAIL (789) *",
|
||||
"test_verbose_skip_reason.py::test_4 XFAIL *",
|
||||
"test_verbose_skip_reason.py::test_5 SKIPPED (unconditional skip) *",
|
||||
"test_verbose_skip_reason.py::test_6 XPASS *",
|
||||
"test_verbose_skip_reason.py::test_7 SKIPPED *",
|
||||
"test_verbose_skip_reason.py::test_8 SKIPPED (888 is great) *",
|
||||
"test_verbose_skip_reason.py::test_9 XFAIL *",
|
||||
"test_verbose_skip_reason.py::test_10 XFAIL (It's 🕙 o'clock) *",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -716,7 +682,9 @@ class TestTerminalFunctional:
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-k", "test_t", testpath)
|
||||
result = pytester.runpytest(
|
||||
"-Wignore::pytest.PytestRemovedIn7Warning", "-k", "test_two:", testpath
|
||||
)
|
||||
result.stdout.fnmatch_lines(
|
||||
["collected 3 items / 1 deselected / 2 selected", "*test_deselected.py ..*"]
|
||||
)
|
||||
@@ -783,33 +751,6 @@ class TestTerminalFunctional:
|
||||
result.stdout.no_fnmatch_line("*= 1 deselected =*")
|
||||
assert result.ret == 0
|
||||
|
||||
def test_selected_count_with_error(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
test_selected_count_3="""
|
||||
def test_one():
|
||||
pass
|
||||
def test_two():
|
||||
pass
|
||||
def test_three():
|
||||
pass
|
||||
""",
|
||||
test_selected_count_error="""
|
||||
5/0
|
||||
def test_foo():
|
||||
pass
|
||||
def test_bar():
|
||||
pass
|
||||
""",
|
||||
)
|
||||
result = pytester.runpytest("-k", "test_t")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"collected 3 items / 1 error / 1 deselected / 2 selected",
|
||||
"* ERROR collecting test_selected_count_error.py *",
|
||||
]
|
||||
)
|
||||
assert result.ret == ExitCode.INTERRUPTED
|
||||
|
||||
def test_no_skip_summary_if_failure(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
|
||||
@@ -1498,30 +1498,3 @@ def test_traceback_pruning(pytester: Pytester) -> None:
|
||||
assert passed == 1
|
||||
assert failed == 1
|
||||
assert reprec.ret == 1
|
||||
|
||||
|
||||
def test_raising_unittest_skiptest_during_collection(
|
||||
pytester: Pytester,
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
|
||||
class TestIt(unittest.TestCase):
|
||||
def test_it(self): pass
|
||||
def test_it2(self): pass
|
||||
|
||||
raise unittest.SkipTest()
|
||||
|
||||
class TestIt2(unittest.TestCase):
|
||||
def test_it(self): pass
|
||||
def test_it2(self): pass
|
||||
"""
|
||||
)
|
||||
reprec = pytester.inline_run()
|
||||
passed, skipped, failed = reprec.countoutcomes()
|
||||
assert passed == 0
|
||||
# Unittest reports one fake test for a skipped module.
|
||||
assert skipped == 1
|
||||
assert failed == 0
|
||||
assert reprec.ret == ExitCode.NO_TESTS_COLLECTED
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
@@ -240,7 +239,7 @@ def test_filterwarnings_mark_registration(pytester: Pytester) -> None:
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("always::UserWarning")
|
||||
def test_warning_recorded_hook(pytester: Pytester) -> None:
|
||||
def test_warning_captured_hook(pytester: Pytester) -> None:
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
def pytest_configure(config):
|
||||
@@ -277,9 +276,9 @@ def test_warning_recorded_hook(pytester: Pytester) -> None:
|
||||
expected = [
|
||||
("config warning", "config", ""),
|
||||
("collect warning", "collect", ""),
|
||||
("setup warning", "runtest", "test_warning_recorded_hook.py::test_func"),
|
||||
("call warning", "runtest", "test_warning_recorded_hook.py::test_func"),
|
||||
("teardown warning", "runtest", "test_warning_recorded_hook.py::test_func"),
|
||||
("setup warning", "runtest", "test_warning_captured_hook.py::test_func"),
|
||||
("call warning", "runtest", "test_warning_captured_hook.py::test_func"),
|
||||
("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"),
|
||||
]
|
||||
for index in range(len(expected)):
|
||||
collected_result = collected[index]
|
||||
@@ -518,7 +517,6 @@ class TestDeprecationWarningsByDefault:
|
||||
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
|
||||
|
||||
|
||||
@pytest.mark.skip("not relevant until pytest 8.0")
|
||||
@pytest.mark.parametrize("change_default", [None, "ini", "cmdline"])
|
||||
def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> None:
|
||||
"""This ensures that PytestRemovedInXWarnings raised by pytest are turned into errors.
|
||||
@@ -530,7 +528,7 @@ def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> No
|
||||
"""
|
||||
import warnings, pytest
|
||||
def test():
|
||||
warnings.warn(pytest.PytestRemovedIn8Warning("some warning"))
|
||||
warnings.warn(pytest.PytestRemovedIn7Warning("some warning"))
|
||||
"""
|
||||
)
|
||||
if change_default == "ini":
|
||||
@@ -538,12 +536,12 @@ def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> No
|
||||
"""
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore::pytest.PytestRemovedIn8Warning
|
||||
ignore::pytest.PytestRemovedIn7Warning
|
||||
"""
|
||||
)
|
||||
|
||||
args = (
|
||||
("-Wignore::pytest.PytestRemovedIn8Warning",)
|
||||
("-Wignore::pytest.PytestRemovedIn7Warning",)
|
||||
if change_default == "cmdline"
|
||||
else ()
|
||||
)
|
||||
@@ -775,57 +773,3 @@ class TestStackLevel:
|
||||
"*Unknown pytest.mark.unknown*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_resource_warning(pytester: Pytester, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Some platforms (notably PyPy) don't have tracemalloc.
|
||||
# We choose to explicitly not skip this in case tracemalloc is not
|
||||
# available, using `importorskip("tracemalloc")` for example,
|
||||
# because we want to ensure the same code path does not break in those platforms.
|
||||
try:
|
||||
import tracemalloc # noqa
|
||||
|
||||
has_tracemalloc = True
|
||||
except ImportError:
|
||||
has_tracemalloc = False
|
||||
|
||||
# Explicitly disable PYTHONTRACEMALLOC in case pytest's test suite is running
|
||||
# with it enabled.
|
||||
monkeypatch.delenv("PYTHONTRACEMALLOC", raising=False)
|
||||
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def open_file(p):
|
||||
f = p.open("r")
|
||||
assert p.read_text() == "hello"
|
||||
|
||||
def test_resource_warning(tmp_path):
|
||||
p = tmp_path.joinpath("foo.txt")
|
||||
p.write_text("hello")
|
||||
open_file(p)
|
||||
"""
|
||||
)
|
||||
result = pytester.run(sys.executable, "-Xdev", "-m", "pytest")
|
||||
expected_extra = (
|
||||
[
|
||||
"*ResourceWarning* unclosed file*",
|
||||
"*Enable tracemalloc to get traceback where the object was allocated*",
|
||||
"*See https* for more info.",
|
||||
]
|
||||
if has_tracemalloc
|
||||
else []
|
||||
)
|
||||
result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"])
|
||||
|
||||
monkeypatch.setenv("PYTHONTRACEMALLOC", "20")
|
||||
|
||||
result = pytester.run(sys.executable, "-Xdev", "-m", "pytest")
|
||||
expected_extra = (
|
||||
[
|
||||
"*ResourceWarning* unclosed file*",
|
||||
"*Object allocated at*",
|
||||
]
|
||||
if has_tracemalloc
|
||||
else []
|
||||
)
|
||||
result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"])
|
||||
|
||||
5
tox.ini
5
tox.ini
@@ -4,6 +4,7 @@ minversion = 3.20.0
|
||||
distshare = {homedir}/.tox/distshare
|
||||
envlist =
|
||||
linting
|
||||
py36
|
||||
py37
|
||||
py38
|
||||
py39
|
||||
@@ -100,9 +101,9 @@ commands =
|
||||
[testenv:plugins]
|
||||
# use latest versions of all plugins, including pre-releases
|
||||
pip_pre=true
|
||||
# use latest pip to get new dependency resolver (#7783)
|
||||
# use latest pip and new dependency resolver (#7783)
|
||||
download=true
|
||||
install_command=python -m pip install {opts} {packages}
|
||||
install_command=python -m pip --use-feature=2020-resolver install {opts} {packages}
|
||||
changedir = testing/plugins_integration
|
||||
deps = -rtesting/plugins_integration/requirements.txt
|
||||
setenv =
|
||||
|
||||
Reference in New Issue
Block a user