This commit is contained in:
Jiajun Xu 2024-06-07 23:58:48 -04:00
commit b356bd52dd
49 changed files with 650 additions and 379 deletions

View File

@ -31,7 +31,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Build and Check Package - name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.5.0 uses: hynek/build-and-inspect-python-package@v2.6.0
with: with:
attest-build-provenance-github: 'true' attest-build-provenance-github: 'true'

View File

@ -35,7 +35,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Build and Check Package - name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.5.0 uses: hynek/build-and-inspect-python-package@v2.6.0
build: build:
needs: [package] needs: [package]

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.4.4" rev: "v0.4.7"
hooks: hooks:
- id: ruff - id: ruff
args: ["--fix"] args: ["--fix"]
@ -43,6 +43,11 @@ repos:
- id: pyproject-fmt - id: pyproject-fmt
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version # https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
additional_dependencies: ["tox>=4.9"] additional_dependencies: ["tox>=4.9"]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
hooks:
- id: pyupgrade
stages: [manual]
- repo: local - repo: local
hooks: hooks:
- id: pylint - id: pylint

View File

@ -192,6 +192,7 @@ Jake VanderPlas
Jakob van Santen Jakob van Santen
Jakub Mitoraj Jakub Mitoraj
James Bourbeau James Bourbeau
James Frost
Jan Balster Jan Balster
Janne Vanhala Janne Vanhala
Jason R. Coombs Jason R. Coombs
@ -278,6 +279,7 @@ Michael Droettboom
Michael Goerz Michael Goerz
Michael Krebs Michael Krebs
Michael Seifert Michael Seifert
Michael Vogt
Michal Wajszczuk Michal Wajszczuk
Michał Górny Michał Górny
Michał Zięba Michał Zięba
@ -289,6 +291,7 @@ Mike Lundy
Milan Lesnek Milan Lesnek
Miro Hrončok Miro Hrončok
mrbean-bremen mrbean-bremen
Nathan Goldbaum
Nathaniel Compton Nathaniel Compton
Nathaniel Waisbrot Nathaniel Waisbrot
Ned Batchelder Ned Batchelder

View File

@ -1 +0,0 @@
Updated Sphinx theme to use Furo instead of Flask, enabling Dark mode theme.

View File

@ -0,0 +1 @@
Fix crash with `assert testcase is not None` assertion failure when re-running unittest tests using plugins like pytest-rerunfailures. Regressed in 8.2.2.

View File

@ -0,0 +1 @@
Do not truncate arguments to functions in output when running with `-vvv`.

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-8.2.2
release-8.2.1 release-8.2.1
release-8.2.0 release-8.2.0
release-8.1.2 release-8.1.2

View File

@ -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

View File

@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache cachedir: .pytest_cache
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 0 items collected 0 items
cache -- .../_pytest/cacheprovider.py:549 cache -- .../_pytest/cacheprovider.py:560
Return a cache object that can persist state between testing sessions. Return a cache object that can persist state between testing sessions.
cache.get(key, default) cache.get(key, default)
@ -115,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`. 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` Session-scoped fixture that returns the session's :class:`pytest.Config`
object. object.

View File

@ -28,6 +28,35 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 8.2.2 (2024-06-04)
=========================
Bug Fixes
---------
- `#12355 <https://github.com/pytest-dev/pytest/issues/12355>`_: Fix possible catastrophic performance slowdown on a certain parametrization pattern involving many higher-scoped parameters.
- `#12367 <https://github.com/pytest-dev/pytest/issues/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 <https://github.com/pytest-dev/pytest/issues/12381>`_: Fix possible "Directory not empty" crashes arising from concurent cache dir (``.pytest_cache``) creation. Regressed in pytest 8.2.0.
Improved Documentation
----------------------
- `#12290 <https://github.com/pytest-dev/pytest/issues/12290>`_: Updated Sphinx theme to use Furo instead of Flask, enabling Dark mode theme.
- `#12356 <https://github.com/pytest-dev/pytest/issues/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 <https://github.com/pytest-dev/pytest/issues/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) pytest 8.2.1 (2024-05-19)
========================= =========================

View File

@ -312,6 +312,9 @@ html_show_sourcelink = False
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = "pytestdoc" 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 -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------

View File

@ -26,19 +26,12 @@ Contact channels
<https://web.libera.chat/#pytest>`_, or `via Matrix <https://web.libera.chat/#pytest>`_, or `via Matrix
<https://matrix.to/#/%23pytest:libera.chat>`_). <https://matrix.to/#/%23pytest:libera.chat>`_).
- private mail to Holger.Krekel at gmail com if you want to communicate sensitive issues
- `merlinux.eu`_ offers pytest and tox-related professional teaching and
consulting.
.. _`pytest issue tracker`: https://github.com/pytest-dev/pytest/issues .. _`pytest issue tracker`: https://github.com/pytest-dev/pytest/issues
.. _`old issue tracker`: https://bitbucket.org/hpk42/py-trunk/issues/ .. _`old issue tracker`: https://bitbucket.org/hpk42/py-trunk/issues/
.. _`pytest discussions`: https://github.com/pytest-dev/pytest/discussions .. _`pytest discussions`: https://github.com/pytest-dev/pytest/discussions
.. _`merlinux.eu`: https://merlinux.eu/
.. _`get an account`: .. _`get an account`:
.. _tetamap: https://tetamap.wordpress.com/ .. _tetamap: https://tetamap.wordpress.com/

View File

@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 8 items collected 8 items
<Dir parametrize.rst-199> <Dir parametrize.rst-200>
<Module test_time.py> <Module test_time.py>
<Function test_timedistance_v0[a0-b0-expected0]> <Function test_timedistance_v0[a0-b0-expected0]>
<Function test_timedistance_v0[a1-b1-expected1]> <Function test_timedistance_v0[a1-b1-expected1]>
@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 4 items collected 4 items
<Dir parametrize.rst-199> <Dir parametrize.rst-200>
<Module test_scenarios.py> <Module test_scenarios.py>
<Class TestSampleWithScenarios> <Class TestSampleWithScenarios>
<Function test_demo1[basic]> <Function test_demo1[basic]>
@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 2 items collected 2 items
<Dir parametrize.rst-199> <Dir parametrize.rst-200>
<Module test_backends.py> <Module test_backends.py>
<Function test_db_initialized[d1]> <Function test_db_initialized[d1]>
<Function test_db_initialized[d2]> <Function test_db_initialized[d2]>

View File

@ -152,7 +152,7 @@ The test collection would look like this:
configfile: pytest.ini configfile: pytest.ini
collected 2 items collected 2 items
<Dir pythoncollection.rst-200> <Dir pythoncollection.rst-201>
<Module check_myapp.py> <Module check_myapp.py>
<Class CheckMyApp> <Class CheckMyApp>
<Function simple_check> <Function simple_check>
@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
configfile: pytest.ini configfile: pytest.ini
collected 3 items collected 3 items
<Dir pythoncollection.rst-200> <Dir pythoncollection.rst-201>
<Dir CWD> <Dir CWD>
<Module pythoncollection.py> <Module pythoncollection.py>
<Function test_function> <Function test_function>

