Skip to content

Commit 1de0923

Browse files
authored
Have pytest.raises match against exception __notes__ (#11227)
The doctest is skipped because add_note is only available in 3.11, Closes #11223
1 parent 7c30f67 commit 1de0923

File tree

5 files changed

+71
-8
lines changed

5 files changed

+71
-8
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ Ian Bicking
168168
Ian Lesperance
169169
Ilya Konstantinov
170170
Ionuț Turturică
171+
Isaac Virshup
171172
Itxaso Aizpurua
172173
Iwan Briquemont
173174
Jaap Broekhuizen

changelog/11227.improvement.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow :func:`pytest.raises` ``match`` argument to match against `PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``.

src/_pytest/_code/code.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,12 @@ def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
704704
If it matches `True` is returned, otherwise an `AssertionError` is raised.
705705
"""
706706
__tracebackhide__ = True
707-
value = str(self.value)
707+
value = "\n".join(
708+
[
709+
str(self.value),
710+
*getattr(self.value, "__notes__", []),
711+
]
712+
)
708713
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
709714
if regexp == value:
710715
msg += "\n Did you mean to `re.escape()` the regex?"

src/_pytest/python_api.py

+8
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,14 @@ def raises( # noqa: F811
843843
>>> with pytest.raises(ValueError, match=r'must be \d+$'):
844844
... raise ValueError("value must be 42")
845845
846+
The ``match`` argument searches the formatted exception string, which includes any
847+
`PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``:
848+
849+
>>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP
850+
... e = ValueError("value must be 42")
851+
... e.add_note("had a note added")
852+
... raise e
853+
846854
The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
847855
details of the captured exception::
848856

testing/code/test_excinfo.py

+55-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
from __future__ import annotations
2+
13
import importlib
24
import io
35
import operator
46
import queue
7+
import re
58
import sys
69
import textwrap
710
from pathlib import Path
811
from typing import Any
9-
from typing import Dict
10-
from typing import Tuple
1112
from typing import TYPE_CHECKING
12-
from typing import Union
1313

1414
import _pytest._code
1515
import pytest
@@ -801,7 +801,7 @@ def entry():
801801
)
802802
excinfo = pytest.raises(ValueError, mod.entry)
803803

804-
styles: Tuple[_TracebackStyle, ...] = ("long", "short")
804+
styles: tuple[_TracebackStyle, ...] = ("long", "short")
805805
for style in styles:
806806
p = FormattedExcinfo(style=style)
807807
reprtb = p.repr_traceback(excinfo)
@@ -928,7 +928,7 @@ def entry():
928928
)
929929
excinfo = pytest.raises(ValueError, mod.entry)
930930

931-
styles: Tuple[_TracebackStyle, ...] = ("short", "long", "no")
931+
styles: tuple[_TracebackStyle, ...] = ("short", "long", "no")
932932
for style in styles:
933933
for showlocals in (True, False):
934934
repr = excinfo.getrepr(style=style, showlocals=showlocals)
@@ -1090,7 +1090,7 @@ def f():
10901090
for funcargs in (True, False)
10911091
],
10921092
)
1093-
def test_format_excinfo(self, reproptions: Dict[str, Any]) -> None:
1093+
def test_format_excinfo(self, reproptions: dict[str, Any]) -> None:
10941094
def bar():
10951095
assert False, "some error"
10961096

@@ -1398,7 +1398,7 @@ def f():
13981398
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
13991399
def test_repr_traceback_with_unicode(style, encoding):
14001400
if encoding is None:
1401-
msg: Union[str, bytes] = "☹"
1401+
msg: str | bytes = "☹"
14021402
else:
14031403
msg = "☹".encode(encoding)
14041404
try:
@@ -1648,3 +1648,51 @@ def test():
16481648
],
16491649
consecutive=True,
16501650
)
1651+
1652+
1653+
def add_note(err: BaseException, msg: str) -> None:
1654+
"""Adds a note to an exception inplace."""
1655+
if sys.version_info < (3, 11):
1656+
err.__notes__ = getattr(err, "__notes__", []) + [msg] # type: ignore[attr-defined]
1657+
else:
1658+
err.add_note(msg)
1659+
1660+
1661+
@pytest.mark.parametrize(
1662+
"error,notes,match",
1663+
[
1664+
(Exception("test"), [], "test"),
1665+
(AssertionError("foo"), ["bar"], "bar"),
1666+
(AssertionError("foo"), ["bar", "baz"], "bar"),
1667+
(AssertionError("foo"), ["bar", "baz"], "baz"),
1668+
(ValueError("foo"), ["bar", "baz"], re.compile(r"bar\nbaz", re.MULTILINE)),
1669+
(ValueError("foo"), ["bar", "baz"], re.compile(r"BAZ", re.IGNORECASE)),
1670+
],
1671+
)
1672+
def test_check_error_notes_success(
1673+
error: Exception, notes: list[str], match: str
1674+
) -> None:
1675+
for note in notes:
1676+
add_note(error, note)
1677+
1678+
with pytest.raises(Exception, match=match):
1679+
raise error
1680+
1681+
1682+
@pytest.mark.parametrize(
1683+
"error, notes, match",
1684+
[
1685+
(Exception("test"), [], "foo"),
1686+
(AssertionError("foo"), ["bar"], "baz"),
1687+
(AssertionError("foo"), ["bar"], "foo\nbaz"),
1688+
],
1689+
)
1690+
def test_check_error_notes_failure(
1691+
error: Exception, notes: list[str], match: str
1692+
) -> None:
1693+
for note in notes:
1694+
add_note(error, note)
1695+
1696+
with pytest.raises(AssertionError):
1697+
with pytest.raises(type(error), match=match):
1698+
raise error

0 commit comments

Comments
 (0)