Skip to content

Commit 753df94

Browse files
committed
harmonize stringify_exception, various comments
1 parent 09d06fe commit 753df94

File tree

6 files changed

+62
-45
lines changed

6 files changed

+62
-45
lines changed

src/_pytest/_code/code.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,32 @@ def recursionindex(self) -> int | None:
459459
return None
460460

461461

462+
def stringify_exception(
463+
exc: BaseException, include_subexception_msg: bool = True
464+
) -> str:
465+
try:
466+
notes = getattr(exc, "__notes__", [])
467+
except KeyError:
468+
# Workaround for https://github.com/python/cpython/issues/98778 on
469+
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
470+
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
471+
if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
472+
notes = []
473+
else:
474+
raise
475+
if not include_subexception_msg and isinstance(exc, BaseExceptionGroup):
476+
message = exc.message
477+
else:
478+
message = str(exc)
479+
480+
return "\n".join(
481+
[
482+
message,
483+
*notes,
484+
]
485+
)
486+
487+
462488
E = TypeVar("E", bound=BaseException, covariant=True)
463489

464490

@@ -736,33 +762,14 @@ def getrepr(
736762
)
737763
return fmt.repr_excinfo(self)
738764

739-
def _stringify_exception(self, exc: BaseException) -> str:
740-
try:
741-
notes = getattr(exc, "__notes__", [])
742-
except KeyError:
743-
# Workaround for https://github.com/python/cpython/issues/98778 on
744-
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
745-
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
746-
if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
747-
notes = []
748-
else:
749-
raise
750-
751-
return "\n".join(
752-
[
753-
str(exc),
754-
*notes,
755-
]
756-
)
757-
758765
def match(self, regexp: str | re.Pattern[str]) -> Literal[True]:
759766
"""Check whether the regular expression `regexp` matches the string
760767
representation of the exception using :func:`python:re.search`.
761768
762769
If it matches `True` is returned, otherwise an `AssertionError` is raised.
763770
"""
764771
__tracebackhide__ = True
765-
value = self._stringify_exception(self.value)
772+
value = stringify_exception(self.value)
766773
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
767774
if regexp == value:
768775
msg += "\n Did you mean to `re.escape()` the regex?"
@@ -794,7 +801,7 @@ def _group_contains(
794801
if not isinstance(exc, expected_exception):
795802
continue
796803
if match is not None:
797-
value = self._stringify_exception(exc)
804+
value = stringify_exception(exc)
798805
if not re.search(match, value):
799806
continue
800807
return True

src/_pytest/outcomes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def __init__(
7777
super().__init__(msg)
7878

7979

80-
# Elaborate hack to work around https://github.com/python/mypy/issues/2087.
81-
# Ideally would just be `exit.Exception = Exit` etc.
80+
# We need a callable protocol to add attributes, for discussion see
81+
# https://github.com/python/mypy/issues/2087.
8282

8383
_F = TypeVar("_F", bound=Callable[..., object])
8484
_ET = TypeVar("_ET", bound=type[BaseException])

src/_pytest/python_api.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,9 @@ def _as_numpy_array(obj: object) -> ndarray | None:
787787

788788

789789
# builtin pytest.raises helper
790+
# FIXME: This should probably me moved to 'src/_pytest.raises_group.py'
791+
# (and rename the file to 'raises.py')
792+
# since it's much more closely tied to those than to the other stuff in this file.
790793

791794

792795
@overload
@@ -1000,9 +1003,11 @@ def raises(
10001003

10011004

10021005
# note: RaisesExc/RaisesGroup uses fail() internally, so this alias
1003-
# indicates (to [internal] plugins?) that `pytest.raises` will
1004-
# raise `_pytest.outcomes.Failed`, where
1005-
# `outcomes.Failed is outcomes.fail.Exception is raises.Exception`
1006+
# indicates (to [internal] plugins?) that `pytest.raises` will
1007+
# raise `_pytest.outcomes.Failed`, where
1008+
# `outcomes.Failed is outcomes.fail.Exception is raises.Exception`
10061009
# note: this is *not* the same as `_pytest.main.Failed`
1007-
# note: mypy does not recognize this attribute
1010+
# note: mypy does not recognize this attribute, and it's not possible
1011+
# to use a protocol/decorator like the others in outcomes due to
1012+
# https://github.com/python/mypy/issues/18715
10081013
raises.Exception = fail.Exception # type: ignore[attr-defined]

src/_pytest/raises_group.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import warnings
1919

2020
from _pytest._code import ExceptionInfo
21+
from _pytest._code.code import stringify_exception
2122
from _pytest.outcomes import fail
2223
from _pytest.warning_types import PytestWarning
2324

@@ -61,17 +62,6 @@
6162
from exceptiongroup import ExceptionGroup
6263

6364

64-
# this differs slightly from pytest.ExceptionInfo._stringify_exception
65-
# as we don't want '(1 sub-exception)' when matching group strings
66-
def _stringify_exception(exc: BaseException) -> str:
67-
return "\n".join(
68-
[
69-
exc.message if isinstance(exc, BaseExceptionGroup) else str(exc),
70-
*getattr(exc, "__notes__", []),
71-
],
72-
)
73-
74-
7565
# String patterns default to including the unicode flag.
7666
_REGEX_NO_FLAGS = re.compile(r"").flags
7767

@@ -141,6 +131,12 @@ def unescape(s: str) -> str:
141131
return re.sub(r"\\([{}()+-.*?^$\[\]\s\\])", r"\1", s)
142132

143133

134+
# These classes conceptually differ from ExceptionInfo in that ExceptionInfo is tied, and
135+
# constructed from, a particular exception - whereas these are constructed with expected
136+
# exceptions, and later allow matching towards particular exceptions.
137+
# But there's overlap in `ExceptionInfo.match` and `AbstractRaises._check_match`, as with
138+
# `AbstractRaises.matches` and `ExceptionInfo.errisinstance`+`ExceptionInfo.group_contains`.
139+
# The interaction between these classes should perhaps be improved.
144140
class AbstractRaises(ABC, Generic[BaseExcT_co]):
145141
"""ABC with common functionality shared between RaisesExc and RaisesGroup"""
146142

@@ -161,7 +157,6 @@ def __init__(
161157
if match == "":
162158
warnings.warn(
163159
PytestWarning(
164-
"session.shouldstop cannot be unset after it has been set; ignoring."
165160
"matching against an empty string will *always* pass. If you want "
166161
"to check for an empty message you need to pass '^$'. If you don't "
167162
"want to match you should pass `None` or leave out the parameter."
@@ -251,10 +246,13 @@ def _check_check(
251246
self._fail_reason = f"check{check_repr} did not return True"
252247
return False
253248

249+
# TODO: harmonize with ExceptionInfo.match
254250
def _check_match(self, e: BaseException) -> bool:
255251
if self.match is None or re.search(
256252
self.match,
257-
stringified_exception := _stringify_exception(e),
253+
stringified_exception := stringify_exception(
254+
e, include_subexception_msg=False
255+
),
258256
):
259257
return True
260258

testing/python/raises.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,10 @@ def tfunc(match):
261261
pytest.raises(ValueError, tfunc, match="").match("match=")
262262

263263
# empty string matches everything, which is probably not what the user wants
264-
# FIXME: I have no clue what session.shouldstop is doing here
265264
with pytest.warns(
266265
PytestWarning,
267266
match=wrap_escape(
268-
"session.shouldstop cannot be unset after it has been set; ignoring.matching against "
269-
"an empty string will *always* pass. If you want to check for an empty message you "
267+
"matching against an empty string will *always* pass. If you want to check for an empty message you "
270268
"need to pass '^$'. If you don't want to match you should pass `None` or leave out the parameter."
271269
),
272270
):

testing/typing_raises_group.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@
66

77
from typing_extensions import assert_type
88

9-
from _pytest.raises_group import RaisesExc
10-
from _pytest.raises_group import RaisesGroup
9+
from _pytest.main import Failed as main_Failed
10+
from _pytest.outcomes import Failed
11+
from pytest import raises
12+
from pytest import RaisesExc
13+
from pytest import RaisesGroup
1114

1215

16+
# does not work
17+
assert_type(raises.Exception, Failed) # type: ignore[assert-type, attr-defined]
18+
19+
# FIXME: these are different for some reason(?)
20+
assert Failed is not main_Failed # type: ignore[comparison-overlap]
21+
1322
if sys.version_info < (3, 11):
1423
from exceptiongroup import BaseExceptionGroup
1524
from exceptiongroup import ExceptionGroup

0 commit comments

Comments
 (0)