View File

@ -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. 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. - 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 - 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. 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 Xfail strict
~~~~~~~~~~~~ ~~~~~~~~~~~~
@ -123,3 +136,6 @@ Resources
* `Flaky Tests at Google and How We Mitigate Them <https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html>`_ by John Micco, 2016 * `Flaky Tests at Google and How We Mitigate Them <https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html>`_ by John Micco, 2016
* `Where do Google's flaky tests come from? <https://testing.googleblog.com/2017/04/where-do-our-flaky-tests-come-from.html>`_ by Jeff Listfield, 2017 * `Where do Google's flaky tests come from? <https://testing.googleblog.com/2017/04/where-do-our-flaky-tests-come-from.html>`_ by Jeff Listfield, 2017
.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist

View File

@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash .. code-block:: bash
$ pytest --version $ pytest --version
pytest 8.2.1 pytest 8.2.2
.. _`simpletest`: .. _`simpletest`:

View File

@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 12 items collected 12 items
<Dir fixtures.rst-218> <Dir fixtures.rst-219>
<Module test_anothersmtp.py> <Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]> <Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]> <Function test_showhelo[mail.python.org]>

View File

@ -4,11 +4,10 @@
.. sidebar:: **Next Open Trainings and Events** .. sidebar:: **Next Open Trainings and Events**
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training): - `pytest development sprint <https://github.com/pytest-dev/sprint>`_, **June 17th -- 22nd 2024**, Klaus (AT) / Remote
* **June 11th to 13th 2024**, Remote - `pytest tips and tricks for a better testsuite <https://ep2024.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague (CZ)
* **March 4th to 6th 2025**, Leipzig, Germany / Remote - `pytest: Professionelles Testen (nicht nur) für Python <https://pretalx.com/workshoptage-2024/talk/9VUHYB/>`_, at `CH Open Workshoptage <https://workshoptage.ch/>`_, **September 2nd 2024**, HSLU Rotkreuz (CH)
- `pytest development sprint <https://github.com/pytest-dev/sprint>`_, **June 17th -- 22nd 2024** - `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training), **March 4th -- 6th 2025**, Leipzig (DE) / Remote
- pytest tips and tricks for a better testsuite, `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague
Also see :doc:`previous talks and blogposts <talks>` Also see :doc:`previous talks and blogposts <talks>`

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import json import json
from pathlib import Path from pathlib import Path
import sys
import requests import requests
@ -17,7 +18,7 @@ def get_issues():
if r.status_code == 403: if r.status_code == 403:
# API request limit exceeded # API request limit exceeded
print(data["message"]) print(data["message"])
exit(1) sys.exit(1)
issues.extend(data) issues.extend(data)
# Look for next page # Look for next page

View File

@ -181,28 +181,25 @@ disable = [
"bad-mcs-method-argument", "bad-mcs-method-argument",
"broad-exception-caught", "broad-exception-caught",
"broad-exception-raised", "broad-exception-raised",
"cell-var-from-loop", "cell-var-from-loop", # B023 from ruff / flake8-bugbear
"comparison-of-constants", "comparison-of-constants",
"comparison-with-callable", "comparison-with-callable",
"comparison-with-itself", "comparison-with-itself",
"condition-evals-to-constant", "condition-evals-to-constant",
"consider-using-dict-items", "consider-using-dict-items",
"consider-using-enumerate",
"consider-using-from-import", "consider-using-from-import",
"consider-using-f-string", "consider-using-f-string",
"consider-using-in", "consider-using-in",
"consider-using-sys-exit",
"consider-using-ternary", "consider-using-ternary",
"consider-using-with", "consider-using-with",
"cyclic-import", "cyclic-import",
"disallowed-name", "disallowed-name", # foo / bar are used often in tests
"duplicate-code", "duplicate-code",
"eval-used", "eval-used",
"exec-used", "exec-used",
"expression-not-assigned", "expression-not-assigned",
"fixme", "fixme",
"global-statement", "global-statement",
"implicit-str-concat",
"import-error", "import-error",
"import-outside-toplevel", "import-outside-toplevel",
"inconsistent-return-statements", "inconsistent-return-statements",
@ -213,10 +210,9 @@ disable = [
"keyword-arg-before-vararg", "keyword-arg-before-vararg",
"line-too-long", "line-too-long",
"method-hidden", "method-hidden",
"misplaced-bare-raise",
"missing-docstring", "missing-docstring",
"missing-timeout", "missing-timeout",
"multiple-statements", "multiple-statements", # multiple-statements-on-one-line-colon (E701) from ruff
"no-else-break", "no-else-break",
"no-else-continue", "no-else-continue",
"no-else-raise", "no-else-raise",
@ -229,6 +225,7 @@ disable = [
"pointless-exception-statement", "pointless-exception-statement",
"pointless-statement", "pointless-statement",
"pointless-string-statement", "pointless-string-statement",
"possibly-used-before-assignment",
"protected-access", "protected-access",
"raise-missing-from", "raise-missing-from",
"redefined-argument-from-local", "redefined-argument-from-local",
@ -276,7 +273,6 @@ disable = [
"useless-else-on-loop", "useless-else-on-loop",
"useless-import-alias", "useless-import-alias",
"useless-return", "useless-return",
"use-maxsplit-arg",
"using-constant-test", "using-constant-test",
"wrong-import-order", "wrong-import-order",
] ]

View File

@ -55,7 +55,7 @@ from _pytest.pathlib import bestrelpath
if sys.version_info < (3, 11): if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup from exceptiongroup import BaseExceptionGroup
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
class Code: class Code:
@ -199,8 +199,8 @@ class TracebackEntry:
rawentry: TracebackType, rawentry: TracebackType,
repr_style: Optional['Literal["short", "long"]'] = None, repr_style: Optional['Literal["short", "long"]'] = None,
) -> None: ) -> None:
self._rawentry: "Final" = rawentry self._rawentry: Final = rawentry
self._repr_style: "Final" = repr_style self._repr_style: Final = repr_style
def with_repr_style( def with_repr_style(
self, repr_style: Optional['Literal["short", "long"]'] self, repr_style: Optional['Literal["short", "long"]']
@ -628,13 +628,14 @@ class ExceptionInfo(Generic[E]):
def getrepr( def getrepr(
self, self,
showlocals: bool = False, showlocals: bool = False,
style: _TracebackStyle = "long", style: TracebackStyle = "long",
abspath: bool = False, abspath: bool = False,
tbfilter: Union[ tbfilter: Union[
bool, Callable[["ExceptionInfo[BaseException]"], Traceback] bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
] = True, ] = True,
funcargs: bool = False, funcargs: bool = False,
truncate_locals: bool = True, truncate_locals: bool = True,
truncate_args: bool = True,
chain: bool = True, chain: bool = True,
) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]: ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
"""Return str()able representation of this exception info. """Return str()able representation of this exception info.
@ -665,6 +666,9 @@ class ExceptionInfo(Generic[E]):
:param bool truncate_locals: :param bool truncate_locals:
With ``showlocals==True``, make sure locals can be safely represented as strings. With ``showlocals==True``, make sure locals can be safely represented as strings.
:param bool truncate_args:
With ``showargs==True``, make sure args can be safely represented as strings.
:param bool chain: :param bool chain:
If chained exceptions in Python 3 should be shown. If chained exceptions in Python 3 should be shown.
@ -691,6 +695,7 @@ class ExceptionInfo(Generic[E]):
tbfilter=tbfilter, tbfilter=tbfilter,
funcargs=funcargs, funcargs=funcargs,
truncate_locals=truncate_locals, truncate_locals=truncate_locals,
truncate_args=truncate_args,
chain=chain, chain=chain,
) )
return fmt.repr_excinfo(self) return fmt.repr_excinfo(self)
@ -804,11 +809,12 @@ class FormattedExcinfo:
fail_marker: ClassVar = "E" fail_marker: ClassVar = "E"
showlocals: bool = False showlocals: bool = False
style: _TracebackStyle = "long" style: TracebackStyle = "long"
abspath: bool = True abspath: bool = True
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
funcargs: bool = False funcargs: bool = False
truncate_locals: bool = True truncate_locals: bool = True
truncate_args: bool = True
chain: bool = True chain: bool = True
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field( astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
default_factory=dict, init=False, repr=False default_factory=dict, init=False, repr=False
@ -839,7 +845,11 @@ class FormattedExcinfo:
if self.funcargs: if self.funcargs:
args = [] args = []
for argname, argvalue in entry.frame.getargs(var=True): for argname, argvalue in entry.frame.getargs(var=True):
args.append((argname, saferepr(argvalue))) if self.truncate_args:
str_repr = saferepr(argvalue)
else:
str_repr = saferepr(argvalue, maxsize=None)
args.append((argname, str_repr))
return ReprFuncArgs(args) return ReprFuncArgs(args)
return None return None
@ -1164,7 +1174,7 @@ class ReprExceptionInfo(ExceptionRepr):
class ReprTraceback(TerminalRepr): class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str] extraline: Optional[str]
style: _TracebackStyle style: TracebackStyle
entrysep: ClassVar = "_ " entrysep: ClassVar = "_ "
@ -1198,7 +1208,7 @@ class ReprTracebackNative(ReprTraceback):
class ReprEntryNative(TerminalRepr): class ReprEntryNative(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
style: ClassVar[_TracebackStyle] = "native" style: ClassVar[TracebackStyle] = "native"
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
tw.write("".join(self.lines)) tw.write("".join(self.lines))
@ -1210,7 +1220,7 @@ class ReprEntry(TerminalRepr):
reprfuncargs: Optional["ReprFuncArgs"] reprfuncargs: Optional["ReprFuncArgs"]
reprlocals: Optional["ReprLocals"] reprlocals: Optional["ReprLocals"]
reprfileloc: Optional["ReprFileLocation"] reprfileloc: Optional["ReprFileLocation"]
style: _TracebackStyle style: TracebackStyle
def _write_entry_lines(self, tw: TerminalWriter) -> None: def _write_entry_lines(self, tw: TerminalWriter) -> None:
"""Write the source code portions of a list of traceback entries with syntax highlighting. """Write the source code portions of a list of traceback entries with syntax highlighting.

View File

@ -161,15 +161,13 @@ class Visitor:
) )
if not self.breadthfirst: if not self.breadthfirst:
for subdir in dirs: for subdir in dirs:
for p in self.gen(subdir): yield from self.gen(subdir)
yield p
for p in self.optsort(entries): for p in self.optsort(entries):
if self.fil is None or self.fil(p): if self.fil is None or self.fil(p):
yield p yield p
if self.breadthfirst: if self.breadthfirst:
for subdir in dirs: for subdir in dirs:
for p in self.gen(subdir): yield from self.gen(subdir)
yield p
class FNMatcher: class FNMatcher:

