Skip to content

Commit eba3621

Browse files
committed
raisesgroup followups
* renames src/_pytest/raises_group.py to src/_pytest/raises.py * moves pytest.raises from src/_pytest/python_api.py to src/_pytest/raises.py * adds several newsfragments that should've been bundlen with * add RaisesGroup & Matcher #13192 * add more detailed error message if you try to do RaisesGroup((ValueError, TypeError)) * mess around with ValueError vs TypeError on invalid expected exception
1 parent 33c7b05 commit eba3621

File tree

10 files changed

+287
-260
lines changed

10 files changed

+287
-260
lines changed

changelog/13192.feature.1.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:func:`pytest.raises` will now print a helpful string diff if matching fails and the match parameter has ``^`` and ``$`` and is otherwise escaped.

changelog/13192.feature.2.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The context-manager form of :func:`pytest.raises` now has a ``check`` parameter that takes a callable that should return `True` when passed the raised exception if it should be accepted.

changelog/13192.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:func:`pytest.raises` will now raise a warning when passing an empty string to ``match``, as this will match against any value. Use ``match="^$"`` if you want to check that an exception has no message.

src/_pytest/mark/structures.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from _pytest.deprecated import check_ispytest
2929
from _pytest.deprecated import MARKED_FIXTURE
3030
from _pytest.outcomes import fail
31-
from _pytest.raises_group import AbstractRaises
31+
from _pytest.raises import AbstractRaises
3232
from _pytest.scope import _ScopeName
3333
from _pytest.warning_types import PytestUnknownMarkWarning
3434

src/_pytest/python_api.py

-237
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# mypy: allow-untyped-defs
22
from __future__ import annotations
33

4-
from collections.abc import Callable
54
from collections.abc import Collection
65
from collections.abc import Mapping
76
from collections.abc import Sequence
@@ -10,23 +9,14 @@
109
import math
1110
from numbers import Complex
1211
import pprint
13-
import re
1412
import sys
1513
from typing import Any
16-
from typing import overload
1714
from typing import TYPE_CHECKING
18-
from typing import TypeVar
19-
20-
from _pytest._code import ExceptionInfo
21-
from _pytest.outcomes import fail
22-
from _pytest.raises_group import RaisesExc
2315

2416

2517
if TYPE_CHECKING:
2618
from numpy import ndarray
2719

28-
E = TypeVar("E", bound=BaseException, default=BaseException)
29-
3020

