Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
33e2e26
build(deps): Bump pytest-asyncio in /testing/plugins_integration (#13…
dependabot[bot] Mar 31, 2025
9eacaef
Enable GitHub Sponsors in FUNDING.yml
nicoddemus Apr 1, 2025
7ca1bc9
Enable thanks.dev in FUNDING.yml
nicoddemus Apr 1, 2025
e35f1bf
Merge pull request #13339 from pytest-dev/gh-sponsors
nicoddemus Apr 1, 2025
0c45497
Fix `approx` when mixing numpy.bool and bool (#13338)
BahramF73 Apr 2, 2025
682013d
Make wasxfail consistent (#13328)
harmin-parra Apr 2, 2025
9a7dbd8
Add title and change spelling for use of approx with non-numerics (#1…
callummscott Apr 3, 2025
4056433
Fix type hints for `TestReport.when` and `TestReport.location` (#13345)
rouge8 Apr 3, 2025
636d956
Clean up type hints and add a test (#13348)
rouge8 Apr 3, 2025
6964cf1
Doc: clarify `approx` behavior regarding int and bools (#13341)
natmokval Apr 4, 2025
be83aa6
[automated] Update plugin list (#13356)
github-actions[bot] Apr 6, 2025
3f46320
build(deps): Bump pytest-django in /testing/plugins_integration (#13357)
dependabot[bot] Apr 7, 2025
18439f5
build(deps): Bump pytest-cov in /testing/plugins_integration (#13358)
dependabot[bot] Apr 7, 2025
46c94ee
Add note on using mixin classes for abstract test classes in document…
BahramF73 Apr 7, 2025
0286f64
Fix test suite when PYTEST_PLUGINS is set
TobiMcNamobi Apr 7, 2025
9d7bf4e
[pre-commit.ci] pre-commit autoupdate (#13362)
pre-commit-ci[bot] Apr 8, 2025
b72f9a2
[automated] Update plugin list (#13372)
github-actions[bot] Apr 13, 2025
103b2b6
[pre-commit.ci] pre-commit autoupdate (#13375)
pre-commit-ci[bot] Apr 15, 2025
fa05162
init
gesslerpd Apr 17, 2025
1e40e83
+ author
gesslerpd Apr 17, 2025
0213748
fix
gesslerpd Apr 17, 2025
22ef71b
Simplify the code a bit
nicoddemus Apr 19, 2025
3c580a0
Small refactor
nicoddemus Apr 19, 2025
018ce13
action rerun
gesslerpd Apr 19, 2025
e468420
+ comment
gesslerpd Apr 19, 2025
fc505cb
fix docs
gesslerpd Apr 19, 2025
9d04569
Revert "fix docs"
gesslerpd Apr 19, 2025
f68ccd9
fix
gesslerpd Apr 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# info:
# * https://help.github.com/en/articles/displaying-a-sponsor-button-in-your-repository
# * https://tidelift.com/subscription/how-to-connect-tidelift-with-github
github: pytest-dev
tidelift: pypi/pytest
open_collective: pytest
thanks_dev: u/gh/pytest-dev
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.11.2"
rev: "v0.11.5"
hooks:
- id: ruff
args: ["--fix"]
Expand Down
2 changes: 2 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Ashley Whetter
Aviral Verma
Aviv Palivoda
Babak Keyvani
Bahram Farahmand
Barney Gale
Ben Brown
Ben Gartner
Expand Down Expand Up @@ -345,6 +346,7 @@ Pavel Karateev
Pavel Zhukov
Paweł Adamczak
Pedro Algarvio
Peter Gessler
Petter Strandmark
Philipp Loose
Pierre Sassoulas
Expand Down
3 changes: 3 additions & 0 deletions changelog/11067.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The test report is now consistent regardless if the test xfailed via :ref:`pytest.mark.xfail <pytest.mark.xfail ref>` or :func:`pytest.fail`.

Previously, *xfailed* tests via the marker would have the string ``"reason: "`` prefixed to the message, while those *xfailed* via the function did not. The prefix has been removed.
1 change: 1 addition & 0 deletions changelog/12647.contrib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed running the test suite with the ``hypothesis`` pytest plugin.
17 changes: 17 additions & 0 deletions changelog/13047.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Restore :func:`pytest.approx` handling of equality checks between `bool` and `numpy.bool_` types.

Comparing `bool` and `numpy.bool_` using :func:`pytest.approx` accidentally changed in version `8.3.4` and `8.3.5` to no longer match:

.. code-block:: pycon

>>> import numpy as np
>>> from pytest import approx
>>> [np.True_, np.True_] == pytest.approx([True, True])
False

This has now been fixed:

.. code-block:: pycon

>>> [np.True_, np.True_] == pytest.approx([True, True])
True
1 change: 1 addition & 0 deletions changelog/13218.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pointed out in the :func:`pytest.approx` documentation that it considers booleans unequal to numeric zero or one.
1 change: 1 addition & 0 deletions changelog/13345.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix type hints for :attr:`pytest.TestReport.when` and :attr:`pytest.TestReport.location`.
1 change: 1 addition & 0 deletions changelog/13380.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix :class:`ExceptionGroup` traceback filtering to exclude pytest internals.
5 changes: 5 additions & 0 deletions changelog/8612.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Add a recipe for handling abstract test classes in the documentation.

A new example has been added to the documentation to demonstrate how to use a mixin class to handle abstract
test classes without manually setting the ``__test__`` attribute for subclasses.
This ensures that subclasses of abstract test classes are automatically collected by pytest.
27 changes: 27 additions & 0 deletions doc/en/example/pythoncollection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,30 @@ with ``Test`` by setting a boolean ``__test__`` attribute to ``False``.
# Will not be discovered as a test
class TestClass:
__test__ = False

.. note::

If you are working with abstract test classes and want to avoid manually setting
the ``__test__`` attribute for subclasses, you can use a mixin class to handle
this automatically. For example:

.. code-block:: python

# Mixin to handle abstract test classes
class NotATest:
def __init_subclass__(cls):
cls.__test__ = NotATest not in cls.__bases__


# Abstract test class
class AbstractTest(NotATest):
pass


# Subclass that will be collected as a test
class RealTest(AbstractTest):
def test_example(self):
assert 1 + 1 == 2

This approach ensures that subclasses of abstract test classes are automatically
collected without needing to explicitly set the ``__test__`` attribute.
256 changes: 156 additions & 100 deletions doc/en/reference/plugin_list.rst

Large diffs are not rendered by default.

64 changes: 42 additions & 22 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
from pathlib import Path
import re
import sys
import traceback
from traceback import extract_tb
from traceback import format_exception
from traceback import format_exception_only
from traceback import FrameSummary
from types import CodeType
from types import FrameType
from types import TracebackType
Expand All @@ -28,6 +30,7 @@
from typing import Literal
from typing import overload
from typing import SupportsIndex
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union

Expand Down Expand Up @@ -208,10 +211,10 @@ def with_repr_style(
def lineno(self) -> int:
return self._rawentry.tb_lineno - 1

def get_python_framesummary(self) -> traceback.FrameSummary:
def get_python_framesummary(self) -> FrameSummary:
# Python's built-in traceback module implements all the nitty gritty
# details to get column numbers of out frames.
stack_summary = traceback.extract_tb(self._rawentry, limit=1)
stack_summary = extract_tb(self._rawentry, limit=1)
return stack_summary[0]

# Column and end line numbers introduced in python 3.11
Expand Down Expand Up @@ -694,8 +697,7 @@ def getrepr(
showlocals: bool = False,
style: TracebackStyle = "long",
abspath: bool = False,
tbfilter: bool
| Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True,
tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True,
funcargs: bool = False,
truncate_locals: bool = True,
truncate_args: bool = True,
Expand Down Expand Up @@ -742,7 +744,7 @@ def getrepr(
if style == "native":
return ReprExceptionInfo(
reprtraceback=ReprTracebackNative(
traceback.format_exception(
format_exception(
self.type,
self.value,
self.traceback[0]._rawentry if self.traceback else None,
Expand Down Expand Up @@ -851,6 +853,17 @@ def group_contains(
return self._group_contains(self.value, expected_exception, match, depth)


if TYPE_CHECKING:
from typing_extensions import TypeAlias

# Type alias for the `tbfilter` setting:
# bool: If True, it should be filtered using Traceback.filter()
# callable: A callable that takes an ExceptionInfo and returns the filtered traceback.
TracebackFilter: TypeAlias = Union[
bool, Callable[[ExceptionInfo[BaseException]], Traceback]
]


@dataclasses.dataclass
class FormattedExcinfo:
"""Presenting information about failing Functions and Generators."""
Expand All @@ -862,7 +875,7 @@ class FormattedExcinfo:
showlocals: bool = False
style: TracebackStyle = "long"
abspath: bool = True
tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True
tbfilter: TracebackFilter = True
funcargs: bool = False
truncate_locals: bool = True
truncate_args: bool = True
Expand Down Expand Up @@ -1100,11 +1113,7 @@ def _makepath(self, path: Path | str) -> str:
return str(path)

def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
traceback = excinfo.traceback
if callable(self.tbfilter):
traceback = self.tbfilter(excinfo)
elif self.tbfilter:
traceback = traceback.filter(excinfo)
traceback = filter_excinfo_traceback(self.tbfilter, excinfo)

if isinstance(excinfo.value, RecursionError):
traceback, extraline = self._truncate_recursive_traceback(traceback)
Expand Down Expand Up @@ -1178,14 +1187,15 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR
# Fall back to native traceback as a temporary workaround until
# full support for exception groups added to ExceptionInfo.
# See https://github.com/pytest-dev/pytest/issues/9159
reprtraceback: ReprTraceback | ReprTracebackNative
if isinstance(e, BaseExceptionGroup):
reprtraceback: ReprTracebackNative | ReprTraceback = (
ReprTracebackNative(
traceback.format_exception(
type(excinfo_.value),
excinfo_.value,
excinfo_.traceback[0]._rawentry,
)
# don't filter any sub-exceptions since they shouldn't have any internal frames
traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
reprtraceback = ReprTracebackNative(
format_exception(
type(excinfo.value),
excinfo.value,
traceback[0]._rawentry,
)
)
else:
Expand All @@ -1194,9 +1204,7 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
reprtraceback = ReprTracebackNative(
traceback.format_exception(type(e), e, None)
)
reprtraceback = ReprTracebackNative(format_exception(type(e), e, None))
reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]

Expand Down Expand Up @@ -1545,3 +1553,15 @@ def filter_traceback(entry: TracebackEntry) -> bool:
return False

return True


def filter_excinfo_traceback(
tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException]
) -> Traceback:
"""Filter the exception traceback in ``excinfo`` according to ``tbfilter``."""
if callable(tbfilter):
return tbfilter(excinfo)
elif tbfilter:
return excinfo.traceback.filter(excinfo)
else:
return excinfo.traceback
39 changes: 31 additions & 8 deletions src/_pytest/python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,23 +421,35 @@ def __repr__(self) -> str:
def __eq__(self, actual) -> bool:
"""Return whether the given value is equal to the expected value
within the pre-specified tolerance."""

def is_bool(val: Any) -> bool:
# Check if `val` is a native bool or numpy bool.
if isinstance(val, bool):
return True
try:
import numpy as np

return isinstance(val, np.bool_)
except ImportError:
return False

asarray = _as_numpy_array(actual)
if asarray is not None:
# Call ``__eq__()`` manually to prevent infinite-recursion with
# numpy<1.13. See #3748.
return all(self.__eq__(a) for a in asarray.flat)

# Short-circuit exact equality, except for bool
if isinstance(self.expected, bool) and not isinstance(actual, bool):
# Short-circuit exact equality, except for bool and np.bool_
if is_bool(self.expected) and not is_bool(actual):
return False
elif actual == self.expected:
return True

# If either type is non-numeric, fall back to strict equality.
# NB: we need Complex, rather than just Number, to ensure that __abs__,
# __sub__, and __float__ are defined. Also, consider bool to be
# nonnumeric, even though it has the required arithmetic.
if isinstance(self.expected, bool) or not (
# non-numeric, even though it has the required arithmetic.
if is_bool(self.expected) or not (
isinstance(self.expected, (Complex, Decimal))
and isinstance(actual, (Complex, Decimal))
):
Expand Down Expand Up @@ -620,8 +632,10 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
>>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
True

You can also use ``approx`` to compare nonnumeric types, or dicts and
sequences containing nonnumeric types, in which case it falls back to
**Non-numeric types**

You can also use ``approx`` to compare non-numeric types, or dicts and
sequences containing non-numeric types, in which case it falls back to
strict equality. This can be useful for comparing dicts and sequences that
can contain optional values::

Expand Down Expand Up @@ -678,6 +692,15 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
from the
`re_assert package <https://github.com/asottile/re-assert>`_.


.. note::

Unlike built-in equality, this function considers
booleans unequal to numeric zero or one. For example::

>>> 1 == approx(True)
False

.. warning::

.. versionchanged:: 3.2
Expand All @@ -696,10 +719,10 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:

.. versionchanged:: 3.7.1
``approx`` raises ``TypeError`` when it encounters a dict value or
sequence element of nonnumeric type.
sequence element of non-numeric type.

.. versionchanged:: 6.1.0
``approx`` falls back to strict equality for nonnumeric types instead
``approx`` falls back to strict equality for non-numeric types instead
of raising ``TypeError``.
"""
# Delegate the comparison to a class that knows how to deal with the type
Expand Down
3 changes: 2 additions & 1 deletion src/_pytest/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class TestReport(BaseReport):
"""

__test__ = False

# Defined by skipping plugin.
# xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish.
wasxfail: str
Expand Down Expand Up @@ -304,7 +305,7 @@ def __init__(
self.longrepr = longrepr

#: One of 'setup', 'call', 'teardown' to indicate runtest phase.
self.when = when
self.when: Literal["setup", "call", "teardown"] = when

#: User properties is a list of tuples (name, value) that holds user
#: defined properties of the test.
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/skipping.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def pytest_runtest_makereport(
pass # don't interfere
elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception):
assert call.excinfo.value.msg is not None
rep.wasxfail = "reason: " + call.excinfo.value.msg
rep.wasxfail = call.excinfo.value.msg
rep.outcome = "skipped"
elif not rep.skipped and xfailed:
if call.excinfo:
Expand Down
3 changes: 3 additions & 0 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,9 @@ def test():
rf"FAILED test_excgroup.py::test - {pre_catch}BaseExceptionGroup: Oops \(2.*"
)
result.stdout.re_match_lines(match_lines)
# Check for traceback filtering of pytest internals.
result.stdout.no_fnmatch_line("*, line *, in pytest_pyfunc_call")
result.stdout.no_fnmatch_line("*, line *, in pytest_runtest_call")


@pytest.mark.skipif(
Expand Down
6 changes: 3 additions & 3 deletions testing/plugins_integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
anyio[trio]==4.9.0
django==5.1.7
pytest-asyncio==0.25.3
pytest-asyncio==0.26.0
pytest-bdd==8.1.0
pytest-cov==6.0.0
pytest-django==4.10.0
pytest-cov==6.1.1
pytest-django==4.11.1
pytest-flakes==4.0.5
pytest-html==4.1.1
pytest-mock==3.14.0
Expand Down
11 changes: 10 additions & 1 deletion testing/python/approx.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,15 @@ def test_expecting_bool(self) -> None:
assert True != approx(False, abs=2) # noqa: E712
assert 1 != approx(True)

def test_expecting_bool_numpy(self) -> None:
"""Check approx comparing with numpy.bool (#13047)."""
np = pytest.importorskip("numpy")
assert np.False_ != approx(True)
assert np.True_ != approx(False)
assert np.True_ == approx(True)
assert np.False_ == approx(False)
assert np.True_ != approx(False, abs=2)

def test_list(self):
actual = [1 + 1e-7, 2 + 1e-8]
expected = [1, 2]
Expand Down Expand Up @@ -930,7 +939,7 @@ def test_nonnumeric_okay_if_equal(self, x):
],
)
def test_nonnumeric_false_if_unequal(self, x):
"""For nonnumeric types, x != pytest.approx(y) reduces to x != y"""
"""For non-numeric types, x != pytest.approx(y) reduces to x != y"""
assert "ab" != approx("abc")
assert ["ab"] != approx(["abc"])
# in particular, both of these should return False
Expand Down
Loading