View File

@ -4,6 +4,7 @@
# This plugin was not named "cache" to avoid conflicts with the external # This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version. # pytest-cache version.
import dataclasses import dataclasses
import errno
import json import json
import os import os
from pathlib import Path from pathlib import Path
@ -227,14 +228,24 @@ class Cache:
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f: with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
f.write(CACHEDIR_TAG_CONTENT) f.write(CACHEDIR_TAG_CONTENT)
path.rename(self._cachedir) try:
# Create a directory in place of the one we just moved so that `TemporaryDirectory`'s path.rename(self._cachedir)
# cleanup doesn't complain. except OSError as e:
# # If 2 concurrent pytests both race to the rename, the loser
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See # gets "Directory not empty" from the rename. In this case,
# https://github.com/python/cpython/issues/74168. Note that passing delete=False would # everything is handled so just continue (while letting the
# do the wrong thing in case of errors and isn't supported until python 3.12. # temporary directory be cleaned up).
path.mkdir() 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: class LFPluginCollWrapper:

View File

@ -54,7 +54,10 @@ from _pytest import __version__
import _pytest._code import _pytest._code
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback from _pytest._code import filter_traceback
from _pytest._code.code import TracebackStyle
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.config.argparsing import Argument
from _pytest.config.argparsing import Parser
import _pytest.deprecated import _pytest.deprecated
import _pytest.hookspec import _pytest.hookspec
from _pytest.outcomes import fail from _pytest.outcomes import fail
@ -71,9 +74,7 @@ from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING: if TYPE_CHECKING:
from .argparsing import Argument from _pytest.cacheprovider import Cache
from .argparsing import Parser
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
@ -1030,6 +1031,9 @@ class Config:
#: 'testpaths' configuration value. #: 'testpaths' configuration value.
TESTPATHS = enum.auto() TESTPATHS = enum.auto()
# Set by cacheprovider plugin.
cache: Optional["Cache"]
def __init__( def __init__(
self, self,
pluginmanager: PytestPluginManager, pluginmanager: PytestPluginManager,
@ -1091,11 +1095,6 @@ class Config:
self.args_source = Config.ArgsSource.ARGS self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = [] self.args: List[str] = []
if TYPE_CHECKING:
from _pytest.cacheprovider import Cache
self.cache: Optional[Cache] = None
@property @property
def rootpath(self) -> Path: def rootpath(self) -> Path:
"""The path to the :ref:`rootdir <rootdir>`. """The path to the :ref:`rootdir <rootdir>`.
@ -1175,7 +1174,7 @@ class Config:
option: Optional[argparse.Namespace] = None, option: Optional[argparse.Namespace] = None,
) -> None: ) -> None:
if option and getattr(option, "fulltrace", False): if option and getattr(option, "fulltrace", False):
style: _TracebackStyle = "long" style: TracebackStyle = "long"
else: else:
style = "native" style = "native"
excrepr = excinfo.getrepr( excrepr = excinfo.getrepr(
@ -1913,7 +1912,7 @@ def parse_warning_filter(
parts.append("") parts.append("")
action_, message, category_, module, lineno_ = (s.strip() for s in parts) action_, message, category_, module, lineno_ = (s.strip() for s in parts)
try: try:
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined] action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined]
except warnings._OptionError as e: except warnings._OptionError as e:
raise UsageError(error_template.format(error=str(e))) from None raise UsageError(error_template.format(error=str(e))) from None
try: try:

View File

@ -13,12 +13,12 @@ from typing import List
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
from typing import TYPE_CHECKING
from typing import Union from typing import Union
import unittest import unittest
from _pytest import outcomes from _pytest import outcomes
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
from _pytest.capture import CaptureManager
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
from _pytest.config import hookimpl from _pytest.config import hookimpl
@ -27,11 +27,7 @@ from _pytest.config.argparsing import Parser
from _pytest.config.exceptions import UsageError from _pytest.config.exceptions import UsageError
from _pytest.nodes import Node from _pytest.nodes import Node
from _pytest.reports import BaseReport from _pytest.reports import BaseReport
from _pytest.runner import CallInfo
if TYPE_CHECKING:
from _pytest.capture import CaptureManager
from _pytest.runner import CallInfo
def _validate_usepdb_cls(value: str) -> Tuple[str, str]: def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
@ -310,7 +306,7 @@ class PdbTrace:
return (yield) return (yield)
def wrap_pytest_function_for_tracing(pyfuncitem): def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
"""Change the Python function object of the given Function item by a """Change the Python function object of the given Function item by a
wrapper which actually enters pdb before calling the python function wrapper which actually enters pdb before calling the python function
itself, effectively leaving the user in the pdb prompt in the first itself, effectively leaving the user in the pdb prompt in the first
@ -322,14 +318,14 @@ def wrap_pytest_function_for_tracing(pyfuncitem):
# python < 3.7.4) runcall's first param is `func`, which means we'd get # python < 3.7.4) runcall's first param is `func`, which means we'd get
# an exception if one of the kwargs to testfunction was called `func`. # an exception if one of the kwargs to testfunction was called `func`.
@functools.wraps(testfunction) @functools.wraps(testfunction)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs) -> None:
func = functools.partial(testfunction, *args, **kwargs) func = functools.partial(testfunction, *args, **kwargs)
_pdb.runcall(func) _pdb.runcall(func)
pyfuncitem.obj = wrapper pyfuncitem.obj = wrapper
def maybe_wrap_pytest_function_for_tracing(pyfuncitem): def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
"""Wrap the given pytestfunct item for tracing support if --trace was given in """Wrap the given pytestfunct item for tracing support if --trace was given in
the command line.""" the command line."""
if pyfuncitem.config.getvalue("trace"): if pyfuncitem.config.getvalue("trace"):