3121
def _compare_approx(
3222
full_object: object,
@@ -778,230 +768,3 @@ def _as_numpy_array(obj: object) -> ndarray | None:
778768
elif hasattr(obj, "__array__") or hasattr("obj", "__array_interface__"):
779769
return np.asarray(obj)
780770
return None
781-
782-
783-
# builtin pytest.raises helper
784-
# FIXME: This should probably me moved to 'src/_pytest.raises_group.py'
785-
# (and rename the file to 'raises.py')
786-
# since it's much more closely tied to those than to the other stuff in this file.
787-
788-
789-
@overload
790-
def raises(
791-
expected_exception: type[E] | tuple[type[E], ...],
792-
*,
793-
match: str | re.Pattern[str] | None = ...,
794-
check: Callable[[E], bool] = ...,
795-
) -> RaisesExc[E]: ...
796-
797-
798-
@overload
799-
def raises(
800-
*,
801-
match: str | re.Pattern[str],
802-
# If exception_type is not provided, check() must do any typechecks itself.
803-
check: Callable[[BaseException], bool] = ...,
804-
) -> RaisesExc[BaseException]: ...
805-
806-
807-
@overload
808-
def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException]: ...
809-
810-
811-
@overload
812-
def raises(
813-
expected_exception: type[E] | tuple[type[E], ...],
814-
func: Callable[..., Any],
815-
*args: Any,
816-
**kwargs: Any,
817-
) -> ExceptionInfo[E]: ...
818-
819-
820-
def raises(
821-
expected_exception: type[E] | tuple[type[E], ...] | None = None,
822-
*args: Any,
823-
**kwargs: Any,
824-
) -> RaisesExc[BaseException] | ExceptionInfo[E]:
825-
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
826-
827-
:param expected_exception:
828-
The expected exception type, or a tuple if one of multiple possible
829-
exception types are expected. Note that subclasses of the passed exceptions
830-
will also match.
831-
832-
:kwparam str | re.Pattern[str] | None match:
833-
If specified, a string containing a regular expression,
834-
or a regular expression object, that is tested against the string
835-
representation of the exception and its :pep:`678` `__notes__`
836-
using :func:`re.search`.
837-
838-
To match a literal string that may contain :ref:`special characters
839-
<re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
840-
841-
(This is only used when ``pytest.raises`` is used as a context manager,
842-
and passed through to the function otherwise.
843-
When using ``pytest.raises`` as a function, you can use:
844-
``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
845-
846-
Use ``pytest.raises`` as a context manager, which will capture the exception of the given
847-
type, or any of its subclasses::
848-
849-
>>> import pytest
850-
>>> with pytest.raises(ZeroDivisionError):
851-
... 1/0
852-
853-
If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example
854-
above), or no exception at all, the check will fail instead.
855-
856-
You can also use the keyword argument ``match`` to assert that the
857-
exception matches a text or regex::
858-
859-
>>> with pytest.raises(ValueError, match='must be 0 or None'):
860-
... raise ValueError("value must be 0 or None")
861-
862-
>>> with pytest.raises(ValueError, match=r'must be \d+$'):
863-
... raise ValueError("value must be 42")
864-
865-
The ``match`` argument searches the formatted exception string, which includes any
866-
`PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``:
867-
868-
>>> with pytest.raises(ValueError, match=r"had a note added"): # doctest: +SKIP
869-
... e = ValueError("value must be 42")
870-
... e.add_note("had a note added")
871-
... raise e
872-
873-
The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
874-
details of the captured exception::
875-
876-
>>> with pytest.raises(ValueError) as exc_info:
877-
... raise ValueError("value must be 42")
878-
>>> assert exc_info.type is ValueError
879-
>>> assert exc_info.value.args[0] == "value must be 42"
880-
881-
.. warning::
882-
883-
Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this::
884-
885-
# Careful, this will catch ANY exception raised.
886-
with pytest.raises(Exception):
887-
some_function()
888-
889-
Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide
890-
real bugs, where the user wrote this expecting a specific exception, but some other exception is being
891-
raised due to a bug introduced during a refactoring.
892-
893-
Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch
894-
**any** exception raised.
895-
896-
.. note::
897-
898-
When using ``pytest.raises`` as a context manager, it's worthwhile to
899-
note that normal context manager rules apply and that the exception
900-
raised *must* be the final line in the scope of the context manager.
901-
Lines of code after that, within the scope of the context manager will
902-
not be executed. For example::
903-
904-
>>> value = 15
905-
>>> with pytest.raises(ValueError) as exc_info:
906-
... if value > 10:
907-
... raise ValueError("value must be <= 10")
908-
... assert exc_info.type is ValueError # This will not execute.
909-
910-
Instead, the following approach must be taken (note the difference in
911-
scope)::
912-
913-
>>> with pytest.raises(ValueError) as exc_info:
914-
... if value > 10:
915-
... raise ValueError("value must be <= 10")
916-
...
917-
>>> assert exc_info.type is ValueError
918-
919-
**Expecting exception groups**
920-
921-
When expecting exceptions wrapped in :exc:`BaseExceptionGroup` or
922-
:exc:`ExceptionGroup`, you should instead use :class:`pytest.RaisesGroup`.
923-
924-
**Using with** ``pytest.mark.parametrize``
925-
926-
When using :ref:`pytest.mark.parametrize ref`
927-
it is possible to parametrize tests such that
928-
some runs raise an exception and others do not.
929-
930-
See :ref:`parametrizing_conditional_raising` for an example.
931-
932-
.. seealso::
933-
934-
:ref:`assertraises` for more examples and detailed discussion.
935-
936-
**Legacy form**
937-
938-
It is possible to specify a callable by passing a to-be-called lambda::
939-
940-
>>> raises(ZeroDivisionError, lambda: 1/0)
941-
<ExceptionInfo ...>
942-
943-
or you can specify an arbitrary callable with arguments::
944-
945-
>>> def f(x): return 1/x
946-
...
947-
>>> raises(ZeroDivisionError, f, 0)
948-
<ExceptionInfo ...>
949-
>>> raises(ZeroDivisionError, f, x=0)
950-
<ExceptionInfo ...>
951-
952-
The form above is fully supported but discouraged for new code because the
953-
context manager form is regarded as more readable and less error-prone.
954-
955-
.. note::
956-
Similar to caught exception objects in Python, explicitly clearing
957-
local references to returned ``ExceptionInfo`` objects can
958-
help the Python interpreter speed up its garbage collection.
959-
960-
Clearing those references breaks a reference cycle
961-
(``ExceptionInfo`` --> caught exception --> frame stack raising
962-
the exception --> current frame stack --> local variables -->
963-
``ExceptionInfo``) which makes Python keep all objects referenced
964-
from that cycle (including all local variables in the current
965-
frame) alive until the next cyclic garbage collection run.
966-
More detailed information can be found in the official Python
967-
documentation for :ref:`the try statement <python:try>`.
968-
"""
969-
__tracebackhide__ = True
970-
971-
if not args:
972-
if set(kwargs) - {"match", "check", "expected_exception"}:
973-
msg = "Unexpected keyword arguments passed to pytest.raises: "
974-
msg += ", ".join(sorted(kwargs))
975-
msg += "\nUse context-manager form instead?"
976-
raise TypeError(msg)
977-
978-
if expected_exception is None:
979-
return RaisesExc(**kwargs)
980-
return RaisesExc(expected_exception, **kwargs)
981-
982-
if not expected_exception:
983-
raise ValueError(
984-
f"Expected an exception type or a tuple of exception types, but got `{expected_exception!r}`. "
985-
f"Raising exceptions is already understood as failing the test, so you don't need "
986-
f"any special code to say 'this should never raise an exception'."
987-
)
988-
func = args[0]
989-
if not callable(func):
990-
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
991-
with RaisesExc(expected_exception) as excinfo:
992-
func(*args[1:], **kwargs)
993-
try:
994-
return excinfo
995-
finally:
996-
del excinfo
997-
998-
999-
# note: RaisesExc/RaisesGroup uses fail() internally, so this alias
1000-
# indicates (to [internal] plugins?) that `pytest.raises` will
1001-
# raise `_pytest.outcomes.Failed`, where
1002-
# `outcomes.Failed is outcomes.fail.Exception is raises.Exception`
1003-
# note: this is *not* the same as `_pytest.main.Failed`
1004-
# note: mypy does not recognize this attribute, and it's not possible
1005-
# to use a protocol/decorator like the others in outcomes due to
1006-
# https://github.com/python/mypy/issues/18715
1007-
raises.Exception = fail.Exception # type: ignore[attr-defined]

0 commit comments

Comments
 (0)