Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 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
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`.
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.

44 changes: 26 additions & 18 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,35 +1099,51 @@ def _makepath(self, path: Path | str) -> str:
return np
return str(path)

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

def _repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
traceback_ = self._filtered_traceback(excinfo)

if isinstance(excinfo.value, RecursionError):
traceback, extraline = self._truncate_recursive_traceback(traceback)
traceback_, extraline = self._truncate_recursive_traceback(traceback_)
else:
extraline = None

if not traceback:
if not traceback_:
if extraline is None:
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
entries = [self.repr_traceback_entry(None, excinfo)]
return ReprTraceback(entries, extraline, style=self.style)

last = traceback[-1]
last = traceback_[-1]
if self.style == "value":
entries = [self.repr_traceback_entry(last, excinfo)]
return ReprTraceback(entries, None, style=self.style)

entries = [
self.repr_traceback_entry(entry, excinfo if last == entry else None)
for entry in traceback
for entry in traceback_
]
return ReprTraceback(entries, extraline, style=self.style)

def _repr_exception_group_traceback(
self, excinfo: ExceptionInfo[BaseExceptionGroup]
) -> ReprTracebackNative:
traceback_ = self._filtered_traceback(excinfo)
return ReprTracebackNative(
traceback.format_exception(
type(excinfo.value),
excinfo.value,
traceback_[0]._rawentry,
)
)

def _truncate_recursive_traceback(
self, traceback: Traceback
) -> tuple[Traceback, str | None]:
Expand Down Expand Up @@ -1179,17 +1195,9 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR
# full support for exception groups added to ExceptionInfo.
# See https://github.com/pytest-dev/pytest/issues/9159
if isinstance(e, BaseExceptionGroup):
reprtraceback: ReprTracebackNative | ReprTraceback = (
ReprTracebackNative(
traceback.format_exception(
type(excinfo_.value),
excinfo_.value,
excinfo_.traceback[0]._rawentry,
)
)
)
reprtraceback = self._repr_exception_group_traceback(excinfo_)
else:
reprtraceback = self.repr_traceback(excinfo_)
reprtraceback = self._repr_traceback(excinfo_)
reprcrash = excinfo_._getreprcrash()
else:
# Fallback to native repr if the exception doesn't have a traceback:
Expand Down
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
2 changes: 2 additions & 0 deletions testing/test_helpconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
monkeypatch.delenv("PYTEST_PLUGINS", raising=False)
result = pytester.runpytest("--version", "--version")
assert result.ret == 0
result.stdout.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"])
Expand All @@ -17,6 +18,7 @@ def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:

def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
monkeypatch.delenv("PYTEST_PLUGINS", raising=False)
result = pytester.runpytest("--version")
assert result.ret == 0
result.stdout.fnmatch_lines([f"pytest {pytest.__version__}"])
Expand Down
8 changes: 4 additions & 4 deletions testing/test_skipping.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,8 @@ def test_this_false():
result = pytester.runpytest(p, "-rx")
result.stdout.fnmatch_lines(
[
"*test_one*test_this - reason: *NOTRUN* noway",
"*test_one*test_this_true - reason: *NOTRUN* condition: True",
"*test_one*test_this - *NOTRUN* noway",
"*test_one*test_this_true - *NOTRUN* condition: True",
"*1 passed*",
]
)
Expand Down Expand Up @@ -492,7 +492,7 @@ def test_this():
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(["*1 xfailed*"])
result = pytester.runpytest(p, "-rx")
result.stdout.fnmatch_lines(["*XFAIL*test_this*reason:*hello*"])
result.stdout.fnmatch_lines(["*XFAIL*test_this*hello*"])
result = pytester.runpytest(p, "--runxfail")
result.stdout.fnmatch_lines(["*1 pass*"])

Expand All @@ -510,7 +510,7 @@ def test_this():
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(["*1 xfailed*"])
result = pytester.runpytest(p, "-rx")
result.stdout.fnmatch_lines(["*XFAIL*test_this*reason:*hello*"])
result.stdout.fnmatch_lines(["*XFAIL*test_this*hello*"])
result = pytester.runpytest(p, "--runxfail")
result.stdout.fnmatch_lines(
"""
Expand Down
1 change: 1 addition & 0 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ def test_header_trailer_info(
self, monkeypatch: MonkeyPatch, pytester: Pytester, request
) -> None:
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
monkeypatch.delenv("PYTEST_PLUGINS", raising=False)
pytester.makepyfile(
"""
def test_passes():
Expand Down
Loading
Loading