View File

@ -298,7 +298,7 @@ class DoctestItem(Item):
def runtest(self) -> None: def runtest(self) -> None:
_check_all_skipped(self.dtest) _check_all_skipped(self.dtest)
self._disable_output_capturing_for_darwin() self._disable_output_capturing_for_darwin()
failures: List["doctest.DocTestFailure"] = [] failures: List[doctest.DocTestFailure] = []
# Type ignored because we change the type of `out` from what # Type ignored because we change the type of `out` from what
# doctest expects. # doctest expects.
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
@ -505,43 +505,46 @@ class DoctestModule(Module):
import doctest import doctest
class MockAwareDocTestFinder(doctest.DocTestFinder): class MockAwareDocTestFinder(doctest.DocTestFinder):
"""A hackish doctest finder that overrides stdlib internals to fix a stdlib bug. if sys.version_info < (3, 11):
https://github.com/pytest-dev/pytest/issues/3456 def _find_lineno(self, obj, source_lines):
https://bugs.python.org/issue25532 """On older Pythons, doctest code does not take into account
""" `@property`. https://github.com/python/cpython/issues/61648
def _find_lineno(self, obj, source_lines): Moreover, wrapped Doctests need to be unwrapped so the correct
"""Doctest code does not take into account `@property`, this line number is returned. #8796
is a hackish way to fix it. https://bugs.python.org/issue17446 """
if isinstance(obj, property):
obj = getattr(obj, "fget", obj)
Wrapped Doctests will need to be unwrapped so the correct if hasattr(obj, "__wrapped__"):
line number is returned. This will be reported upstream. #8796 # Get the main obj in case of it being wrapped
""" obj = inspect.unwrap(obj)
if isinstance(obj, property):
obj = getattr(obj, "fget", obj)
if hasattr(obj, "__wrapped__"):
# Get the main obj in case of it being wrapped
obj = inspect.unwrap(obj)
# Type ignored because this is a private function.
return super()._find_lineno( # type:ignore[misc]
obj,
source_lines,
)
def _find(
self, tests, obj, name, module, source_lines, globs, seen
) -> None:
if _is_mocked(obj):
return
with _patch_unwrap_mock_aware():
# Type ignored because this is a private function. # Type ignored because this is a private function.
super()._find( # type:ignore[misc] return super()._find_lineno( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen obj,
source_lines,
) )
if sys.version_info < (3, 10):
def _find(
self, tests, obj, name, module, source_lines, globs, seen
) -> None:
"""Override _find to work around issue in stdlib.
https://github.com/pytest-dev/pytest/issues/3456
https://github.com/python/cpython/issues/69718
"""
if _is_mocked(obj):
return # pragma: no cover
with _patch_unwrap_mock_aware():
# Type ignored because this is a private function.
super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen
)
if sys.version_info < (3, 13): if sys.version_info < (3, 13):
def _from_module(self, module, object): def _from_module(self, module, object):
@ -556,9 +559,6 @@ class DoctestModule(Module):
# Type ignored because this is a private function. # Type ignored because this is a private function.
return super()._from_module(module, object) # type: ignore[misc] return super()._from_module(module, object) # type: ignore[misc]
else: # pragma: no cover
pass
try: try:
module = self.obj module = self.obj
except Collector.CollectError: except Collector.CollectError:

View File

@ -21,9 +21,11 @@ from typing import Generic
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
from typing import Mapping
from typing import MutableMapping from typing import MutableMapping
from typing import NoReturn from typing import NoReturn
from typing import Optional from typing import Optional
from typing import OrderedDict
from typing import overload from typing import overload
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
@ -58,6 +60,7 @@ from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import MARKED_FIXTURE
from _pytest.deprecated import YIELD_FIXTURE from _pytest.deprecated import YIELD_FIXTURE
from _pytest.main import Session
from _pytest.mark import Mark from _pytest.mark import Mark
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import MarkDecorator
@ -76,9 +79,6 @@ if sys.version_info < (3, 11):
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Deque
from _pytest.main import Session
from _pytest.python import CallSpec2 from _pytest.python import CallSpec2
from _pytest.python import Function from _pytest.python import Function
from _pytest.python import Metafunc from _pytest.python import Metafunc
@ -161,6 +161,12 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
) )
# Algorithm for sorting on a per-parametrized resource setup basis.
# It is called for Session scope first and performs sorting
# down to the lower scopes such as to minimize number of "high scope"
# setups and teardowns.
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class FixtureArgKey: class FixtureArgKey:
argname: str argname: str
@ -169,97 +175,95 @@ class FixtureArgKey:
item_cls: Optional[type] item_cls: Optional[type]
def get_parametrized_fixture_keys( _V = TypeVar("_V")
OrderedSet = Dict[_V, None]
def get_parametrized_fixture_argkeys(
item: nodes.Item, scope: Scope item: nodes.Item, scope: Scope
) -> Iterator[FixtureArgKey]: ) -> Iterator[FixtureArgKey]:
"""Return list of keys for all parametrized arguments which match """Return list of keys for all parametrized arguments which match
the specified scope.""" the specified scope."""
assert scope is not Scope.Function assert scope is not Scope.Function
try: try:
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined] callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
except AttributeError: except AttributeError:
return return
item_cls = None
if scope is Scope.Session:
scoped_item_path = None
elif scope is Scope.Package:
# Package key = module's directory.
scoped_item_path = item.path.parent
elif scope is Scope.Module:
scoped_item_path = item.path
elif scope is Scope.Class:
scoped_item_path = item.path
item_cls = item.cls # type: ignore[attr-defined]
else:
assert_never(scope)
for argname in callspec.indices: for argname in callspec.indices:
if callspec._arg2scope[argname] != scope: if callspec._arg2scope[argname] != scope:
continue continue
item_cls = None
if scope is Scope.Session:
scoped_item_path = None
elif scope is Scope.Package:
# Package key = module's directory.
scoped_item_path = item.path.parent
elif scope is Scope.Module:
scoped_item_path = item.path
elif scope is Scope.Class:
scoped_item_path = item.path
item_cls = item.cls # type: ignore[attr-defined]
else:
assert_never(scope)
param_index = callspec.indices[argname] param_index = callspec.indices[argname]
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls) yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
# Algorithm for sorting on a per-parametrized resource setup basis.
# It is called for Session scope first and performs sorting
# down to the lower scopes such as to minimize number of "high scope"
# setups and teardowns.
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {} argkeys_by_item: Dict[Scope, Dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
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: for scope in HIGH_SCOPES:
scoped_argkeys_cache = argkeys_cache[scope] = {} scoped_argkeys_by_item = argkeys_by_item[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: for item in items:
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None) argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope))
if keys: if argkeys:
scoped_argkeys_cache[item] = keys scoped_argkeys_by_item[item] = argkeys
for key in keys: for argkey in argkeys:
scoped_items_by_argkey[key].append(item) scoped_items_by_argkey[argkey][item] = None
items_dict = dict.fromkeys(items, None)
items_set = dict.fromkeys(items)
return list( return list(
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session) reorder_items_atscope(
items_set, argkeys_by_item, items_by_argkey, Scope.Session
)
) )
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]"]],
) -> None:
for scope in HIGH_SCOPES:
for key in argkeys_cache[scope].get(item, []):
items_by_argkey[scope][key].appendleft(item)
def reorder_items_atscope( def reorder_items_atscope(
items: Dict[nodes.Item, None], items: OrderedSet[nodes.Item],
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]], argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]],
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]], items_by_argkey: Mapping[
Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]]
],
scope: Scope, scope: Scope,
) -> Dict[nodes.Item, None]: ) -> OrderedSet[nodes.Item]:
if scope is Scope.Function or len(items) < 3: if scope is Scope.Function or len(items) < 3:
return items return items
ignore: Set[Optional[FixtureArgKey]] = set()
items_deque = deque(items)
items_done: Dict[nodes.Item, None] = {}
scoped_items_by_argkey = items_by_argkey[scope] scoped_items_by_argkey = items_by_argkey[scope]
scoped_argkeys_cache = argkeys_cache[scope] scoped_argkeys_by_item = argkeys_by_item[scope]
ignore: Set[FixtureArgKey] = set()
items_deque = deque(items)
items_done: OrderedSet[nodes.Item] = {}
while items_deque: while items_deque:
no_argkey_group: Dict[nodes.Item, None] = {} no_argkey_items: OrderedSet[nodes.Item] = {}
slicing_argkey = None slicing_argkey = None
while items_deque: while items_deque:
item = items_deque.popleft() item = items_deque.popleft()
if item in items_done or item in no_argkey_group: if item in items_done or item in no_argkey_items:
continue continue
argkeys = dict.fromkeys( argkeys = dict.fromkeys(
(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore
) )
if not argkeys: if not argkeys:
no_argkey_group[item] = None no_argkey_items[item] = None
else: else:
slicing_argkey, _ = argkeys.popitem() slicing_argkey, _ = argkeys.popitem()
# We don't have to remove relevant items from later in the # We don't have to remove relevant items from later in the
@ -268,16 +272,23 @@ def reorder_items_atscope(
i for i in scoped_items_by_argkey[slicing_argkey] if i in items i for i in scoped_items_by_argkey[slicing_argkey] if i in items
] ]
for i in reversed(matching_items): for i in reversed(matching_items):
fix_cache_order(i, argkeys_cache, items_by_argkey)
items_deque.appendleft(i) items_deque.appendleft(i)
# Fix items_by_argkey order.
for other_scope in HIGH_SCOPES:
other_scoped_items_by_argkey = items_by_argkey[other_scope]
for argkey in argkeys_by_item[other_scope].get(i, ()):
other_scoped_items_by_argkey[argkey][i] = None
other_scoped_items_by_argkey[argkey].move_to_end(
i, last=False
)
break break
if no_argkey_group: if no_argkey_items:
no_argkey_group = reorder_items_atscope( reordered_no_argkey_items = reorder_items_atscope(
no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower() no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower()
) )
for item in no_argkey_group: items_done.update(reordered_no_argkey_items)
items_done[item] = None if slicing_argkey is not None:
ignore.add(slicing_argkey) ignore.add(slicing_argkey)
return items_done return items_done
@ -1627,7 +1638,7 @@ class FixtureManager:
func: "_FixtureFunc[object]", func: "_FixtureFunc[object]",
nodeid: Optional[str], nodeid: Optional[str],
scope: Union[ scope: Union[
Scope, _ScopeName, Callable[[str, Config], _ScopeName], None Scope, _ScopeName, Callable[[str, Config], _ScopeName]
] = "function", ] = "function",
params: Optional[Sequence[object]] = None, params: Optional[Sequence[object]] = None,
ids: Optional[ ids: Optional[
@ -1823,7 +1834,10 @@ def _show_fixtures_per_test(config: Config, session: "Session") -> None:
fixture_doc = inspect.getdoc(fixture_def.func) fixture_doc = inspect.getdoc(fixture_def.func)
if fixture_doc: if fixture_doc:
write_docstring( write_docstring(
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc tw,
fixture_doc.split("\n\n", maxsplit=1)[0]
if verbose <= 0
else fixture_doc,
) )
else: else:
tw.line(" no docstring available", red=True) tw.line(" no docstring available", red=True)
@ -1905,7 +1919,9 @@ def _showfixtures_main(config: Config, session: "Session") -> None:
tw.write("\n") tw.write("\n")
doc = inspect.getdoc(fixturedef.func) doc = inspect.getdoc(fixturedef.func)
if doc: if doc:
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc) write_docstring(
tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc
)
else: else:
tw.line(" no docstring available", red=True) tw.line(" no docstring available", red=True)
tw.line() tw.line()

View File

@ -311,7 +311,12 @@ def pytest_collection_finish(session: "Session") -> None:
def pytest_ignore_collect( def pytest_ignore_collect(
collection_path: Path, path: "LEGACY_PATH", config: "Config" collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]: ) -> 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 This hook is consulted for all files and directories prior to calling
more specific hooks. more specific hooks.

View File

@ -401,7 +401,7 @@ class LogCaptureHandler(logging_StreamHandler):
# The default behavior of logging is to print "Logging error" # The default behavior of logging is to print "Logging error"
# to stderr with the call stack and some extra details. # to stderr with the call stack and some extra details.
# pytest wants to make such mistakes visible during testing. # pytest wants to make such mistakes visible during testing.
raise raise # pylint: disable=misplaced-bare-raise
@final @final

View File

@ -38,7 +38,6 @@ from _pytest.config import PytestPluginManager
from _pytest.config import UsageError from _pytest.config import UsageError
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.config.compat import PathAwareHookProxy from _pytest.config.compat import PathAwareHookProxy
from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
@ -55,6 +54,8 @@ from _pytest.warning_types import PytestWarning
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Self from typing import Self
from _pytest.fixtures import FixtureManager
def pytest_addoption(parser: Parser) -> None: def pytest_addoption(parser: Parser) -> None:
parser.addini( parser.addini(
@ -551,7 +552,7 @@ class Session(nodes.Collector):
# Set on the session by runner.pytest_sessionstart. # Set on the session by runner.pytest_sessionstart.
_setupstate: SetupState _setupstate: SetupState
# Set on the session by fixtures.pytest_sessionstart. # Set on the session by fixtures.pytest_sessionstart.
_fixturemanager: FixtureManager _fixturemanager: "FixtureManager"
exitstatus: Union[int, ExitCode] exitstatus: Union[int, ExitCode]
def __init__(self, config: Config) -> None: def __init__(self, config: Config) -> None:

View File

@ -31,6 +31,7 @@ from _pytest.config import Config
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import MARKED_FIXTURE
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.scope import _ScopeName
from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestUnknownMarkWarning
@ -430,7 +431,6 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
# Typing for builtin pytest marks. This is cheating; it gives builtin marks # Typing for builtin pytest marks. This is cheating; it gives builtin marks
# special privilege, and breaks modularity. But practicality beats purity... # special privilege, and breaks modularity. But practicality beats purity...
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.scope import _ScopeName
class _SkipMarkDecorator(MarkDecorator): class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,no-overload-impl] @overload # type: ignore[override,no-overload-impl]

View File

@ -30,6 +30,7 @@ from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback from _pytest._code.code import Traceback
from _pytest._code.code import TracebackStyle
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
@ -49,7 +50,6 @@ if TYPE_CHECKING:
from typing import Self from typing import Self
# Imported here due to circular import. # Imported here due to circular import.
from _pytest._code.code import _TracebackStyle
from _pytest.main import Session from _pytest.main import Session
@ -416,7 +416,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
def _repr_failure_py( def _repr_failure_py(
self, self,
excinfo: ExceptionInfo[BaseException], excinfo: ExceptionInfo[BaseException],
style: "Optional[_TracebackStyle]" = None, style: "Optional[TracebackStyle]" = None,
) -> TerminalRepr: ) -> TerminalRepr:
from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupError
@ -448,6 +448,8 @@ class Node(abc.ABC, metaclass=NodeMeta):
else: else:
truncate_locals = True truncate_locals = True
truncate_args = False if self.config.getoption("verbose", 0) > 2 else True
# excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
# It is possible for a fixture/test to change the CWD while this code runs, which # It is possible for a fixture/test to change the CWD while this code runs, which
# would then result in the user seeing confusing paths in the failure message. # would then result in the user seeing confusing paths in the failure message.
@ -466,12 +468,13 @@ class Node(abc.ABC, metaclass=NodeMeta):
style=style, style=style,
tbfilter=tbfilter, tbfilter=tbfilter,
truncate_locals=truncate_locals, truncate_locals=truncate_locals,
truncate_args=truncate_args,
) )
def repr_failure( def repr_failure(
self, self,
excinfo: ExceptionInfo[BaseException], excinfo: ExceptionInfo[BaseException],
style: "Optional[_TracebackStyle]" = None, style: "Optional[TracebackStyle]" = None,
) -> Union[str, TerminalRepr]: ) -> Union[str, TerminalRepr]:
"""Return a representation of a collection or test failure. """Return a representation of a collection or test failure.

View File

@ -1,18 +1,14 @@
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import TYPE_CHECKING
from _pytest import nodes from _pytest import nodes
from _pytest.cacheprovider import Cache
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.main import Session from _pytest.main import Session
from _pytest.reports import TestReport from _pytest.reports import TestReport
import pytest
if TYPE_CHECKING:
from _pytest.cacheprovider import Cache
STEPWISE_CACHE_DIR = "cache/stepwise" STEPWISE_CACHE_DIR = "cache/stepwise"
@ -37,7 +33,6 @@ def pytest_addoption(parser: Parser) -> None:
) )
@pytest.hookimpl
def pytest_configure(config: Config) -> None: def pytest_configure(config: Config) -> None:
if config.option.stepwise_skip: if config.option.stepwise_skip:
# allow --stepwise-skip to work on its own merits. # allow --stepwise-skip to work on its own merits.

View File

@ -34,8 +34,8 @@ class catch_threading_exception:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self.args: Optional["threading.ExceptHookArgs"] = None self.args: Optional[threading.ExceptHookArgs] = None
self._old_hook: Optional[Callable[["threading.ExceptHookArgs"], Any]] = None self._old_hook: Optional[Callable[[threading.ExceptHookArgs], Any]] = None
def _hook(self, args: "threading.ExceptHookArgs") -> None: def _hook(self, args: "threading.ExceptHookArgs") -> None:
self.args = args self.args = args

View File

@ -41,10 +41,11 @@ if TYPE_CHECKING:
import twisted.trial.unittest import twisted.trial.unittest
_SysExcInfoType = Union[
Tuple[Type[BaseException], BaseException, types.TracebackType], _SysExcInfoType = Union[
Tuple[None, None, None], Tuple[Type[BaseException], BaseException, types.TracebackType],
] Tuple[None, None, None],
]
def pytest_pycollect_makeitem( def pytest_pycollect_makeitem(
@ -218,16 +219,17 @@ class TestCaseFunction(Function):
super().setup() super().setup()
def teardown(self) -> None: def teardown(self) -> None:
super().teardown()
if self._explicit_tearDown is not None: if self._explicit_tearDown is not None:
self._explicit_tearDown() self._explicit_tearDown()
self._explicit_tearDown = None self._explicit_tearDown = None
self._obj = None self._obj = None
del self._instance
super().teardown()
def startTest(self, testcase: "unittest.TestCase") -> None: def startTest(self, testcase: "unittest.TestCase") -> None:
pass pass
def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None: def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None:
# Unwrap potential exception info (see twisted trial support below). # Unwrap potential exception info (see twisted trial support below).
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
try: try:
@ -263,7 +265,7 @@ class TestCaseFunction(Function):
self.__dict__.setdefault("_excinfo", []).append(excinfo) self.__dict__.setdefault("_excinfo", []).append(excinfo)
def addError( def addError(
self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" self, testcase: "unittest.TestCase", rawexcinfo: _SysExcInfoType
) -> None: ) -> None:
try: try:
if isinstance(rawexcinfo[1], exit.Exception): if isinstance(rawexcinfo[1], exit.Exception):
@ -273,7 +275,7 @@ class TestCaseFunction(Function):
self._addexcinfo(rawexcinfo) self._addexcinfo(rawexcinfo)
def addFailure( def addFailure(
self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" self, testcase: "unittest.TestCase", rawexcinfo: _SysExcInfoType
) -> None: ) -> None:
self._addexcinfo(rawexcinfo) self._addexcinfo(rawexcinfo)
@ -286,7 +288,7 @@ class TestCaseFunction(Function):
def addExpectedFailure( def addExpectedFailure(
self, self,
testcase: "unittest.TestCase", testcase: "unittest.TestCase",
rawexcinfo: "_SysExcInfoType", rawexcinfo: _SysExcInfoType,
reason: str = "", reason: str = "",
) -> None: ) -> None:
try: try:

View File

@ -34,8 +34,8 @@ class catch_unraisable_exception:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self.unraisable: Optional["sys.UnraisableHookArgs"] = None self.unraisable: Optional[sys.UnraisableHookArgs] = None
self._old_hook: Optional[Callable[["sys.UnraisableHookArgs"], Any]] = None self._old_hook: Optional[Callable[[sys.UnraisableHookArgs], Any]] = None
def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None: def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None:
# Storing unraisable.object can resurrect an object which is being # Storing unraisable.object can resurrect an object which is being

View File

@ -207,7 +207,7 @@ class CommonFSTests:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fil", "fil",
["*dir", "*dir", pytest.mark.skip("sys.version_info <" " (3,6)")(b"*dir")], ["*dir", "*dir", pytest.mark.skip("sys.version_info < (3,6)")(b"*dir")],
) )
def test_visit_filterfunc_is_string(self, path1, fil): def test_visit_filterfunc_is_string(self, path1, fil):
lst = [] lst = []

View File

@ -11,6 +11,7 @@ import re
import sys import sys
import textwrap import textwrap
from typing import Any from typing import Any
from typing import cast
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _pytest._code import _pytest._code
@ -27,7 +28,7 @@ import pytest
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle from _pytest._code.code import TracebackStyle
if sys.version_info < (3, 11): if sys.version_info < (3, 11):
from exceptiongroup import ExceptionGroup from exceptiongroup import ExceptionGroup
@ -712,6 +713,29 @@ raise ValueError()
assert full_reprlocals.lines assert full_reprlocals.lines
assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
def test_repr_args_not_truncated(self, importasmod) -> None:
mod = importasmod(
"""
def func1(m):
raise ValueError("hello\\nworld")
"""
)
excinfo = pytest.raises(ValueError, mod.func1, "m" * 500)
excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True, truncate_args=True)
reprfuncargs = p.repr_args(entry)
assert reprfuncargs is not None
arg1 = cast(str, reprfuncargs.args[0][1])
assert len(arg1) < 500
assert "..." in arg1
# again without truncate
p = FormattedExcinfo(funcargs=True, truncate_args=False)
reprfuncargs = p.repr_args(entry)
assert reprfuncargs is not None
assert reprfuncargs.args[0] == ("m", repr("m" * 500))
assert "..." not in cast(str, reprfuncargs.args[0][1])
def test_repr_tracebackentry_lines(self, importasmod) -> None: def test_repr_tracebackentry_lines(self, importasmod) -> None:
mod = importasmod( mod = importasmod(
""" """
@ -901,7 +925,7 @@ raise ValueError()
) )
excinfo = pytest.raises(ValueError, mod.entry) excinfo = pytest.raises(ValueError, mod.entry)
styles: tuple[_TracebackStyle, ...] = ("long", "short") styles: tuple[TracebackStyle, ...] = ("long", "short")
for style in styles: for style in styles:
p = FormattedExcinfo(style=style) p = FormattedExcinfo(style=style)
reprtb = p.repr_traceback(excinfo) reprtb = p.repr_traceback(excinfo)
@ -1028,7 +1052,7 @@ raise ValueError()
) )
excinfo = pytest.raises(ValueError, mod.entry) excinfo = pytest.raises(ValueError, mod.entry)
styles: tuple[_TracebackStyle, ...] = ("short", "long", "no") styles: tuple[TracebackStyle, ...] = ("short", "long", "no")
for style in styles: for style in styles:
for showlocals in (True, False): for showlocals in (True, False):
repr = excinfo.getrepr(style=style, showlocals=showlocals) repr = excinfo.getrepr(style=style, showlocals=showlocals)

View File

@ -0,0 +1,11 @@
import unittest
class MyTestCase(unittest.TestCase):
first_time = True
def test_fail_the_first_time(self) -> None:
"""Regression test for issue #12424."""
if self.first_time:
type(self).first_time = False
self.fail()

