Skip to content

Commit e1e1874

Browse files
committed
rename AbstractMatcher -> AbstractRaises, Matcher->RaisesExc. Add docs on RaisesGroup&RaisesExc. Add warnings to group_contains. Remove group_contains example from getting-started page
1 parent 4737c8c commit e1e1874

15 files changed

+325
-215
lines changed

changelog/11538.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added :class:`pytest.RaisesGroup` (also export as ``pytest.raises_group``) and :class:`pytest.RaisesExc`, as an equivalent to :func:`pytest.raises` for expecting :exc:`ExceptionGroup`. It includes the ability to specify multiple different expected exceptions, the structure of nested exception groups, and flags for emulating :ref:`except* <except_star>`. See :ref:`assert-matching-exception-groups` and docstrings for more information.

changelog/11671.feature.rst

-1
This file was deleted.

changelog/12504.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:func:`pytest.mark.xfail` now accepts :class:`pytest.RaisesGroup` for the ``raises`` parameter when you expect an exception group. You can also pass a :class:`pytest.RaisesExc` if you e.g. want to make use of the ``check`` parameter.

doc/en/conf.py

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@
106106
("py:obj", "_pytest.fixtures.FixtureValue"),
107107
("py:obj", "_pytest.stash.T"),
108108
("py:class", "_ScopeName"),
109+
("py:class", "BaseExcT_1"),
110+
("py:class", "ExcT_1"),
109111
]
110112

111113
add_module_names = False

doc/en/getting-started.rst

+2-24
Original file line numberDiff line numberDiff line change
@@ -97,30 +97,6 @@ Use the :ref:`raises <assertraises>` helper to assert that some code raises an e
9797
with pytest.raises(SystemExit):
9898
f()
9999
100-
You can also use the context provided by :ref:`raises <assertraises>` to
101-
assert that an expected exception is part of a raised :class:`ExceptionGroup`:
102-
103-
.. code-block:: python
104-
105-
# content of test_exceptiongroup.py
106-
import pytest
107-
108-
109-
def f():
110-
raise ExceptionGroup(
111-
"Group message",
112-
[
113-
RuntimeError(),
114-
],
115-
)
116-
117-
118-
def test_exception_in_group():
119-
with pytest.raises(ExceptionGroup) as excinfo:
120-
f()
121-
assert excinfo.group_contains(RuntimeError)
122-
assert not excinfo.group_contains(TypeError)
123-
124100
Execute the test function with “quiet” reporting mode:
125101

126102
.. code-block:: pytest
@@ -133,6 +109,8 @@ Execute the test function with “quiet” reporting mode:
133109

134110
The ``-q/--quiet`` flag keeps the output brief in this and following examples.
135111

112+
See :ref:`assertraises` for specifying more details about the expected exception.
113+
136114
Group multiple tests in a class
137115
--------------------------------------------------------------
138116

doc/en/how-to/assert.rst

+87-2
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,93 @@ Notes:
145145

146146
.. _`assert-matching-exception-groups`:
147147

