diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index cc0e6331d45..20a72270fde 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -19,6 +19,11 @@ jobs:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
timeout-minutes: 10
+ # Required by attest-build-provenance-github.
+ permissions:
+ id-token: write
+ attestations: write
+
steps:
- uses: actions/checkout@v4
with:
@@ -26,7 +31,9 @@ jobs:
persist-credentials: false
- name: Build and Check Package
- uses: hynek/build-and-inspect-python-package@v2.4.0
+ uses: hynek/build-and-inspect-python-package@v2.5.0
+ with:
+ attest-build-provenance-github: 'true'
deploy:
if: github.repository == 'pytest-dev/pytest'
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4434740675e..09d37aaa2c8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -35,7 +35,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
- uses: hynek/build-and-inspect-python-package@v2.4.0
+ uses: hynek/build-and-inspect-python-package@v2.5.0
build:
needs: [package]
@@ -55,6 +55,7 @@ jobs:
"windows-py310",
"windows-py311",
"windows-py312",
+ "windows-py313",
"ubuntu-py38",
"ubuntu-py38-pluggy",
@@ -63,12 +64,14 @@ jobs:
"ubuntu-py310",
"ubuntu-py311",
"ubuntu-py312",
+ "ubuntu-py313",
"ubuntu-pypy3",
"macos-py38",
"macos-py39",
"macos-py310",
"macos-py312",
+ "macos-py313",
"doctesting",
"plugins",
@@ -97,9 +100,13 @@ jobs:
os: windows-latest
tox_env: "py311"
- name: "windows-py312"
- python: "3.12-dev"
+ python: "3.12"
os: windows-latest
tox_env: "py312"
+ - name: "windows-py313"
+ python: "3.13-dev"
+ os: windows-latest
+ tox_env: "py313"
- name: "ubuntu-py38"
python: "3.8"
@@ -128,10 +135,15 @@ jobs:
tox_env: "py311"
use_coverage: true
- name: "ubuntu-py312"
- python: "3.12-dev"
+ python: "3.12"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
+ - name: "ubuntu-py313"
+ python: "3.13-dev"
+ os: ubuntu-latest
+ tox_env: "py313"
+ use_coverage: true
- name: "ubuntu-pypy3"
python: "pypy-3.8"
os: ubuntu-latest
@@ -151,9 +163,13 @@ jobs:
os: macos-latest
tox_env: "py310-xdist"
- name: "macos-py312"
- python: "3.12-dev"
+ python: "3.12"
os: macos-latest
tox_env: "py312-xdist"
+ - name: "macos-py313"
+ python: "3.13-dev"
+ os: macos-latest
+ tox_env: "py313-xdist"
- name: "plugins"
python: "3.12"
diff --git a/AUTHORS b/AUTHORS
index 4f61c05914b..748b9bae2c8 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -36,6 +36,7 @@ Andrey Paramonov
Andrzej Klajnert
Andrzej Ostrowski
Andy Freeland
+Anita Hammer
Anthon van der Neut
Anthony Shaw
Anthony Sottile
@@ -191,6 +192,7 @@ Jake VanderPlas
Jakob van Santen
Jakub Mitoraj
James Bourbeau
+James Frost
Jan Balster
Janne Vanhala
Jason R. Coombs
@@ -288,6 +290,7 @@ Mike Lundy
Milan Lesnek
Miro Hrončok
mrbean-bremen
+Nathan Goldbaum
Nathaniel Compton
Nathaniel Waisbrot
Ned Batchelder
@@ -357,6 +360,7 @@ Sadra Barikbin
Saiprasad Kale
Samuel Colvin
Samuel Dion-Girardeau
+Samuel Jirovec
Samuel Searles-Bryant
Samuel Therrien (Avasam)
Samuele Pedroni
@@ -440,6 +444,7 @@ Yao Xiao
Yoav Caspi
Yuliang Shao
Yusuke Kadowaki
+Yutian Li
Yuval Shimon
Zac Hatfield-Dodds
Zachary Kneupper
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index d7da59c812d..0bf440da261 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -5,10 +5,6 @@ Contribution getting started
Contributions are highly welcomed and appreciated. Every little bit of help counts,
so do not hesitate!
-.. contents::
- :depth: 2
- :backlinks: none
-
.. _submitfeedback:
diff --git a/doc/en/img/pytest1.png b/doc/en/_static/pytest1.png
similarity index 100%
rename from doc/en/img/pytest1.png
rename to doc/en/_static/pytest1.png
diff --git a/doc/en/_templates/sidebar/brand.html b/doc/en/_templates/sidebar/brand.html
new file mode 100644
index 00000000000..f997c4cca5f
--- /dev/null
+++ b/doc/en/_templates/sidebar/brand.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/doc/en/_templates/slim_searchbox.html b/doc/en/_templates/slim_searchbox.html
deleted file mode 100644
index f088ff8d312..00000000000
--- a/doc/en/_templates/slim_searchbox.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{#
- basic/searchbox.html with heading removed.
-#}
-{%- if pagename != "search" and builder != "singlehtml" %}
-
-
-{%- endif %}
diff --git a/doc/en/_templates/style.html b/doc/en/_templates/style.html
new file mode 100644
index 00000000000..400cb75ff97
--- /dev/null
+++ b/doc/en/_templates/style.html
@@ -0,0 +1,7 @@
+
diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst
index 4d0a3ab558e..c65eb5f3613 100644
--- a/doc/en/announce/index.rst
+++ b/doc/en/announce/index.rst
@@ -6,6 +6,8 @@ Release announcements
:maxdepth: 2
+ release-8.2.2
+ release-8.2.1
release-8.2.0
release-8.1.2
release-8.1.1
diff --git a/doc/en/announce/release-2.0.0.rst b/doc/en/announce/release-2.0.0.rst
index ecb1a1db988..c2a9f6da4d5 100644
--- a/doc/en/announce/release-2.0.0.rst
+++ b/doc/en/announce/release-2.0.0.rst
@@ -62,7 +62,7 @@ New Features
- new "-q" option which decreases verbosity and prints a more
nose/unittest-style "dot" output.
-- many many more detailed improvements details
+- many, many, more detailed improvements details
Fixes
-----------------------
@@ -109,7 +109,7 @@ Important Notes
in conftest.py files. They will cause nothing special.
- removed support for calling the pre-1.0 collection API of "run()" and "join"
- removed reading option values from conftest.py files or env variables.
- This can now be done much much better and easier through the ini-file
+ This can now be done much, much, better and easier through the ini-file
mechanism and the "addopts" entry in particular.
- removed the "disabled" attribute in test classes. Use the skipping
and pytestmark mechanism to skip or xfail a test class.
diff --git a/doc/en/announce/release-2.2.2.rst b/doc/en/announce/release-2.2.2.rst
index 22ef0bc7a16..510b35ee1d0 100644
--- a/doc/en/announce/release-2.2.2.rst
+++ b/doc/en/announce/release-2.2.2.rst
@@ -4,7 +4,7 @@ pytest-2.2.2: bug fixes
pytest-2.2.2 (updated to 2.2.3 to fix packaging issues) is a minor
backward-compatible release of the versatile py.test testing tool. It
contains bug fixes and a few refinements particularly to reporting with
-"--collectonly", see below for betails.
+"--collectonly", see below for details.
For general information see here:
diff --git a/doc/en/announce/release-2.4.0.rst b/doc/en/announce/release-2.4.0.rst
index 138cc89576c..9b864329674 100644
--- a/doc/en/announce/release-2.4.0.rst
+++ b/doc/en/announce/release-2.4.0.rst
@@ -181,7 +181,7 @@ Bug fixes:
partially failed (finalizers would not always be called before)
- fix issue320 - fix class scope for fixtures when mixed with
- module-level functions. Thanks Anatloy Bubenkoff.
+ module-level functions. Thanks Anatoly Bubenkoff.
- you can specify "-q" or "-qq" to get different levels of "quieter"
reporting (thanks Katarzyna Jachim)
diff --git a/doc/en/announce/release-2.5.0.rst b/doc/en/announce/release-2.5.0.rst
index c6cdcdd8a83..fe64f1b8668 100644
--- a/doc/en/announce/release-2.5.0.rst
+++ b/doc/en/announce/release-2.5.0.rst
@@ -83,7 +83,7 @@ holger krekel
Thanks Ralph Schmitt for the precise failure example.
- fix issue244 by implementing special index for parameters to only use
- indices for paramentrized test ids
+ indices for parametrized test ids
- fix issue287 by running all finalizers but saving the exception
from the first failing finalizer and re-raising it so teardown will
diff --git a/doc/en/announce/release-2.6.0.rst b/doc/en/announce/release-2.6.0.rst
index 56fbd6cc1e4..c00df585738 100644
--- a/doc/en/announce/release-2.6.0.rst
+++ b/doc/en/announce/release-2.6.0.rst
@@ -73,7 +73,7 @@ holger krekel
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
Gospodnetic for the PR.
-- change XPASS colour to yellow rather then red when tests are run
+- change XPASS colour to yellow rather than red when tests are run
with -v.
- fix issue473: work around mock putting an unbound method into a class
diff --git a/doc/en/announce/release-2.7.0.rst b/doc/en/announce/release-2.7.0.rst
index 2840178a07f..83cddb34157 100644
--- a/doc/en/announce/release-2.7.0.rst
+++ b/doc/en/announce/release-2.7.0.rst
@@ -55,7 +55,7 @@ holger krekel
github. See https://pytest.org/en/stable/contributing.html .
Thanks to Anatoly for pushing and initial work on this.
-- fix issue650: new option ``--docttest-ignore-import-errors`` which
+- fix issue650: new option ``--doctest-ignore-import-errors`` which
will turn import errors in doctests into skips. Thanks Charles Cloud
for the complete PR.
diff --git a/doc/en/announce/release-8.2.1.rst b/doc/en/announce/release-8.2.1.rst
new file mode 100644
index 00000000000..4452edec110
--- /dev/null
+++ b/doc/en/announce/release-8.2.1.rst
@@ -0,0 +1,19 @@
+pytest-8.2.1
+=======================================
+
+pytest 8.2.1 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+ pip install --upgrade pytest
+
+The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
+
+Thanks to all of the contributors to this release:
+
+* Bruno Oliveira
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/doc/en/announce/release-8.2.2.rst b/doc/en/announce/release-8.2.2.rst
new file mode 100644
index 00000000000..3b1d93bd08b
--- /dev/null
+++ b/doc/en/announce/release-8.2.2.rst
@@ -0,0 +1,19 @@
+pytest-8.2.2
+=======================================
+
+pytest 8.2.2 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+ pip install --upgrade pytest
+
+The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
+
+Thanks to all of the contributors to this release:
+
+* Bruno Oliveira
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/doc/en/announce/sprint2016.rst b/doc/en/announce/sprint2016.rst
index 8e706589876..8d47a205c71 100644
--- a/doc/en/announce/sprint2016.rst
+++ b/doc/en/announce/sprint2016.rst
@@ -49,7 +49,7 @@ place on 20th, 21st, 22nd, 24th and 25th. On the 23rd we took a break
day for some hot hiking in the Black Forest.
Sprint activity was organised heavily around pairing, with plenty of group
-discusssions to take advantage of the high bandwidth, and lightning talks
+discussions to take advantage of the high bandwidth, and lightning talks
as well.
diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst
index d5f2e9a1b0f..8dfffb0828a 100644
--- a/doc/en/builtin.rst
+++ b/doc/en/builtin.rst
@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 0 items
- cache -- .../_pytest/cacheprovider.py:542
+ cache -- .../_pytest/cacheprovider.py:560
Return a cache object that can persist state between testing sessions.
cache.get(key, default)
@@ -115,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`.
- pytestconfig [session scope] -- .../_pytest/fixtures.py:1335
+ pytestconfig [session scope] -- .../_pytest/fixtures.py:1338
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst
index 2630d95cf28..3ab2307ee5d 100644
--- a/doc/en/changelog.rst
+++ b/doc/en/changelog.rst
@@ -28,9 +28,88 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
+pytest 8.2.2 (2024-06-04)
+=========================
+
+Bug Fixes
+---------
+
+- `#12355 `_: Fix possible catastrophic performance slowdown on a certain parametrization pattern involving many higher-scoped parameters.
+
+
+- `#12367 `_: Fix a regression in pytest 8.2.0 where unittest class instances (a fresh one is created for each test) were not released promptly on test teardown but only on session teardown.
+
+
+- `#12381 `_: Fix possible "Directory not empty" crashes arising from concurent cache dir (``.pytest_cache``) creation. Regressed in pytest 8.2.0.
+
+
+
+Improved Documentation
+----------------------
+
+- `#12290 `_: Updated Sphinx theme to use Furo instead of Flask, enabling Dark mode theme.
+
+
+- `#12356 `_: Added a subsection to the documentation for debugging flaky tests to mention
+ lack of thread safety in pytest as a possible source of flakyness.
+
+
+- `#12363 `_: The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results.
+
+
+pytest 8.2.1 (2024-05-19)
+=========================
+
+Improvements
+------------
+
+- `#12334 `_: Support for Python 3.13 (beta1 at the time of writing).
+
+
+
+Bug Fixes
+---------
+
+- `#12120 `_: Fix `PermissionError` crashes arising from directories which are not selected on the command-line.
+
+
+- `#12191 `_: Keyboard interrupts and system exits are now properly handled during the test collection.
+
+
+- `#12300 `_: Fixed handling of 'Function not implemented' error under squashfuse_ll, which is a different way to say that the mountpoint is read-only.
+
+
+- `#12308 `_: Fix a regression in pytest 8.2.0 where the permissions of automatically-created ``.pytest_cache`` directories became ``rwx------`` instead of the expected ``rwxr-xr-x``.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#12333 `_: pytest releases are now attested using the recent `Artifact Attestation `_ support from GitHub, allowing users to verify the provenance of pytest's sdist and wheel artifacts.
+
+
pytest 8.2.0 (2024-04-27)
=========================
+Breaking Changes
+----------------
+
+- `#12089 `_: pytest now requires that :class:`unittest.TestCase` subclasses can be instantiated freely using ``MyTestCase('runTest')``.
+
+ If the class doesn't allow this, you may see an error during collection such as ``AttributeError: 'MyTestCase' object has no attribute 'runTest'``.
+
+ Classes which do not override ``__init__``, or do not access the test method in ``__init__`` using ``getattr`` or similar, are unaffected.
+
+ Classes which do should take care to not crash when ``"runTest"`` is given, as is shown in `unittest.TestCases's implementation `_.
+ Alternatively, consider using :meth:`setUp ` instead of ``__init__``.
+
+ If you run into this issue using ``tornado.AsyncTestCase``, please see `issue 12263 `_.
+
+ If you run into this issue using an abstract ``TestCase`` subclass, please see `issue 12275 `_.
+
+ Historical note: the effect of this change on custom TestCase implementations was not properly considered initially, this is why it was done in a minor release. We apologize for the inconvenience.
+
Deprecations
------------
@@ -150,7 +229,7 @@ Improvements
- `#11311 `_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
as the relative directory.
- Previoulsy this would raise an :class:`AssertionError`.
+ Previously this would raise an :class:`AssertionError`.
- `#11475 `_: :ref:`--import-mode=importlib ` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
@@ -1393,7 +1472,7 @@ Deprecations
``__init__`` method, they should take ``**kwargs``. See
:ref:`uncooperative-constructors-deprecated` for details.
- Note that a deprection warning is only emitted when there is a conflict in the
+ Note that a deprecation warning is only emitted when there is a conflict in the
arguments pytest expected to pass. This deprecation was already part of pytest
7.0.0rc1 but wasn't documented.
@@ -1435,7 +1514,7 @@ Breaking Changes
- `#7259 `_: The :ref:`Node.reportinfo() ` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
- Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
+ Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffected.
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
@@ -1943,7 +2022,7 @@ Bug Fixes
the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with
private permissions.
- pytest used to silently use a pre-existing ``/tmp/pytest-of-`` directory,
+ pytest used to silently use a preexisting ``/tmp/pytest-of-`` directory,
even if owned by another user. This means another user could pre-create such a
directory and gain control of another user's temporary directory. Now such a
condition results in an error.
@@ -2670,7 +2749,7 @@ Features
also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules
that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``).
- ``--import-mode=importlib`` uses more fine grained import mechanisms from ``importlib`` which don't
+ ``--import-mode=importlib`` uses more fine-grained import mechanisms from ``importlib`` which don't
require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks
of the previous mode.
@@ -2687,7 +2766,7 @@ Improvements
------------
- :issue:`4375`: The ``pytest`` command now suppresses the ``BrokenPipeError`` error message that
- is printed to stderr when the output of ``pytest`` is piped and and the pipe is
+ is printed to stderr when the output of ``pytest`` is piped and the pipe is
closed by the piped-to program (common examples are ``less`` and ``head``).
@@ -2989,7 +3068,7 @@ Breaking Changes
This hook has been marked as deprecated and not been even called by pytest for over 10 years now.
-- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that sth. expected is missing in the result and "+" means that there are unexpected extras in the result.
+- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that something expected is missing in the result and "+" means that there are unexpected extras in the result.
- :issue:`6737`: The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when
@@ -4594,7 +4673,7 @@ Bug Fixes
Improved Documentation
----------------------
-- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability liminations
+- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability limitations
@@ -6452,7 +6531,7 @@ Features
Bug Fixes
---------
-- Fix hanging pexpect test on MacOS by using flush() instead of wait().
+- Fix hanging pexpect test on macOS by using flush() instead of wait().
(:issue:`2022`)
- Fix restoring Python state after in-process pytest runs with the
@@ -6500,7 +6579,7 @@ Trivial/Internal Changes
------------------------
- Show a simple and easy error when keyword expressions trigger a syntax error
- (for example, ``"-k foo and import"`` will show an error that you can not use
+ (for example, ``"-k foo and import"`` will show an error that you cannot use
the ``import`` keyword in expressions). (:issue:`2953`)
- Change parametrized automatic test id generation to use the ``__name__``
@@ -8276,7 +8355,7 @@ time or change existing behaviors in order to make them less surprising/more use
one will also have a "reprec" attribute with the recorded events/reports.
- fix monkeypatch.setattr("x.y", raising=False) to actually not raise
- if "y" is not a pre-existing attribute. Thanks Florian Bruhin.
+ if "y" is not a preexisting attribute. Thanks Florian Bruhin.
- fix issue741: make running output from testdir.run copy/pasteable
Thanks Bruno Oliveira.
@@ -8332,7 +8411,7 @@ time or change existing behaviors in order to make them less surprising/more use
- fix issue854: autouse yield_fixtures defined as class members of
unittest.TestCase subclasses now work as expected.
- Thannks xmo-odoo for the report and Bruno Oliveira for the PR.
+ Thanks xmo-odoo for the report and Bruno Oliveira for the PR.
- fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the
fixtures declared on the first one.
@@ -8436,7 +8515,7 @@ time or change existing behaviors in order to make them less surprising/more use
github. See https://pytest.org/en/stable/contributing.html .
Thanks to Anatoly for pushing and initial work on this.
-- fix issue650: new option ``--docttest-ignore-import-errors`` which
+- fix issue650: new option ``--doctest-ignore-import-errors`` which
will turn import errors in doctests into skips. Thanks Charles Cloud
for the complete PR.
@@ -8624,7 +8703,7 @@ time or change existing behaviors in order to make them less surprising/more use
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
Gospodnetic for the PR.
-- change XPASS colour to yellow rather then red when tests are run
+- change XPASS colour to yellow rather than red when tests are run
with -v.
- fix issue473: work around mock putting an unbound method into a class
@@ -8797,7 +8876,7 @@ time or change existing behaviors in order to make them less surprising/more use
Thanks Ralph Schmitt for the precise failure example.
- fix issue244 by implementing special index for parameters to only use
- indices for paramentrized test ids
+ indices for parametrized test ids
- fix issue287 by running all finalizers but saving the exception
from the first failing finalizer and re-raising it so teardown will
@@ -8805,7 +8884,7 @@ time or change existing behaviors in order to make them less surprising/more use
it might be the cause for other finalizers to fail.
- fix ordering when mock.patch or other standard decorator-wrappings
- are used with test methods. This fixues issue346 and should
+ are used with test methods. This fixes issue346 and should
help with random "xdist" collection failures. Thanks to
Ronny Pfannschmidt and Donald Stufft for helping to isolate it.
@@ -9062,7 +9141,7 @@ Bug fixes:
partially failed (finalizers would not always be called before)
- fix issue320 - fix class scope for fixtures when mixed with
- module-level functions. Thanks Anatloy Bubenkoff.
+ module-level functions. Thanks Anatoly Bubenkoff.
- you can specify "-q" or "-qq" to get different levels of "quieter"
reporting (thanks Katarzyna Jachim)
@@ -9484,7 +9563,7 @@ Bug fixes:
unexpected exceptions
- fix issue47: timing output in junitxml for test cases is now correct
- fix issue48: typo in MarkInfo repr leading to exception
-- fix issue49: avoid confusing error when initizaliation partially fails
+- fix issue49: avoid confusing error when initialization partially fails
- fix issue44: env/username expansion for junitxml file path
- show releaselevel information in test runs for pypy
- reworked doc pages for better navigation and PDF generation
@@ -9609,7 +9688,7 @@ Bug fixes:
collection-before-running semantics were not
setup as with pytest 1.3.4. Note, however, that
the recommended and much cleaner way to do test
- parametraization remains the "pytest_generate_tests"
+ parameterization remains the "pytest_generate_tests"
mechanism, see the docs.
2.0.0 (2010-11-25)
diff --git a/doc/en/conf.py b/doc/en/conf.py
index 32ecaa17435..48b40ef9079 100644
--- a/doc/en/conf.py
+++ b/doc/en/conf.py
@@ -15,9 +15,7 @@
#
# The full version, including alpha/beta/rc tags.
# The short X.Y version.
-import os
import shutil
-import sys
from textwrap import dedent
from typing import TYPE_CHECKING
@@ -65,7 +63,6 @@
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
- "pallets_sphinx_themes",
"pygments_pytest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
@@ -140,10 +137,6 @@
# output. They are ignored by default.
# show_authors = False
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = "sphinx"
-
-
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@@ -216,12 +209,9 @@
# -- Options for HTML output ---------------------------------------------------
-sys.path.append(os.path.abspath("_themes"))
-html_theme_path = ["_themes"]
-
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = "flask"
+html_theme = "furo"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -266,18 +256,24 @@
html_sidebars = {
"index": [
- "slim_searchbox.html",
+ "sidebar/brand.html",
+ "sidebar/search.html",
+ "sidebar/scroll-start.html",
"sidebarintro.html",
"globaltoc.html",
"links.html",
- "sourcelink.html",
+ "sidebar/scroll-end.html",
+ "style.html",
],
"**": [
- "slim_searchbox.html",
+ "sidebar/brand.html",
+ "sidebar/search.html",
+ "sidebar/scroll-start.html",
"globaltoc.html",
"relations.html",
"links.html",
- "sourcelink.html",
+ "sidebar/scroll-end.html",
+ "style.html",
],
}
@@ -316,6 +312,9 @@
# Output file base name for HTML help builder.
htmlhelp_basename = "pytestdoc"
+# The base URL which points to the root of the HTML documentation. It is used
+# to indicate the location of document using the canonical link relation (#12363).
+html_baseurl = "https://docs.pytest.org/en/stable/"
# -- Options for LaTeX output --------------------------------------------------
@@ -337,10 +336,6 @@
)
]
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-latex_logo = "img/pytest1.png"
-
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst
index 5ac93f15144..bf6268a4980 100644
--- a/doc/en/deprecations.rst
+++ b/doc/en/deprecations.rst
@@ -7,10 +7,6 @@ This page lists all pytest features that are currently deprecated or have been r
The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives
should be used instead.
-.. contents::
- :depth: 3
- :local:
-
Deprecated Features
-------------------
@@ -462,7 +458,7 @@ Now :class:`~pytest.Class` collects the test methods directly.
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
using a check such as ``if isinstance(node, Instance): return``.
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
-However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
+However, to keep such uses working, a dummy type has been instanced in ``pytest.Instance`` and ``_pytest.python.Instance``,
and importing it emits a deprecation warning. This was removed in pytest 8.
diff --git a/doc/en/example/customdirectory/conftest.py b/doc/en/example/customdirectory/conftest.py
index 350893cab43..b2f68dba41a 100644
--- a/doc/en/example/customdirectory/conftest.py
+++ b/doc/en/example/customdirectory/conftest.py
@@ -21,7 +21,7 @@ def collect(self):
@pytest.hookimpl
def pytest_collect_directory(path, parent):
- # Use our custom collector for directories containing a `mainfest.json` file.
+ # Use our custom collector for directories containing a `manifest.json` file.
if path.joinpath("manifest.json").is_file():
return ManifestDirectory.from_parent(parent=parent, path=path)
# Otherwise fallback to the standard behavior.
diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst
index 1bbe2faaad0..d540bf08337 100644
--- a/doc/en/example/parametrize.rst
+++ b/doc/en/example/parametrize.rst
@@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
rootdir: /home/sweet/project
collected 8 items
-
+
@@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
rootdir: /home/sweet/project
collected 4 items
-
+
@@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
rootdir: /home/sweet/project
collected 2 items
-
+
diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst
index a383173d07e..39b799ed934 100644
--- a/doc/en/example/pythoncollection.rst
+++ b/doc/en/example/pythoncollection.rst
@@ -152,7 +152,7 @@ The test collection would look like this:
configfile: pytest.ini
collected 2 items
-
+
@@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
configfile: pytest.ini
collected 3 items
-
+
diff --git a/doc/en/explanation/flaky.rst b/doc/en/explanation/flaky.rst
index 41cbe847989..cb6c3983424 100644
--- a/doc/en/explanation/flaky.rst
+++ b/doc/en/explanation/flaky.rst
@@ -18,7 +18,7 @@ System state
Broadly speaking, a flaky test indicates that the test relies on some system state that is not being appropriately controlled - the test environment is not sufficiently isolated. Higher level tests are more likely to be flaky as they rely on more state.
-Flaky tests sometimes appear when a test suite is run in parallel (such as use of pytest-xdist). This can indicate a test is reliant on test ordering.
+Flaky tests sometimes appear when a test suite is run in parallel (such as use of `pytest-xdist`_). This can indicate a test is reliant on test ordering.
- Perhaps a different test is failing to clean up after itself and leaving behind data which causes the flaky test to fail.
- The flaky test is reliant on data from a previous test that doesn't clean up after itself, and in parallel runs that previous test is not always present
@@ -30,9 +30,22 @@ Overly strict assertion
Overly strict assertions can cause problems with floating point comparison as well as timing issues. :func:`pytest.approx` is useful here.
+Thread safety
+~~~~~~~~~~~~~
-Pytest features
-^^^^^^^^^^^^^^^
+pytest is single-threaded, executing its tests always in the same thread, sequentially, never spawning any threads itself.
+
+Even in case of plugins which run tests in parallel, for example `pytest-xdist`_, usually work by spawning multiple *processes* and running tests in batches, without using multiple threads.
+
+It is of course possible (and common) for tests and fixtures to spawn threads themselves as part of their testing workflow (for example, a fixture that starts a server thread in the background, or a test which executes production code that spawns threads), but some care must be taken:
+
+* Make sure to eventually wait on any spawned threads -- for example at the end of a test, or during the teardown of a fixture.
+* Avoid using primitives provided by pytest (:func:`pytest.warns`, :func:`pytest.raises`, etc) from multiple threads, as they are not thread-safe.
+
+If your test suite uses threads and your are seeing flaky test results, do not discount the possibility that the test is implicitly using global state in pytest itself.
+
+Related features
+^^^^^^^^^^^^^^^^
Xfail strict
~~~~~~~~~~~~
@@ -123,3 +136,6 @@ Resources
* `Flaky Tests at Google and How We Mitigate Them `_ by John Micco, 2016
* `Where do Google's flaky tests come from? `_ by Jeff Listfield, 2017
+
+
+.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist
diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst
index 5b33e308d13..85bee729ba1 100644
--- a/doc/en/getting-started.rst
+++ b/doc/en/getting-started.rst
@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
- pytest 8.2.0
+ pytest 8.2.2
.. _`simpletest`:
@@ -274,7 +274,7 @@ Continue reading
Check out additional pytest resources to help you customize tests for your unique workflow:
* ":ref:`usage`" for command line invocation examples
-* ":ref:`existingtestsuite`" for working with pre-existing tests
+* ":ref:`existingtestsuite`" for working with preexisting tests
* ":ref:`mark`" for information on the ``pytest.mark`` mechanism
* ":ref:`fixtures`" for providing a functional baseline to your tests
* ":ref:`plugins`" for managing and writing plugins
diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst
index 72b69a14681..35c3238dea7 100644
--- a/doc/en/how-to/fixtures.rst
+++ b/doc/en/how-to/fixtures.rst
@@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
rootdir: /home/sweet/project
collected 12 items
-
+
diff --git a/doc/en/index.rst b/doc/en/index.rst
index 67501c0530c..58527ea7331 100644
--- a/doc/en/index.rst
+++ b/doc/en/index.rst
@@ -1,16 +1,15 @@
:orphan:
-.. sidebar:: Next Open Trainings and Events
+.. _features:
- - `Professional Testing with Python `_, via `Python Academy `_ (3 day in-depth training):
- * **June 11th to 13th 2024**, Remote
- * **March 4th to 6th 2025**, Leipzig, Germany / Remote
- - `pytest development sprint `_, **June 17th -- 22nd 2024**
- - pytest tips and tricks for a better testsuite, `Europython 2024 `_, **July 8th -- 14th 2024** (3h), Prague
+.. sidebar:: **Next Open Trainings and Events**
- Also see :doc:`previous talks and blogposts `.
+ - `pytest development sprint `_, **June 17th -- 22nd 2024**, Klaus (AT) / Remote
+ - `pytest tips and tricks for a better testsuite `_, at `Europython 2024 `_, **July 8th -- 14th 2024** (3h), Prague (CZ)
+ - `pytest: Professionelles Testen (nicht nur) für Python `_, at `CH Open Workshoptage `_, **September 2nd 2024**, HSLU Rotkreuz (CH)
+ - `Professional Testing with Python `_, via `Python Academy `_ (3 day in-depth training), **March 4th -- 6th 2025**, Leipzig (DE) / Remote
-.. _features:
+ Also see :doc:`previous talks and blogposts `
pytest: helps you write better programs
=======================================
@@ -25,7 +24,6 @@ scale to support complex functional testing for applications and libraries.
**PyPI package name**: :pypi:`pytest`
-
A quick example
---------------
diff --git a/doc/en/naming20.rst b/doc/en/naming20.rst
index 5a81df2698d..11213066384 100644
--- a/doc/en/naming20.rst
+++ b/doc/en/naming20.rst
@@ -8,7 +8,7 @@ If you used older version of the ``py`` distribution (which
included the py.test command line tool and Python name space)
you accessed helpers and possibly collection classes through
the ``py.test`` Python namespaces. The new ``pytest``
-Python module flaty provides the same objects, following
+Python module flatly provides the same objects, following
these renaming rules::
py.test.XYZ -> pytest.XYZ
diff --git a/doc/en/reference/fixtures.rst b/doc/en/reference/fixtures.rst
index 8ba59395e3e..dff93a035ef 100644
--- a/doc/en/reference/fixtures.rst
+++ b/doc/en/reference/fixtures.rst
@@ -39,7 +39,7 @@ Built-in fixtures
Store and retrieve values across pytest runs.
:fixture:`doctest_namespace`
- Provide a dict injected into the docstests namespace.
+ Provide a dict injected into the doctests namespace.
:fixture:`monkeypatch`
Temporarily modify classes, functions, dictionaries,
diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst
index 39317497ebd..3675c7cb2ee 100644
--- a/doc/en/reference/reference.rst
+++ b/doc/en/reference/reference.rst
@@ -7,9 +7,6 @@ API Reference
This page contains the full reference to pytest's API.
-.. contents::
- :depth: 3
- :local:
Constants
---------
@@ -59,11 +56,19 @@ pytest.fail
.. autofunction:: pytest.fail(reason, [pytrace=True, msg=None])
+.. class:: pytest.fail.Exception
+
+ The exception raised by :func:`pytest.fail`.
+
pytest.skip
~~~~~~~~~~~
.. autofunction:: pytest.skip(reason, [allow_module_level=False, msg=None])
+.. class:: pytest.skip.Exception
+
+ The exception raised by :func:`pytest.skip`.
+
.. _`pytest.importorskip ref`:
pytest.importorskip
@@ -76,11 +81,19 @@ pytest.xfail
.. autofunction:: pytest.xfail
+.. class:: pytest.xfail.Exception
+
+ The exception raised by :func:`pytest.xfail`.
+
pytest.exit
~~~~~~~~~~~
.. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
+.. class:: pytest.exit.Exception
+
+ The exception raised by :func:`pytest.exit`.
+
pytest.main
~~~~~~~~~~~
@@ -246,9 +259,10 @@ Marks a test function as *expected to fail*.
to specify ``reason`` (see :ref:`condition string `).
:keyword str reason:
Reason why the test function is marked as xfail.
- :keyword Type[Exception] raises:
+ :keyword raises:
Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test.
Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works).
+ :type raises: Type[:py:exc:`Exception`]
:keyword bool run:
Whether the test function should actually be executed. If ``False``, the function will always xfail and will
@@ -1923,7 +1937,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
general:
-k EXPRESSION Only run tests which match the given substring
- expression. An expression is a Python evaluatable
+ expression. An expression is a Python evaluable
expression where all names are substring-matched
against test names and their parent classes.
Example: -k 'test_method or test_other' matches all
diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt
index 974988c8cf4..6e7221d645a 100644
--- a/doc/en/requirements.txt
+++ b/doc/en/requirements.txt
@@ -1,4 +1,3 @@
-pallets-sphinx-themes
pluggy>=1.5.0
pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0
@@ -8,4 +7,5 @@ sphinxcontrib-svg2pdfconverter
# Pin packaging because it no longer handles 'latest' version, which
# is the version that is assigned to the docs.
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
-packaging <22
+packaging
+furo
diff --git a/pyproject.toml b/pyproject.toml
index 43efacf09f8..01acfbf7660 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Testing",
"Topic :: Utilities",
diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py
index c65ce79f7e5..ee6a5597c2c 100644
--- a/src/_pytest/_code/code.py
+++ b/src/_pytest/_code/code.py
@@ -424,15 +424,14 @@ def recursionindex(self) -> Optional[int]:
# which generates code objects that have hash/value equality
# XXX needs a test
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
- # print "checking for recursion at", key
values = cache.setdefault(key, [])
+ # Since Python 3.13 f_locals is a proxy, freeze it.
+ loc = dict(entry.frame.f_locals)
if values:
- f = entry.frame
- loc = f.f_locals
for otherloc in values:
if otherloc == loc:
return i
- values.append(entry.frame.f_locals)
+ values.append(loc)
return None
diff --git a/src/_pytest/_py/error.py b/src/_pytest/_py/error.py
index 68f1eed7ec0..ab3a4ed318e 100644
--- a/src/_pytest/_py/error.py
+++ b/src/_pytest/_py/error.py
@@ -41,7 +41,7 @@ def __str__(self) -> str:
3: errno.ENOENT,
17: errno.EEXIST,
18: errno.EXDEV,
- 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable
+ 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable
22: errno.ENOTDIR,
20: errno.ENOTDIR,
267: errno.ENOTDIR,
diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py
index 7bb3693f938..9b4ec68950d 100644
--- a/src/_pytest/_py/path.py
+++ b/src/_pytest/_py/path.py
@@ -836,7 +836,7 @@ def mtime(self) -> float:
def copy(self, target, mode=False, stat=False):
"""Copy path to target.
- If mode is True, will copy copy permission from path to target.
+ If mode is True, will copy permission from path to target.
If stat is True, copy permission, last modification
time, last access time, and flags from path to target.
"""
@@ -1047,7 +1047,7 @@ def chmod(self, mode, rec=0):
def pypkgpath(self):
"""Return the Python package path by looking for the last
directory upwards which still contains an __init__.py.
- Return None if a pkgpath can not be determined.
+ Return None if a pkgpath cannot be determined.
"""
pkgpath = None
for parent in self.parts(reverse=True):
diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py
index 678471ee992..1e722f2ba15 100644
--- a/src/_pytest/assertion/rewrite.py
+++ b/src/_pytest/assertion/rewrite.py
@@ -584,7 +584,7 @@ def _write_and_reset() -> None:
# multi-line assert with message
elif lineno in seen_lines:
lines[-1] = lines[-1][:offset]
- # multi line assert with escapd newline before message
+ # multi line assert with escaped newline before message
else:
lines.append(line[:offset])
_write_and_reset()
@@ -1171,7 +1171,10 @@ def try_makedirs(cache_dir: Path) -> bool:
return False
except OSError as e:
# as of now, EROFS doesn't have an equivalent OSError-subclass
- if e.errno == errno.EROFS:
+ #
+ # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not
+ # implemented" for a read-only error
+ if e.errno in {errno.EROFS, errno.ENOSYS}:
return False
raise
return True
diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py
index cb671641041..e49c42cfcf7 100644
--- a/src/_pytest/assertion/util.py
+++ b/src/_pytest/assertion/util.py
@@ -325,7 +325,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
def _compare_eq_iterable(
left: Iterable[Any],
right: Iterable[Any],
- highligher: _HighlightFunc,
+ highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
if verbose <= 0 and not running_on_ci():
@@ -340,7 +340,7 @@ def _compare_eq_iterable(
# "right" is the expected base against which we compare "left",
# see https://github.com/pytest-dev/pytest/issues/3333
explanation.extend(
- highligher(
+ highlighter(
"\n".join(
line.rstrip()
for line in difflib.ndiff(right_formatting, left_formatting)
diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py
index e9f66f1f44f..30a71aaeb67 100755
--- a/src/_pytest/cacheprovider.py
+++ b/src/_pytest/cacheprovider.py
@@ -4,6 +4,7 @@
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
import dataclasses
+import errno
import json
import os
from pathlib import Path
@@ -213,6 +214,13 @@ def _ensure_cache_dir_and_supporting_files(self) -> None:
dir=self._cachedir.parent,
) as newpath:
path = Path(newpath)
+
+ # Reset permissions to the default, see #12308.
+ # Note: there's no way to get the current umask atomically, eek.
+ umask = os.umask(0o022)
+ os.umask(umask)
+ path.chmod(0o777 - umask)
+
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
f.write(README_CONTENT)
with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f:
@@ -220,14 +228,24 @@ def _ensure_cache_dir_and_supporting_files(self) -> None:
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
f.write(CACHEDIR_TAG_CONTENT)
- path.rename(self._cachedir)
- # Create a directory in place of the one we just moved so that `TemporaryDirectory`'s
- # cleanup doesn't complain.
- #
- # TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See
- # https://github.com/python/cpython/issues/74168. Note that passing delete=False would
- # do the wrong thing in case of errors and isn't supported until python 3.12.
- path.mkdir()
+ try:
+ path.rename(self._cachedir)
+ except OSError as e:
+ # If 2 concurrent pytests both race to the rename, the loser
+ # gets "Directory not empty" from the rename. In this case,
+ # everything is handled so just continue (while letting the
+ # temporary directory be cleaned up).
+ if e.errno != errno.ENOTEMPTY:
+ raise
+ else:
+ # Create a directory in place of the one we just moved so that
+ # `TemporaryDirectory`'s cleanup doesn't complain.
+ #
+ # TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10.
+ # See https://github.com/python/cpython/issues/74168. Note that passing
+ # delete=False would do the wrong thing in case of errors and isn't supported
+ # until python 3.12.
+ path.mkdir()
class LFPluginCollWrapper:
@@ -244,7 +262,7 @@ def pytest_make_collect_report(
# Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths
- # Use stable sort to priorize last failed.
+ # Use stable sort to prioritize last failed.
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
return node.path in lf_paths
diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py
index 9d9411818ac..614848e0dba 100644
--- a/src/_pytest/compat.py
+++ b/src/_pytest/compat.py
@@ -53,7 +53,7 @@ def iscoroutinefunction(func: object) -> bool:
def syntax, and doesn't contain yield), or a function decorated with
@asyncio.coroutine.
- Note: copied and modified from Python 3.5's builtin couroutines.py to avoid
+ Note: copied and modified from Python 3.5's builtin coroutines.py to avoid
importing asyncio directly, which in turns also initializes the "logging"
module as a side-effect (see issue #8).
"""
diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py
index 306b14cce28..3f46073ac4a 100644
--- a/src/_pytest/config/__init__.py
+++ b/src/_pytest/config/__init__.py
@@ -462,7 +462,7 @@ def parse_hookimpl_opts(
# (see issue #1073).
if not name.startswith("pytest_"):
return None
- # Ignore names which can not be hooks.
+ # Ignore names which cannot be hooks.
if name == "pytest_plugins":
return None
@@ -574,8 +574,8 @@ def _set_initial_conftests(
self._noconftest = noconftest
self._using_pyargs = pyargs
foundanchor = False
- for intitial_path in args:
- path = str(intitial_path)
+ for initial_path in args:
+ path = str(initial_path)
# remove node-id syntax
i = path.find("::")
if i != -1:
diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py
index 09fd07422fc..647328e3bea 100644
--- a/src/_pytest/fixtures.py
+++ b/src/_pytest/fixtures.py
@@ -23,6 +23,7 @@
from typing import MutableMapping
from typing import NoReturn
from typing import Optional
+from typing import OrderedDict
from typing import overload
from typing import Sequence
from typing import Set
@@ -75,8 +76,6 @@
if TYPE_CHECKING:
- from typing import Deque
-
from _pytest.main import Session
from _pytest.python import CallSpec2
from _pytest.python import Function
@@ -207,16 +206,18 @@ def get_parametrized_fixture_keys(
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
- items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
+ items_by_argkey: Dict[
+ Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
+ ] = {}
for scope in HIGH_SCOPES:
scoped_argkeys_cache = argkeys_cache[scope] = {}
- scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
+ scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
for item in items:
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
if keys:
scoped_argkeys_cache[item] = keys
for key in keys:
- scoped_items_by_argkey[key].append(item)
+ scoped_items_by_argkey[key][item] = None
items_dict = dict.fromkeys(items, None)
return list(
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
@@ -226,17 +227,19 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
def fix_cache_order(
item: nodes.Item,
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
- items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
+ items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
) -> None:
for scope in HIGH_SCOPES:
+ scoped_items_by_argkey = items_by_argkey[scope]
for key in argkeys_cache[scope].get(item, []):
- items_by_argkey[scope][key].appendleft(item)
+ scoped_items_by_argkey[key][item] = None
+ scoped_items_by_argkey[key].move_to_end(item, last=False)
def reorder_items_atscope(
items: Dict[nodes.Item, None],
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
- items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
+ items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
scope: Scope,
) -> Dict[nodes.Item, None]:
if scope is Scope.Function or len(items) < 3:
@@ -325,7 +328,7 @@ def prune_dependency_tree(self) -> None:
working_set = set(self.initialnames)
while working_set:
argname = working_set.pop()
- # Argname may be smth not included in the original names_closure,
+ # Argname may be something not included in the original names_closure,
# in which case we ignore it. This currently happens with pseudo
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
# So they introduce the new dependency 'request' which might have
@@ -1701,7 +1704,7 @@ def parsefactories(
If `node_or_object` is a collection node (with an underlying Python
object), the node's object is traversed and the node's nodeid is used to
- determine the fixtures' visibilty. `nodeid` must not be specified in
+ determine the fixtures' visibility. `nodeid` must not be specified in
this case.
If `node_or_object` is an object (e.g. a plugin), the object is
diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py
index acfe7eb9587..b19cd3272a9 100644
--- a/src/_pytest/hookspec.py
+++ b/src/_pytest/hookspec.py
@@ -310,7 +310,12 @@ def pytest_collection_finish(session: "Session") -> None:
def pytest_ignore_collect(
collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]:
- """Return True to prevent considering this path for collection.
+ """Return ``True`` to ignore this path for collection.
+
+ Return ``None`` to let other plugins ignore the path for collection.
+
+ Returning ``False`` will forcefully *not* ignore this path for collection,
+ without giving a chance for other plugins to ignore this path.
This hook is consulted for all files and directories prior to calling
more specific hooks.
@@ -650,7 +655,7 @@ def pytest_runtest_protocol(
- ``pytest_runtest_logreport(report)``
- ``pytest_exception_interact(call, report)`` if an interactive exception occurred
- - Call phase, if the the setup passed and the ``setuponly`` pytest option is not set:
+ - Call phase, if the setup passed and the ``setuponly`` pytest option is not set:
- ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``)
- ``report = pytest_runtest_makereport(item, call)``
- ``pytest_runtest_logreport(report)``
@@ -860,7 +865,7 @@ def pytest_fixture_setup(
) -> Optional[object]:
"""Perform fixture setup execution.
- :param fixturdef:
+ :param fixturedef:
The fixture definition object.
:param request:
The fixture request object.
@@ -890,7 +895,7 @@ def pytest_fixture_post_finalizer(
the fixture result ``fixturedef.cached_result`` is still available (not
``None``).
- :param fixturdef:
+ :param fixturedef:
The fixture definition object.
:param request:
The fixture request object.
diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py
index b28c89767fe..d9de65b1a53 100644
--- a/src/_pytest/legacypath.py
+++ b/src/_pytest/legacypath.py
@@ -384,7 +384,7 @@ def Config_inifile(self: Config) -> Optional[LEGACY_PATH]:
return legacy_path(str(self.inipath)) if self.inipath else None
-def Session_stardir(self: Session) -> LEGACY_PATH:
+def Session_startdir(self: Session) -> LEGACY_PATH:
"""The path from which pytest was invoked.
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
@@ -439,7 +439,7 @@ def pytest_load_initial_conftests(early_config: Config) -> None:
mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
# Add Session.startdir property.
- mp.setattr(Session, "startdir", property(Session_stardir), raising=False)
+ mp.setattr(Session, "startdir", property(Session_startdir), raising=False)
# Add pathlist configuration type.
mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)
diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py
index 77dabd95dec..01d6e7165f2 100644
--- a/src/_pytest/mark/__init__.py
+++ b/src/_pytest/mark/__init__.py
@@ -78,7 +78,7 @@ def pytest_addoption(parser: Parser) -> None:
default="",
metavar="EXPRESSION",
help="Only run tests which match the given substring expression. "
- "An expression is a Python evaluatable expression "
+ "An expression is a Python evaluable expression "
"where all names are substring-matched against test names "
"and their parent classes. Example: -k 'test_method or test_"
"other' matches all test functions and classes whose name "
diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py
index 76d94accd0d..f953dabe03d 100644
--- a/src/_pytest/outcomes.py
+++ b/src/_pytest/outcomes.py
@@ -114,6 +114,9 @@ def exit(
:param returncode:
Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
+
+ :raises pytest.exit.Exception:
+ The exception that is raised.
"""
__tracebackhide__ = True
raise Exit(reason, returncode)
@@ -142,6 +145,9 @@ def skip(
Defaults to False.
+ :raises pytest.skip.Exception:
+ The exception that is raised.
+
.. note::
It is better to use the :ref:`pytest.mark.skipif ref` marker when
possible to declare a test to be skipped under certain conditions
@@ -163,6 +169,9 @@ def fail(reason: str = "", pytrace: bool = True) -> NoReturn:
:param pytrace:
If False, msg represents the full failure information and no
python traceback will be reported.
+
+ :raises pytest.fail.Exception:
+ The exception that is raised.
"""
__tracebackhide__ = True
raise Failed(msg=reason, pytrace=pytrace)
@@ -188,6 +197,9 @@ def xfail(reason: str = "") -> NoReturn:
It is better to use the :ref:`pytest.mark.xfail ref` marker when
possible to declare a test to be xfailed under certain conditions
like known bugs or missing features.
+
+ :raises pytest.xfail.Exception:
+ The exception that is raised.
"""
__tracebackhide__ = True
raise XFailed(reason)
@@ -227,6 +239,9 @@ def importorskip(
:returns:
The imported module. This should be assigned to its canonical name.
+ :raises pytest.skip.Exception:
+ If the module cannot be imported.
+
Example::
docutils = pytest.importorskip("docutils")
diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py
index e14c2acd328..b11eea4e7ef 100644
--- a/src/_pytest/pathlib.py
+++ b/src/_pytest/pathlib.py
@@ -173,7 +173,7 @@ def rm_rf(path: Path) -> None:
def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]:
- """Find all elements in root that begin with the prefix, case insensitive."""
+ """Find all elements in root that begin with the prefix, case-insensitive."""
l_prefix = prefix.lower()
for x in os.scandir(root):
if x.name.lower().startswith(l_prefix):
@@ -776,7 +776,7 @@ def resolve_package_path(path: Path) -> Optional[Path]:
"""Return the Python package path by looking for the last
directory upwards which still contains an __init__.py.
- Returns None if it can not be determined.
+ Returns None if it cannot be determined.
"""
result = None
for parent in itertools.chain((path,), path.parents):
diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py
index 23f44da69ca..9ba8e6a8182 100644
--- a/src/_pytest/pytester.py
+++ b/src/_pytest/pytester.py
@@ -289,7 +289,8 @@ def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None:
__tracebackhide__ = True
i = 0
entries = list(entries)
- backlocals = sys._getframe(1).f_locals
+ # Since Python 3.13, f_locals is not a dict, but eval requires a dict.
+ backlocals = dict(sys._getframe(1).f_locals)
while entries:
name, check = entries.pop(0)
for ind, call in enumerate(self.calls[i:]):
@@ -760,6 +761,9 @@ def _makefile(
) -> Path:
items = list(files.items())
+ if ext is None:
+ raise TypeError("ext must not be None")
+
if ext and not ext.startswith("."):
raise ValueError(
f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
diff --git a/src/_pytest/python.py b/src/_pytest/python.py
index 5e059f2c4e6..41a2fe39af3 100644
--- a/src/_pytest/python.py
+++ b/src/_pytest/python.py
@@ -176,7 +176,12 @@ def pytest_collect_directory(
path: Path, parent: nodes.Collector
) -> Optional[nodes.Collector]:
pkginit = path / "__init__.py"
- if pkginit.is_file():
+ try:
+ has_pkginit = pkginit.is_file()
+ except PermissionError:
+ # See https://github.com/pytest-dev/pytest/issues/12120#issuecomment-2106349096.
+ return None
+ if has_pkginit:
return Package.from_parent(parent, path=path)
return None
diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py
index 9bc544ea742..d15a682f979 100644
--- a/src/_pytest/runner.py
+++ b/src/_pytest/runner.py
@@ -388,7 +388,9 @@ def collect() -> List[Union[Item, Collector]]:
return list(collector.collect())
- call = CallInfo.from_call(collect, "collect")
+ call = CallInfo.from_call(
+ collect, "collect", reraise=(KeyboardInterrupt, SystemExit)
+ )
longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
if not call.excinfo:
outcome: Literal["passed", "skipped", "failed"] = "passed"
diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py
index 3ebebc288f8..92d3a297e0d 100644
--- a/src/_pytest/stepwise.py
+++ b/src/_pytest/stepwise.py
@@ -40,7 +40,7 @@ def pytest_addoption(parser: Parser) -> None:
@pytest.hookimpl
def pytest_configure(config: Config) -> None:
if config.option.stepwise_skip:
- # allow --stepwise-skip to work on it's own merits.
+ # allow --stepwise-skip to work on its own merits.
config.option.stepwise = True
if config.getoption("stepwise"):
config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py
index 8f1791bf744..baca58bd9b2 100644
--- a/src/_pytest/unittest.py
+++ b/src/_pytest/unittest.py
@@ -212,11 +212,12 @@ def setup(self) -> None:
super().setup()
def teardown(self) -> None:
- super().teardown()
if self._explicit_tearDown is not None:
self._explicit_tearDown()
self._explicit_tearDown = None
self._obj = None
+ self._instance = None
+ super().teardown()
def startTest(self, testcase: "unittest.TestCase") -> None:
pass
diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py
index ad2526571e6..1b5b344551c 100644
--- a/testing/_py/test_local.py
+++ b/testing/_py/test_local.py
@@ -667,7 +667,7 @@ def test_tilde_expansion(self, monkeypatch, tmpdir):
assert p == os.path.expanduser("~")
@pytest.mark.skipif(
- not sys.platform.startswith("win32"), reason="case insensitive only on windows"
+ not sys.platform.startswith("win32"), reason="case-insensitive only on windows"
)
def test_eq_hash_are_case_insensitive_on_windows(self):
a = local("/some/path")
@@ -898,7 +898,7 @@ def test_sysfind_bat_exe_before(self, tmpdir, monkeypatch):
class TestExecution:
pytestmark = skiponwin32
- def test_sysfind_no_permisson_ignored(self, monkeypatch, tmpdir):
+ def test_sysfind_no_permission_ignored(self, monkeypatch, tmpdir):
noperm = tmpdir.ensure("noperm", dir=True)
monkeypatch.setenv("PATH", str(noperm), prepend=":")
noperm.chmod(0)
diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py
index dd4bd22c8b8..e95510f92d6 100644
--- a/testing/code/test_excinfo.py
+++ b/testing/code/test_excinfo.py
@@ -1,6 +1,7 @@
# mypy: allow-untyped-defs
from __future__ import annotations
+import fnmatch
import importlib
import io
import operator
@@ -237,7 +238,7 @@ def f(n):
n += 1
f(n)
- excinfo = pytest.raises(RuntimeError, f, 8)
+ excinfo = pytest.raises(RecursionError, f, 8)
traceback = excinfo.traceback
recindex = traceback.recursionindex()
assert recindex == 3
@@ -373,7 +374,10 @@ def test_excinfo_no_sourcecode():
except ValueError:
excinfo = _pytest._code.ExceptionInfo.from_current()
s = str(excinfo.traceback[-1])
- assert s == " File '':1 in \n ???\n"
+ # TODO: Since Python 3.13b1 under pytest-xdist, the * is `import
+ # sys;exec(eval(sys.stdin.readline()))` (execnet bootstrap code)
+ # instead of `???` like before. Is this OK?
+ fnmatch.fnmatch(s, " File '':1 in \n *\n")
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
@@ -1515,7 +1519,7 @@ def test(tmp_path):
result.stderr.no_fnmatch_line("*INTERNALERROR*")
-def test_regression_nagative_line_index(pytester: Pytester) -> None:
+def test_regression_negative_line_index(pytester: Pytester) -> None:
"""
With Python 3.10 alphas, there was an INTERNALERROR reported in
https://github.com/pytest-dev/pytest/pull/8227
diff --git a/testing/code/test_source.py b/testing/code/test_source.py
index 12ea27b3517..a00259976c4 100644
--- a/testing/code/test_source.py
+++ b/testing/code/test_source.py
@@ -255,7 +255,7 @@ def g():
assert str(g_source).strip() == "def g():\n pass # pragma: no cover"
-def test_getfuncsource_with_multine_string() -> None:
+def test_getfuncsource_with_multiline_string() -> None:
def f():
c = """while True:
pass
@@ -370,7 +370,11 @@ class B:
pass
B.__name__ = B.__qualname__ = "B2"
- assert getfslineno(B)[1] == -1
+ # Since Python 3.13 this started working.
+ if sys.version_info >= (3, 13):
+ assert getfslineno(B)[1] != -1
+ else:
+ assert getfslineno(B)[1] == -1
def test_code_of_object_instance_with_call() -> None:
diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py
index 2e16913f099..c1cfff632af 100644
--- a/testing/logging/test_fixture.py
+++ b/testing/logging/test_fixture.py
@@ -117,7 +117,7 @@ def test2(caplog):
result.stdout.no_fnmatch_line("*log from test2*")
-def test_change_level_undos_handler_level(pytester: Pytester) -> None:
+def test_change_level_undoes_handler_level(pytester: Pytester) -> None:
"""Ensure that 'set_level' is undone after the end of the test (handler).
Issue #7569. Tests the handler level specifically.
@@ -302,7 +302,15 @@ def logging_during_setup_and_teardown(
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
-def test_caplog_captures_for_all_stages(
+def private_assert_caplog_records_is_setup_call(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ # This reaches into private API, don't use this type of thing in real tests!
+ caplog_records = caplog._item.stash[caplog_records_key]
+ assert set(caplog_records) == {"setup", "call"}
+
+
+def test_captures_for_all_stages(
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
) -> None:
assert not caplog.records
@@ -312,9 +320,7 @@ def test_caplog_captures_for_all_stages(
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
- # This reaches into private API, don't use this type of thing in real tests!
- caplog_records = caplog._item.stash[caplog_records_key]
- assert set(caplog_records) == {"setup", "call"}
+ private_assert_caplog_records_is_setup_call(caplog)
def test_clear_for_call_stage(
@@ -323,21 +329,18 @@ def test_clear_for_call_stage(
logger.info("a_call_log")
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
- caplog_records = caplog._item.stash[caplog_records_key]
- assert set(caplog_records) == {"setup", "call"}
+ private_assert_caplog_records_is_setup_call(caplog)
caplog.clear()
assert caplog.get_records("call") == []
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
- caplog_records = caplog._item.stash[caplog_records_key]
- assert set(caplog_records) == {"setup", "call"}
+ private_assert_caplog_records_is_setup_call(caplog)
logging.info("a_call_log_after_clear")
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
- caplog_records = caplog._item.stash[caplog_records_key]
- assert set(caplog_records) == {"setup", "call"}
+ private_assert_caplog_records_is_setup_call(caplog)
def test_ini_controls_global_log_level(pytester: Pytester) -> None:
@@ -363,11 +366,11 @@ def test_log_level_override(request, caplog):
)
result = pytester.runpytest()
- # make sure that that we get a '0' exit code for the testsuite
+ # make sure that we get a '0' exit code for the testsuite
assert result.ret == 0
-def test_caplog_can_override_global_log_level(pytester: Pytester) -> None:
+def test_can_override_global_log_level(pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest
@@ -406,7 +409,7 @@ def test_log_level_override(request, caplog):
assert result.ret == 0
-def test_caplog_captures_despite_exception(pytester: Pytester) -> None:
+def test_captures_despite_exception(pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest
diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py
index 12ca6e92630..ebdf451e40b 100644
--- a/testing/python/fixtures.py
+++ b/testing/python/fixtures.py
@@ -933,7 +933,7 @@ def test_request_subrequest_addfinalizer_exceptions(
) -> None:
"""
Ensure exceptions raised during teardown by finalizers are suppressed
- until all finalizers are called, then re-reaised together in an
+ until all finalizers are called, then re-raised together in an
exception group (#2440)
"""
pytester.makepyfile(
@@ -2219,6 +2219,25 @@ def test_check():
reprec = pytester.inline_run("-s")
reprec.assertoutcome(passed=2)
+ def test_reordering_catastrophic_performance(self, pytester: Pytester) -> None:
+ """Check that a certain high-scope parametrization pattern doesn't cause
+ a catasrophic slowdown.
+
+ Regression test for #12355.
+ """
+ pytester.makepyfile("""
+ import pytest
+
+ params = tuple("abcdefghijklmnopqrstuvwxyz")
+ @pytest.mark.parametrize(params, [range(len(params))] * 3, scope="module")
+ def test_parametrize(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z):
+ pass
+ """)
+
+ result = pytester.runpytest()
+
+ result.assert_outcomes(passed=3)
+
class TestFixtureMarker:
def test_parametrize(self, pytester: Pytester) -> None:
diff --git a/testing/python/integration.py b/testing/python/integration.py
index 219ebf9cec8..c20aaeed839 100644
--- a/testing/python/integration.py
+++ b/testing/python/integration.py
@@ -163,7 +163,7 @@ def mock_basename(path):
@mock.patch("os.path.abspath")
@mock.patch("os.path.normpath")
@mock.patch("os.path.basename", new=mock_basename)
- def test_someting(normpath, abspath, tmp_path):
+ def test_something(normpath, abspath, tmp_path):
abspath.return_value = "this"
os.path.normpath(os.path.abspath("hello"))
normpath.assert_any_call("this")
@@ -176,7 +176,7 @@ def test_someting(normpath, abspath, tmp_path):
funcnames = [
call.report.location[2] for call in calls if call.report.when == "call"
]
- assert funcnames == ["T.test_hello", "test_someting"]
+ assert funcnames == ["T.test_hello", "test_something"]
def test_mock_sorting(self, pytester: Pytester) -> None:
pytest.importorskip("mock", "1.0.1")
diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py
index 7acc8cdf1d9..ac93c57dbd7 100644
--- a/testing/test_assertrewrite.py
+++ b/testing/test_assertrewrite.py
@@ -1974,6 +1974,11 @@ def fake_mkdir(p, exist_ok=False, *, exc):
monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err))
assert not try_makedirs(p)
+ err = OSError()
+ err.errno = errno.ENOSYS
+ monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err))
+ assert not try_makedirs(p)
+
# unhandled OSError should raise
err = OSError()
err.errno = errno.ECHILD
diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py
index 304e5414abc..ea662e87f07 100644
--- a/testing/test_cacheprovider.py
+++ b/testing/test_cacheprovider.py
@@ -31,6 +31,21 @@ def test_config_cache_mkdir(self, pytester: Pytester) -> None:
p = config.cache.mkdir("name")
assert p.is_dir()
+ def test_cache_dir_permissions(self, pytester: Pytester) -> None:
+ """The .pytest_cache directory should have world-readable permissions
+ (depending on umask).
+
+ Regression test for #12308.
+ """
+ pytester.makeini("[pytest]")
+ config = pytester.parseconfigure()
+ assert config.cache is not None
+ p = config.cache.mkdir("name")
+ assert p.is_dir()
+ # Instead of messing with umask, make sure .pytest_cache has the same
+ # permissions as the default that `mkdir` gives `p`.
+ assert (p.parent.stat().st_mode & 0o777) == (p.stat().st_mode & 0o777)
+
def test_config_cache_dataerror(self, pytester: Pytester) -> None:
pytester.makeini("[pytest]")
config = pytester.parseconfigure()
@@ -43,7 +58,7 @@ def test_config_cache_dataerror(self, pytester: Pytester) -> None:
assert val == -2
@pytest.mark.filterwarnings("ignore:could not create cache path")
- def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None:
+ def test_cache_writefail_cachefile_silent(self, pytester: Pytester) -> None:
pytester.makeini("[pytest]")
pytester.path.joinpath(".pytest_cache").write_text(
"gone wrong", encoding="utf-8"
@@ -179,7 +194,7 @@ def test_custom_cache_dir_with_env_var(
assert pytester.path.joinpath("custom_cache_dir").is_dir()
-@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir")))
+@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "mydir/tox-env")))
def test_cache_reportheader(
env: Sequence[str], pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
diff --git a/testing/test_collection.py b/testing/test_collection.py
index 1491ec85990..7f0790693a5 100644
--- a/testing/test_collection.py
+++ b/testing/test_collection.py
@@ -7,6 +7,7 @@
import tempfile
import textwrap
from typing import List
+from typing import Type
from _pytest.assertion.util import running_on_ci
from _pytest.config import ExitCode
@@ -284,6 +285,23 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No
items, reprec = pytester.inline_genitems()
assert [x.name for x in items] == ["test_%s" % dirname]
+ def test_missing_permissions_on_unselected_directory_doesnt_crash(
+ self, pytester: Pytester
+ ) -> None:
+ """Regression test for #12120."""
+ test = pytester.makepyfile(test="def test(): pass")
+ bad = pytester.mkdir("bad")
+ try:
+ bad.chmod(0)
+
+ result = pytester.runpytest(test)
+ finally:
+ bad.chmod(750)
+ bad.rmdir()
+
+ assert result.ret == ExitCode.OK
+ result.assert_outcomes(passed=1)
+
class TestCollectPluginHookRelay:
def test_pytest_collect_file(self, pytester: Pytester) -> None:
@@ -1857,3 +1875,33 @@ def test_do_not_collect_symlink_siblings(
# Ensure we collect it only once if we pass the symlinked directory.
result = pytester.runpytest(symlink_path, "-sv")
result.assert_outcomes(passed=1)
+
+
+@pytest.mark.parametrize(
+ "exception_class, msg",
+ [
+ (KeyboardInterrupt, "*!!! KeyboardInterrupt !!!*"),
+ (SystemExit, "INTERNALERROR> SystemExit"),
+ ],
+)
+def test_respect_system_exceptions(
+ pytester: Pytester,
+ exception_class: Type[BaseException],
+ msg: str,
+):
+ head = "Before exception"
+ tail = "After exception"
+ ensure_file(pytester.path / "test_eggs.py").write_text(
+ f"print('{head}')", encoding="UTF-8"
+ )
+ ensure_file(pytester.path / "test_ham.py").write_text(
+ f"raise {exception_class.__name__}()", encoding="UTF-8"
+ )
+ ensure_file(pytester.path / "test_spam.py").write_text(
+ f"print('{tail}')", encoding="UTF-8"
+ )
+
+ result = pytester.runpytest_subprocess("-s")
+ result.stdout.fnmatch_lines([f"*{head}*"])
+ result.stdout.fnmatch_lines([msg])
+ result.stdout.no_fnmatch_line(f"*{tail}*")
diff --git a/testing/test_compat.py b/testing/test_compat.py
index c898af7c531..73ac1bad858 100644
--- a/testing/test_compat.py
+++ b/testing/test_compat.py
@@ -94,7 +94,7 @@ def foo(x):
assert get_real_func(partial(foo)) is foo
-@pytest.mark.skipif(sys.version_info >= (3, 11), reason="couroutine removed")
+@pytest.mark.skipif(sys.version_info >= (3, 11), reason="coroutine removed")
def test_is_generator_asyncio(pytester: Pytester) -> None:
pytester.makepyfile(
"""
diff --git a/testing/test_config.py b/testing/test_config.py
index 147c2cb851c..0d097f71622 100644
--- a/testing/test_config.py
+++ b/testing/test_config.py
@@ -217,7 +217,7 @@ def test_toml_parse_error(self, pytester: Pytester) -> None:
def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None:
# If --confcutdir is not specified, and there is no configfile, default
- # to the roothpath.
+ # to the rootpath.
sub = pytester.mkdir("sub")
os.chdir(sub)
config = pytester.parseconfigure()
@@ -1740,7 +1740,7 @@ def pytest_addoption(parser):
)
pytester.makepyfile(
r"""
- def test_overriden(pytestconfig):
+ def test_overridden(pytestconfig):
config_paths = pytestconfig.getini("paths")
print(config_paths)
for cpf in config_paths:
diff --git a/testing/test_conftest.py b/testing/test_conftest.py
index 3116dfe2584..06438f082a9 100644
--- a/testing/test_conftest.py
+++ b/testing/test_conftest.py
@@ -70,7 +70,7 @@ def test_basic_init(self, basedir: Path) -> None:
)
assert conftest._rget_with_confmod("a", p)[1] == 1
- def test_immediate_initialiation_and_incremental_are_the_same(
+ def test_immediate_initialization_and_incremental_are_the_same(
self, basedir: Path
) -> None:
conftest = PytestPluginManager()
@@ -396,7 +396,7 @@ def fixture():
@pytest.mark.skipif(
os.path.normcase("x") != os.path.normcase("X"),
- reason="only relevant for case insensitive file systems",
+ reason="only relevant for case-insensitive file systems",
)
def test_conftest_badcase(pytester: Pytester) -> None:
"""Check conftest.py loading when directory casing is wrong (#5792)."""
diff --git a/testing/test_debugging.py b/testing/test_debugging.py
index 53ebadbdba4..7582dac6742 100644
--- a/testing/test_debugging.py
+++ b/testing/test_debugging.py
@@ -1122,7 +1122,7 @@ def test_func_kw(myparam, request, func="func_kw"):
def test_trace_after_runpytest(pytester: Pytester) -> None:
- """Test that debugging's pytest_configure is re-entrant."""
+ """Test that debugging's pytest_configure is reentrant."""
p1 = pytester.makepyfile(
"""
from _pytest.debugging import pytestPDB
@@ -1153,7 +1153,7 @@ def test_inner():
def test_quit_with_swallowed_SystemExit(pytester: Pytester) -> None:
- """Test that debugging's pytest_configure is re-entrant."""
+ """Test that debugging's pytest_configure is reentrant."""
p1 = pytester.makepyfile(
"""
def call_pdb_set_trace():
diff --git a/testing/test_doctest.py b/testing/test_doctest.py
index 58fce244f45..a95dde9a6bf 100644
--- a/testing/test_doctest.py
+++ b/testing/test_doctest.py
@@ -224,11 +224,7 @@ def test_doctest_unexpected_exception(self, pytester: Pytester):
"Traceback (most recent call last):",
' File "*/doctest.py", line *, in __run',
" *",
- *(
- (" *^^^^*",)
- if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
- else ()
- ),
+ *((" *^^^^*", " *", " *") if sys.version_info >= (3, 13) else ()),
' File "", line 1, in ',
"ZeroDivisionError: division by zero",
"*/test_doctest_unexpected_exception.txt:2: UnexpectedException",
@@ -385,7 +381,7 @@ def some_property(self):
"*= FAILURES =*",
"*_ [[]doctest[]] test_doctest_linedata_on_property.Sample.some_property _*",
"004 ",
- "005 >>> Sample().some_property",
+ "005 *>>> Sample().some_property",
"Expected:",
" 'another thing'",
"Got:",
@@ -396,7 +392,7 @@ def some_property(self):
]
)
- def test_doctest_no_linedata_on_overriden_property(self, pytester: Pytester):
+ def test_doctest_no_linedata_on_overridden_property(self, pytester: Pytester):
pytester.makepyfile(
"""
class Sample(object):
@@ -414,7 +410,7 @@ def some_property(self):
result.stdout.fnmatch_lines(
[
"*= FAILURES =*",
- "*_ [[]doctest[]] test_doctest_no_linedata_on_overriden_property.Sample.some_property _*",
+ "*_ [[]doctest[]] test_doctest_no_linedata_on_overridden_property.Sample.some_property _*",
"EXAMPLE LOCATION UNKNOWN, not showing all tests of that example",
"[?][?][?] >>> Sample().some_property",
"Expected:",
@@ -422,7 +418,7 @@ def some_property(self):
"Got:",
" 'something'",
"",
- "*/test_doctest_no_linedata_on_overriden_property.py:None: DocTestFailure",
+ "*/test_doctest_no_linedata_on_overridden_property.py:None: DocTestFailure",
"*= 1 failed in *",
]
)
diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py
index 3b92d65bdb9..86edfbbeb61 100644
--- a/testing/test_junitxml.py
+++ b/testing/test_junitxml.py
@@ -1002,7 +1002,7 @@ def repr_failure(self, excinfo):
@pytest.mark.parametrize("junit_logging", ["no", "system-out"])
def test_nullbyte(pytester: Pytester, junit_logging: str) -> None:
- # A null byte can not occur in XML (see section 2.2 of the spec)
+ # A null byte cannot occur in XML (see section 2.2 of the spec)
pytester.makepyfile(
"""
import sys
diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py
index 49e620c1138..ad4e22e46b4 100644
--- a/testing/test_legacypath.py
+++ b/testing/test_legacypath.py
@@ -155,7 +155,7 @@ def pytest_addoption(parser):
)
pytester.makepyfile(
r"""
- def test_overriden(pytestconfig):
+ def test_overridden(pytestconfig):
config_paths = pytestconfig.getini("paths")
print(config_paths)
for cpf in config_paths:
diff --git a/testing/test_main.py b/testing/test_main.py
index 345aa1e62cf..6294f66b360 100644
--- a/testing/test_main.py
+++ b/testing/test_main.py
@@ -3,7 +3,6 @@
import os
from pathlib import Path
import re
-import sys
from typing import Optional
from _pytest.config import ExitCode
@@ -45,32 +44,18 @@ def pytest_internalerror(excrepr, excinfo):
assert result.ret == ExitCode.INTERNAL_ERROR
assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):"
- end_lines = (
- result.stdout.lines[-4:]
- if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
- else result.stdout.lines[-3:]
- )
+ end_lines = result.stdout.lines[-3:]
if exc == SystemExit:
assert end_lines == [
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
'INTERNALERROR> raise SystemExit("boom")',
- *(
- ("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",)
- if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
- else ()
- ),
"INTERNALERROR> SystemExit: boom",
]
else:
assert end_lines == [
f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart',
'INTERNALERROR> raise ValueError("boom")',
- *(
- ("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",)
- if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11)
- else ()
- ),
"INTERNALERROR> ValueError: boom",
]
if returncode is False:
diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py
index a7a9cf3044a..07c89f90838 100644
--- a/testing/test_mark_expression.py
+++ b/testing/test_mark_expression.py
@@ -61,7 +61,7 @@ def test_basic(expr: str, expected: bool) -> None:
("not not not not not true", False),
),
)
-def test_syntax_oddeties(expr: str, expected: bool) -> None:
+def test_syntax_oddities(expr: str, expected: bool) -> None:
matcher = {"true": True, "false": False}.__getitem__
assert evaluate(expr, matcher) is expected
diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py
index da43364f643..99b003b66ed 100644
--- a/testing/test_pluginmanager.py
+++ b/testing/test_pluginmanager.py
@@ -420,7 +420,7 @@ def test_consider_conftest_deps(
pytestpm.consider_conftest(mod, registration_name="unused")
-class TestPytestPluginManagerBootstrapming:
+class TestPytestPluginManagerBootstrapping:
def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
pytest.raises(
ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
@@ -446,7 +446,7 @@ def test_plugin_prevent_register(self, pytestpm: PytestPluginManager) -> None:
assert len(l2) == len(l1)
assert 42 not in l2
- def test_plugin_prevent_register_unregistered_alredy_registered(
+ def test_plugin_prevent_register_unregistered_already_registered(
self, pytestpm: PytestPluginManager
) -> None:
pytestpm.register(42, name="abc")
diff --git a/testing/test_runner.py b/testing/test_runner.py
index 8b41ec28a38..99c11a3d92c 100644
--- a/testing/test_runner.py
+++ b/testing/test_runner.py
@@ -409,7 +409,7 @@ def test_func():
# assert rep.outcome.when == "setup"
# assert rep.outcome.where.lineno == 3
# assert rep.outcome.where.path.basename == "test_func.py"
- # assert instanace(rep.failed.failurerepr, PythonFailureRepr)
+ # assert isinstance(rep.failed.failurerepr, PythonFailureRepr)
def test_systemexit_does_not_bail_out(self, pytester: Pytester) -> None:
try:
diff --git a/testing/test_terminal.py b/testing/test_terminal.py
index 170f1efcf91..5ed0fee82e6 100644
--- a/testing/test_terminal.py
+++ b/testing/test_terminal.py
@@ -926,7 +926,7 @@ def test_header(self, pytester: Pytester) -> None:
def test_header_absolute_testpath(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
- """Regresstion test for #7814."""
+ """Regression test for #7814."""
tests = pytester.path.joinpath("tests")
tests.mkdir()
pytester.makepyprojecttoml(
diff --git a/testing/test_unittest.py b/testing/test_unittest.py
index d726e74d603..d238ffbfa74 100644
--- a/testing/test_unittest.py
+++ b/testing/test_unittest.py
@@ -1,5 +1,4 @@
# mypy: allow-untyped-defs
-import gc
import sys
from typing import List
@@ -192,30 +191,35 @@ def test_check(self):
def test_teardown_issue1649(pytester: Pytester) -> None:
"""
Are TestCase objects cleaned up? Often unittest TestCase objects set
- attributes that are large and expensive during setUp.
+ attributes that are large and expensive during test run or setUp.
The TestCase will not be cleaned up if the test fails, because it
would then exist in the stackframe.
+
+ Regression test for #1649 (see also #12367).
"""
- testpath = pytester.makepyfile(
+ pytester.makepyfile(
"""
import unittest
- class TestCaseObjectsShouldBeCleanedUp(unittest.TestCase):
- def setUp(self):
- self.an_expensive_object = 1
- def test_demo(self):
- pass
+ import gc
- """
+ class TestCaseObjectsShouldBeCleanedUp(unittest.TestCase):
+ def test_expensive(self):
+ self.an_expensive_obj = object()
+
+ def test_is_it_still_alive(self):
+ gc.collect()
+ for obj in gc.get_objects():
+ if type(obj).__name__ == "TestCaseObjectsShouldBeCleanedUp":
+ assert not hasattr(obj, "an_expensive_obj")
+ break
+ else:
+ assert False, "Could not find TestCaseObjectsShouldBeCleanedUp instance"
+ """
)
- pytester.inline_run("-s", testpath)
- gc.collect()
-
- # Either already destroyed, or didn't run setUp.
- for obj in gc.get_objects():
- if type(obj).__name__ == "TestCaseObjectsShouldBeCleanedUp":
- assert not hasattr(obj, "an_expensive_obj")
+ result = pytester.runpytest()
+ assert result.ret == ExitCode.OK
def test_unittest_skip_issue148(pytester: Pytester) -> None:
@@ -299,7 +303,7 @@ def test_func2(self):
@classmethod
def tearDownClass(cls):
cls.x -= 1
- def test_teareddown():
+ def test_torn_down():
assert MyTestCase.x == 0
"""
)
@@ -346,7 +350,7 @@ def test_func2(self):
assert self.x == 1
def teardown_class(cls):
cls.x -= 1
- def test_teareddown():
+ def test_torn_down():
assert MyTestCase.x == 0
"""
)
@@ -881,7 +885,7 @@ def test_method1(self):
def tearDownClass(cls):
cls.x = 1
- def test_not_teareddown():
+ def test_not_torn_down():
assert TestFoo.x == 0
"""
diff --git a/tox.ini b/tox.ini
index cb3ca4b8366..30d3e68defc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,6 +9,7 @@ envlist =
py310
py311
py312
+ py313
pypy3
py38-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib}
doctesting