View File

@ -1,4 +1,4 @@
anyio[curio,trio]==4.3.0 anyio[curio,trio]==4.4.0
django==5.0.6 django==5.0.6
pytest-asyncio==0.23.7 pytest-asyncio==0.23.7
pytest-bdd==7.1.2 pytest-bdd==7.1.2
@ -9,7 +9,7 @@ pytest-html==4.1.1
pytest-mock==3.14.0 pytest-mock==3.14.0
pytest-rerunfailures==14.0 pytest-rerunfailures==14.0
pytest-sugar==1.0.0 pytest-sugar==1.0.0
pytest-trio==0.7.0 pytest-trio==0.8.0
pytest-twisted==1.14.1 pytest-twisted==1.14.1
twisted==24.3.0 twisted==24.3.0
pytest-xvfb==3.0.0 pytest-xvfb==3.0.0

View File

@ -2219,6 +2219,25 @@ class TestAutouseManagement:
reprec = pytester.inline_run("-s") reprec = pytester.inline_run("-s")
reprec.assertoutcome(passed=2) 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: class TestFixtureMarker:
def test_parametrize(self, pytester: Pytester) -> None: def test_parametrize(self, pytester: Pytester) -> None:

View File

@ -2045,3 +2045,36 @@ def test_fine_grained_assertion_verbosity(pytester: Pytester):
f"E AssertionError: assert 'hello world' in '{long_text}'", f"E AssertionError: assert 'hello world' in '{long_text}'",
] ]
) )
def test_full_output_vvv(pytester: Pytester) -> None:
pytester.makepyfile(
r"""
def crash_helper(m):
assert 1 == 2
def test_vvv():
crash_helper(500 * "a")
"""
)
result = pytester.runpytest("")
# without -vvv, the passed args are truncated
expected_non_vvv_arg_line = "m = 'aaaaaaaaaaaaaaa*..aaaaaaaaaaaa*"
result.stdout.fnmatch_lines(
[
expected_non_vvv_arg_line,
"test_full_output_vvv.py:2: AssertionError",
],
)
# double check that the untruncated part is not in the output
expected_vvv_arg_line = "m = '{}'".format(500 * "a")
result.stdout.no_fnmatch_line(expected_vvv_arg_line)
# but with "-vvv" the args are not truncated
result = pytester.runpytest("-vvv")
result.stdout.fnmatch_lines(
[
expected_vvv_arg_line,
"test_full_output_vvv.py:2: AssertionError",
]
)
result.stdout.no_fnmatch_line(expected_non_vvv_arg_line)