148-
Matching exception groups
149-
~~~~~~~~~~~~~~~~~~~~~~~~~
148+
Assertions about expected exception groups
149+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
150+
151+
When expecting a :exc:`BaseExceptionGroup` or :exc:`ExceptionGroup` you can use :class:`pytest.RaisesGroup`, also available as :class:`pytest.raises_group <pytest.RaisesGroup>`:
152+
153+
.. code-block:: python
154+
155+
def test_exception_in_group():
156+
with pytest.raises_group(ValueError):
157+
raise ExceptionGroup("group msg", [ValueError("value msg")])
158+
with pytest.raises_group(ValueError, TypeError):
159+
raise ExceptionGroup("msg", [ValueError("foo"), TypeError("bar")])
160+
161+
162+
It accepts a ``match`` parameter, that checks against the group message, and a ``check`` parameter that takes an arbitrary callable which it passes the group to, and only succeeds if the callable returns ``True``.
163+
164+
.. code-block:: python
165+
166+
def test_raisesgroup_match_and_check():
167+
with pytest.raises_group(BaseException, match="my group msg"):
168+
raise BaseExceptionGroup("my group msg", [KeyboardInterrupt()])
169+
with pytest.raises_group(
170+
Exception, check=lambda eg: isinstance(eg.__cause__, ValueError)
171+
):
172+
raise ExceptionGroup("", [TypeError()]) from ValueError()
173+
174+
It is strict about structure and unwrapped exceptions, unlike :ref:`except* <except_star>`, so you might want to set the ``flatten_subgroups`` and/or ``allow_unwrapped`` parameters.
175+
176+
.. code-block:: python
177+
178+
def test_structure():
179+
with pytest.raises_group(pytest.raises_group(ValueError)):
180+
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
181+
with pytest.raises_group(ValueError, flatten_subgroups=True):
182+
raise ExceptionGroup("1st group", [ExceptionGroup("2nd group", [ValueError()])])
183+
with pytest.raises_group(ValueError, allow_unwrapped=True):
184+
raise ValueError
185+
186+
To specify more details about the contained exception you can use :class:`pytest.RaisesExc`
187+
188+
.. code-block:: python
189+
190+
def test_raises_exc():
191+
with pytest.raises_group(pytest.RaisesExc(ValueError, match="foo")):
192+
raise ExceptionGroup("", (ValueError("foo")))
193+
194+
They both supply a method :meth:`pytest.RaisesGroup.matches` :meth:`pytest.RaisesExc.matches` if you want to do matching outside of using it as a contextmanager. This can be helpful when checking ``.__context__`` or ``.__cause__``.
195+
196+
.. code-block:: python
197+
198+
def test_matches():
199+
exc = ValueError()
200+
exc_group = ExceptionGroup("", [exc])
201+
if RaisesGroup(ValueError).matches(exc_group):
202+
...
203+
# helpful error is available in `.fail_reason` if it fails to match
204+
r = RaisesExc(ValueError)
205+
assert r.matches(e), r.fail_reason
206+
207+
Check the documentation on :class:`pytest.RaisesGroup` and :class:`pytest.RaisesExc` for more details and examples.
208+
209+
``ExceptionInfo.group_contains()``
210+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
211+
212+
.. warning::
213+
214+
This helper makes it easy to check for the presence of specific exceptions, but it is very bad for checking that the group does *not* contain *any other exceptions*. So this will pass:
215+
216+
.. code-block:: python
217+
218+
class EXTREMELYBADERROR(BaseException):
219+
"""This is a very bad error to miss"""
220+
221+
222+
def test_for_value_error():
223+
with pytest.raises(ExceptionGroup) as excinfo:
224+
excs = [ValueError()]
225+
if very_unlucky():
226+
excs.append(EXTREMELYBADERROR())
227+
raise ExceptionGroup("", excs)
228+
# this passes regardless of if there's other exceptions
229+
assert excinfo.group_contains(ValueError)
230+
# you can't simply list all exceptions you *don't* want to get here
231+
232+
233+
There is no good way of using :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>` to ensure you're not getting *any* other exceptions than the one you expected.
234+
You should instead use :class:`pytest.raises_group <pytest.RaisesGroup>`, see :ref:`assert-matching-exception-groups`.
150235

151236
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
152237
method to test for exceptions returned as part of an :class:`ExceptionGroup`:

doc/en/reference/reference.rst

+12
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,18 @@ PytestPluginManager
10141014
:inherited-members:
10151015
:show-inheritance:
10161016

1017+
RaisesExc
1018+
~~~~~~~~~
1019+
1020+
.. autoclass:: pytest.RaisesExc()
1021+
:members:
1022+
1023+
RaisesGroup
1024+
~~~~~~~~~~~
1025+
1026+
.. autoclass:: pytest.RaisesGroup()
1027+
:members:
1028+
10171029
TerminalReporter
10181030
~~~~~~~~~~~~~~~~
10191031

src/_pytest/_code/code.py

+7
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,13 @@ def group_contains(
828828
the exceptions contained within the topmost exception group).
829829
830830
.. versionadded:: 8.0
831+
832+
.. warning::
833+
This helper makes it easy to check for the presence of specific exceptions,
834+
but it is very bad for checking that the group does *not* contain
835+
*any other exceptions*.
836+
You should instead consider using :class:`pytest.raises_group <pytest.RaisesGroup>`
837+
831838
"""
832839
msg = "Captured exception is not an instance of `BaseExceptionGroup`"
833840
assert isinstance(self.value, BaseExceptionGroup), msg

0 commit comments

Comments
 (0)