View File

@ -1163,7 +1163,7 @@ class TestNewFirst:
) )
p1.write_text( p1.write_text(
"def test_1(): assert 1\n" "def test_2(): assert 1\n", encoding="utf-8" "def test_1(): assert 1\ndef test_2(): assert 1\n", encoding="utf-8"
) )
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))

View File

@ -100,14 +100,13 @@ class TestReportSerialization:
rep_entries = rep.longrepr.reprtraceback.reprentries rep_entries = rep.longrepr.reprtraceback.reprentries
a_entries = a.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries
for i in range(len(a_entries)): assert len(rep_entries) == len(a_entries) # python < 3.10 zip(strict=True)
rep_entry = rep_entries[i] for a_entry, rep_entry in zip(a_entries, rep_entries):
assert isinstance(rep_entry, ReprEntry) assert isinstance(rep_entry, ReprEntry)
assert rep_entry.reprfileloc is not None assert rep_entry.reprfileloc is not None
assert rep_entry.reprfuncargs is not None assert rep_entry.reprfuncargs is not None
assert rep_entry.reprlocals is not None assert rep_entry.reprlocals is not None
a_entry = a_entries[i]
assert isinstance(a_entry, ReprEntry) assert isinstance(a_entry, ReprEntry)
assert a_entry.reprfileloc is not None assert a_entry.reprfileloc is not None
assert a_entry.reprfuncargs is not None assert a_entry.reprfuncargs is not None
@ -146,9 +145,10 @@ class TestReportSerialization:
rep_entries = rep.longrepr.reprtraceback.reprentries rep_entries = rep.longrepr.reprtraceback.reprentries
a_entries = a.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries
for i in range(len(a_entries)): assert len(rep_entries) == len(a_entries) # python < 3.10 zip(strict=True)
assert isinstance(rep_entries[i], ReprEntryNative) for rep_entry, a_entry in zip(rep_entries, a_entries):
assert rep_entries[i].lines == a_entries[i].lines assert isinstance(rep_entry, ReprEntryNative)
assert rep_entry.lines == a_entry.lines
def test_itemreport_outcomes(self, pytester: Pytester) -> None: def test_itemreport_outcomes(self, pytester: Pytester) -> None:
# This test came originally from test_remote.py in xdist (ca03269). # This test came originally from test_remote.py in xdist (ca03269).

View File

@ -1,5 +1,4 @@
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
import gc
import sys import sys
from typing import List from typing import List
@ -192,30 +191,35 @@ def test_teardown(pytester: Pytester) -> None:
def test_teardown_issue1649(pytester: Pytester) -> None: def test_teardown_issue1649(pytester: Pytester) -> None:
""" """
Are TestCase objects cleaned up? Often unittest TestCase objects set 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 The TestCase will not be cleaned up if the test fails, because it
would then exist in the stackframe. would then exist in the stackframe.
Regression test for #1649 (see also #12367).
""" """
testpath = pytester.makepyfile( pytester.makepyfile(
""" """
import unittest import unittest
class TestCaseObjectsShouldBeCleanedUp(unittest.TestCase): import gc
def setUp(self):
self.an_expensive_object = 1
def test_demo(self):
pass
""" 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) result = pytester.runpytest()
gc.collect() assert result.ret == ExitCode.OK
# 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")
def test_unittest_skip_issue148(pytester: Pytester) -> None: def test_unittest_skip_issue148(pytester: Pytester) -> None:
@ -380,7 +384,7 @@ def test_testcase_adderrorandfailure_defers(pytester: Pytester, type: str) -> No
@pytest.mark.parametrize("type", ["Error", "Failure"]) @pytest.mark.parametrize("type", ["Error", "Failure"])
def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None: def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None:
pytester.makepyfile( pytester.makepyfile(
""" f"""
from typing import Generic, TypeVar from typing import Generic, TypeVar
from unittest import TestCase from unittest import TestCase
import pytest, _pytest._code import pytest, _pytest._code
@ -409,7 +413,7 @@ def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None:
def test_hello(self): def test_hello(self):
pass pass
""".format(**locals()) """
) )
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(

View File

@ -280,10 +280,8 @@ def test_warning_recorded_hook(pytester: Pytester) -> None:
("call 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"), ("teardown warning", "runtest", "test_warning_recorded_hook.py::test_func"),
] ]
for index in range(len(expected)): assert len(collected) == len(expected) # python < 3.10 zip(strict=True)
collected_result = collected[index] for collected_result, expected_result in zip(collected, expected):
expected_result = expected[index]
assert collected_result[0] == expected_result[0], str(collected) assert collected_result[0] == expected_result[0], str(collected)
assert collected_result[1] == expected_result[1], str(collected) assert collected_result[1] == expected_result[1], str(collected)
assert collected_result[2] == expected_result[2], str(collected) assert collected_result[2] == expected_result[2], str(collected)

View File

@ -141,7 +141,7 @@ commands =
pytest --cov=. simple_integration.py pytest --cov=. simple_integration.py
pytest --ds=django_settings simple_integration.py pytest --ds=django_settings simple_integration.py
pytest --html=simple.html simple_integration.py pytest --html=simple.html simple_integration.py
pytest --reruns 5 simple_integration.py pytest --reruns 5 simple_integration.py pytest_rerunfailures_integration.py
pytest pytest_anyio_integration.py pytest pytest_anyio_integration.py
pytest pytest_asyncio_integration.py pytest pytest_asyncio_integration.py
pytest pytest_mock_integration.py pytest pytest_mock_integration.py