| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517 |
- from __future__ import annotations
- from abc import ABC
- from abc import abstractmethod
- import re
- from re import Pattern
- import sys
- from textwrap import indent
- from typing import Any
- from typing import cast
- from typing import final
- from typing import Generic
- from typing import get_args
- from typing import get_origin
- from typing import Literal
- from typing import overload
- from typing import TYPE_CHECKING
- import warnings
- from _pytest._code import ExceptionInfo
- from _pytest._code.code import stringify_exception
- from _pytest.outcomes import fail
- from _pytest.warning_types import PytestWarning
- if TYPE_CHECKING:
- from collections.abc import Callable
- from collections.abc import Sequence
- # for some reason Sphinx does not play well with 'from types import TracebackType'
- import types
- from typing import TypeGuard
- from typing_extensions import ParamSpec
- from typing_extensions import TypeVar
- P = ParamSpec("P")
- # this conditional definition is because we want to allow a TypeVar default
- BaseExcT_co_default = TypeVar(
- "BaseExcT_co_default",
- bound=BaseException,
- default=BaseException,
- covariant=True,
- )
- # Use short name because it shows up in docs.
- E = TypeVar("E", bound=BaseException, default=BaseException)
- else:
- from typing import TypeVar
- BaseExcT_co_default = TypeVar(
- "BaseExcT_co_default", bound=BaseException, covariant=True
- )
- # RaisesGroup doesn't work with a default.
- BaseExcT_co = TypeVar("BaseExcT_co", bound=BaseException, covariant=True)
- BaseExcT_1 = TypeVar("BaseExcT_1", bound=BaseException)
- BaseExcT_2 = TypeVar("BaseExcT_2", bound=BaseException)
- ExcT_1 = TypeVar("ExcT_1", bound=Exception)
- ExcT_2 = TypeVar("ExcT_2", bound=Exception)
- if sys.version_info < (3, 11):
- from exceptiongroup import BaseExceptionGroup
- from exceptiongroup import ExceptionGroup
- # String patterns default to including the unicode flag.
- _REGEX_NO_FLAGS = re.compile(r"").flags
- # pytest.raises helper
- @overload
- def raises(
- expected_exception: type[E] | tuple[type[E], ...],
- *,
- match: str | re.Pattern[str] | None = ...,
- check: Callable[[E], bool] = ...,
- ) -> RaisesExc[E]: ...
- @overload
- def raises(
- *,
- match: str | re.Pattern[str],
- # If exception_type is not provided, check() must do any typechecks itself.
- check: Callable[[BaseException], bool] = ...,
- ) -> RaisesExc[BaseException]: ...
- @overload
- def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException]: ...
- @overload
- def raises(
- expected_exception: type[E] | tuple[type[E], ...],
- func: Callable[..., Any],
- *args: Any,
- **kwargs: Any,
- ) -> ExceptionInfo[E]: ...
- def raises(
- expected_exception: type[E] | tuple[type[E], ...] | None = None,
- *args: Any,
- **kwargs: Any,
- ) -> RaisesExc[BaseException] | ExceptionInfo[E]:
- r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
- :param expected_exception:
- The expected exception type, or a tuple if one of multiple possible
- exception types are expected. Note that subclasses of the passed exceptions
- will also match.
- This is not a required parameter, you may opt to only use ``match`` and/or
- ``check`` for verifying the raised exception.
- :kwparam str | re.Pattern[str] | None match:
- If specified, a string containing a regular expression,
- or a regular expression object, that is tested against the string
- representation of the exception and its :pep:`678` `__notes__`
- using :func:`re.search`.
- To match a literal string that may contain :ref:`special characters
- <re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
- (This is only used when ``pytest.raises`` is used as a context manager,
- and passed through to the function otherwise.
- When using ``pytest.raises`` as a function, you can use:
- ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
- :kwparam Callable[[BaseException], bool] check:
- .. versionadded:: 8.4
- If specified, a callable that will be called with the exception as a parameter
- after checking the type and the match regex if specified.
- If it returns ``True`` it will be considered a match, if not it will
- be considered a failed match.
- Use ``pytest.raises`` as a context manager, which will capture the exception of the given
- type, or any of its subclasses::
- >>> import pytest
- >>> with pytest.raises(ZeroDivisionError):
- ... 1/0
- If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example
- above), or no exception at all, the check will fail instead.
- You can also use the keyword argument ``match`` to assert that the
- exception matches a text or regex::
- >>> with pytest.raises(ValueError, match='must be 0 or None'):
- ... raise ValueError("value must be 0 or None")
- >>> with pytest.raises(ValueError, match=r'must be \d+$'):
- ... raise ValueError("value must be 42")
- The ``match`` argument searches the formatted exception string, which includes any
- `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``:
- >>> with pytest.raises(ValueError, match=r"had a note added"): # doctest: +SKIP
- ... e = ValueError("value must be 42")
- ... e.add_note("had a note added")
- ... raise e
- The ``check`` argument, if provided, must return True when passed the raised exception
- for the match to be successful, otherwise an :exc:`AssertionError` is raised.
- >>> import errno
- >>> with pytest.raises(OSError, check=lambda e: e.errno == errno.EACCES):
- ... raise OSError(errno.EACCES, "no permission to view")
- The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
- details of the captured exception::
- >>> with pytest.raises(ValueError) as exc_info:
- ... raise ValueError("value must be 42")
- >>> assert exc_info.type is ValueError
- >>> assert exc_info.value.args[0] == "value must be 42"
- .. warning::
- Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this::
- # Careful, this will catch ANY exception raised.
- with pytest.raises(Exception):
- some_function()
- Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide
- real bugs, where the user wrote this expecting a specific exception, but some other exception is being
- raised due to a bug introduced during a refactoring.
- Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch
- **any** exception raised.
- .. note::
- When using ``pytest.raises`` as a context manager, it's worthwhile to
- note that normal context manager rules apply and that the exception
- raised *must* be the final line in the scope of the context manager.
- Lines of code after that, within the scope of the context manager will
- not be executed. For example::
- >>> value = 15
- >>> with pytest.raises(ValueError) as exc_info:
- ... if value > 10:
- ... raise ValueError("value must be <= 10")
- ... assert exc_info.type is ValueError # This will not execute.
- Instead, the following approach must be taken (note the difference in
- scope)::
- >>> with pytest.raises(ValueError) as exc_info:
- ... if value > 10:
- ... raise ValueError("value must be <= 10")
- ...
- >>> assert exc_info.type is ValueError
- **Expecting exception groups**
- When expecting exceptions wrapped in :exc:`BaseExceptionGroup` or
- :exc:`ExceptionGroup`, you should instead use :class:`pytest.RaisesGroup`.
- **Using with** ``pytest.mark.parametrize``
- When using :ref:`pytest.mark.parametrize ref`
- it is possible to parametrize tests such that
- some runs raise an exception and others do not.
- See :ref:`parametrizing_conditional_raising` for an example.
- .. seealso::
- :ref:`assertraises` for more examples and detailed discussion.
- **Legacy form**
- It is possible to specify a callable by passing a to-be-called lambda::
- >>> raises(ZeroDivisionError, lambda: 1/0)
- <ExceptionInfo ...>
- or you can specify an arbitrary callable with arguments::
- >>> def f(x): return 1/x
- ...
- >>> raises(ZeroDivisionError, f, 0)
- <ExceptionInfo ...>
- >>> raises(ZeroDivisionError, f, x=0)
- <ExceptionInfo ...>
- The form above is fully supported but discouraged for new code because the
- context manager form is regarded as more readable and less error-prone.
- .. note::
- Similar to caught exception objects in Python, explicitly clearing
- local references to returned ``ExceptionInfo`` objects can
- help the Python interpreter speed up its garbage collection.
- Clearing those references breaks a reference cycle
- (``ExceptionInfo`` --> caught exception --> frame stack raising
- the exception --> current frame stack --> local variables -->
- ``ExceptionInfo``) which makes Python keep all objects referenced
- from that cycle (including all local variables in the current
- frame) alive until the next cyclic garbage collection run.
- More detailed information can be found in the official Python
- documentation for :ref:`the try statement <python:try>`.
- """
- __tracebackhide__ = True
- if not args:
- if set(kwargs) - {"match", "check", "expected_exception"}:
- msg = "Unexpected keyword arguments passed to pytest.raises: "
- msg += ", ".join(sorted(kwargs))
- msg += "\nUse context-manager form instead?"
- raise TypeError(msg)
- if expected_exception is None:
- return RaisesExc(**kwargs)
- return RaisesExc(expected_exception, **kwargs)
- if not expected_exception:
- raise ValueError(
- f"Expected an exception type or a tuple of exception types, but got `{expected_exception!r}`. "
- f"Raising exceptions is already understood as failing the test, so you don't need "
- f"any special code to say 'this should never raise an exception'."
- )
- func = args[0]
- if not callable(func):
- raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
- with RaisesExc(expected_exception) as excinfo:
- func(*args[1:], **kwargs)
- try:
- return excinfo
- finally:
- del excinfo
- # note: RaisesExc/RaisesGroup uses fail() internally, so this alias
- # indicates (to [internal] plugins?) that `pytest.raises` will
- # raise `_pytest.outcomes.Failed`, where
- # `outcomes.Failed is outcomes.fail.Exception is raises.Exception`
- # note: this is *not* the same as `_pytest.main.Failed`
- # note: mypy does not recognize this attribute, and it's not possible
- # to use a protocol/decorator like the others in outcomes due to
- # https://github.com/python/mypy/issues/18715
- raises.Exception = fail.Exception # type: ignore[attr-defined]
- def _match_pattern(match: Pattern[str]) -> str | Pattern[str]:
- """Helper function to remove redundant `re.compile` calls when printing regex"""
- return match.pattern if match.flags == _REGEX_NO_FLAGS else match
- def repr_callable(fun: Callable[[BaseExcT_1], bool]) -> str:
- """Get the repr of a ``check`` parameter.
- Split out so it can be monkeypatched (e.g. by hypothesis)
- """
- return repr(fun)
- def backquote(s: str) -> str:
- return "`" + s + "`"
- def _exception_type_name(
- e: type[BaseException] | tuple[type[BaseException], ...],
- ) -> str:
- if isinstance(e, type):
- return e.__name__
- if len(e) == 1:
- return e[0].__name__
- return "(" + ", ".join(ee.__name__ for ee in e) + ")"
- def _check_raw_type(
- expected_type: type[BaseException] | tuple[type[BaseException], ...] | None,
- exception: BaseException,
- ) -> str | None:
- if expected_type is None or expected_type == ():
- return None
- if not isinstance(
- exception,
- expected_type,
- ):
- actual_type_str = backquote(_exception_type_name(type(exception)) + "()")
- expected_type_str = backquote(_exception_type_name(expected_type))
- if (
- isinstance(exception, BaseExceptionGroup)
- and isinstance(expected_type, type)
- and not issubclass(expected_type, BaseExceptionGroup)
- ):
- return f"Unexpected nested {actual_type_str}, expected {expected_type_str}"
- return f"{actual_type_str} is not an instance of {expected_type_str}"
- return None
- def is_fully_escaped(s: str) -> bool:
- # we know we won't compile with re.VERBOSE, so whitespace doesn't need to be escaped
- metacharacters = "{}()+.*?^$[]"
- return not any(
- c in metacharacters and (i == 0 or s[i - 1] != "\\") for (i, c) in enumerate(s)
- )
- def unescape(s: str) -> str:
- return re.sub(r"\\([{}()+-.*?^$\[\]\s\\])", r"\1", s)
- # These classes conceptually differ from ExceptionInfo in that ExceptionInfo is tied, and
- # constructed from, a particular exception - whereas these are constructed with expected
- # exceptions, and later allow matching towards particular exceptions.
- # But there's overlap in `ExceptionInfo.match` and `AbstractRaises._check_match`, as with
- # `AbstractRaises.matches` and `ExceptionInfo.errisinstance`+`ExceptionInfo.group_contains`.
- # The interaction between these classes should perhaps be improved.
- class AbstractRaises(ABC, Generic[BaseExcT_co]):
- """ABC with common functionality shared between RaisesExc and RaisesGroup"""
- def __init__(
- self,
- *,
- match: str | Pattern[str] | None,
- check: Callable[[BaseExcT_co], bool] | None,
- ) -> None:
- if isinstance(match, str):
- # juggle error in order to avoid context to fail (necessary?)
- re_error = None
- try:
- self.match: Pattern[str] | None = re.compile(match)
- except re.error as e:
- re_error = e
- if re_error is not None:
- fail(f"Invalid regex pattern provided to 'match': {re_error}")
- if match == "":
- warnings.warn(
- PytestWarning(
- "matching against an empty string will *always* pass. If you want "
- "to check for an empty message you need to pass '^$'. If you don't "
- "want to match you should pass `None` or leave out the parameter."
- ),
- stacklevel=2,
- )
- else:
- self.match = match
- # check if this is a fully escaped regex and has ^$ to match fully
- # in which case we can do a proper diff on error
- self.rawmatch: str | None = None
- if isinstance(match, str) or (
- isinstance(match, Pattern) and match.flags == _REGEX_NO_FLAGS
- ):
- if isinstance(match, Pattern):
- match = match.pattern
- if (
- match
- and match[0] == "^"
- and match[-1] == "$"
- and is_fully_escaped(match[1:-1])
- ):
- self.rawmatch = unescape(match[1:-1])
- self.check = check
- self._fail_reason: str | None = None
- # used to suppress repeated printing of `repr(self.check)`
- self._nested: bool = False
- # set in self._parse_exc
- self.is_baseexception = False
- def _parse_exc(
- self, exc: type[BaseExcT_1] | types.GenericAlias, expected: str
- ) -> type[BaseExcT_1]:
- if isinstance(exc, type) and issubclass(exc, BaseException):
- if not issubclass(exc, Exception):
- self.is_baseexception = True
- return exc
- # because RaisesGroup does not support variable number of exceptions there's
- # still a use for RaisesExc(ExceptionGroup[Exception]).
- origin_exc: type[BaseException] | None = get_origin(exc)
- if origin_exc and issubclass(origin_exc, BaseExceptionGroup):
- exc_type = get_args(exc)[0]
- if (
- issubclass(origin_exc, ExceptionGroup) and exc_type in (Exception, Any)
- ) or (
- issubclass(origin_exc, BaseExceptionGroup)
- and exc_type in (BaseException, Any)
- ):
- if not issubclass(origin_exc, ExceptionGroup):
- self.is_baseexception = True
- return cast(type[BaseExcT_1], origin_exc)
- else:
- raise ValueError(
- f"Only `ExceptionGroup[Exception]` or `BaseExceptionGroup[BaseException]` "
- f"are accepted as generic types but got `{exc}`. "
- f"As `raises` will catch all instances of the specified group regardless of the "
- f"generic argument specific nested exceptions has to be checked "
- f"with `RaisesGroup`."
- )
- # unclear if the Type/ValueError distinction is even helpful here
- msg = f"Expected {expected}, but got "
- if isinstance(exc, type): # type: ignore[unreachable]
- raise ValueError(msg + f"{exc.__name__!r}")
- if isinstance(exc, BaseException): # type: ignore[unreachable]
- raise TypeError(msg + f"an exception instance: {type(exc).__name__}")
- raise TypeError(msg + repr(type(exc).__name__))
- @property
- def fail_reason(self) -> str | None:
- """Set after a call to :meth:`matches` to give a human-readable reason for why the match failed.
- When used as a context manager the string will be printed as the reason for the
- test failing."""
- return self._fail_reason
- def _check_check(
- self: AbstractRaises[BaseExcT_1],
- exception: BaseExcT_1,
- ) -> bool:
- if self.check is None:
- return True
- if self.check(exception):
- return True
- check_repr = "" if self._nested else " " + repr_callable(self.check)
- self._fail_reason = f"check{check_repr} did not return True"
- return False
- # TODO: harmonize with ExceptionInfo.match
- def _check_match(self, e: BaseException) -> bool:
- if self.match is None or re.search(
- self.match,
- stringified_exception := stringify_exception(
- e, include_subexception_msg=False
- ),
- ):
- return True
- # if we're matching a group, make sure we're explicit to reduce confusion
- # if they're trying to match an exception contained within the group
- maybe_specify_type = (
- f" the `{_exception_type_name(type(e))}()`"
- if isinstance(e, BaseExceptionGroup)
- else ""
- )
- if isinstance(self.rawmatch, str):
- # TODO: it instructs to use `-v` to print leading text, but that doesn't work
- # I also don't know if this is the proper entry point, or tool to use at all
- from _pytest.assertion.util import _diff_text
- from _pytest.assertion.util import dummy_highlighter
- diff = _diff_text(self.rawmatch, stringified_exception, dummy_highlighter)
- self._fail_reason = ("\n" if diff[0][0] == "-" else "") + "\n".join(diff)
- return False
- self._fail_reason = (
- f"Regex pattern did not match{maybe_specify_type}.\n"
- f" Expected regex: {_match_pattern(self.match)!r}\n"
- f" Actual message: {stringified_exception!r}"
- )
- if _match_pattern(self.match) == stringified_exception:
- self._fail_reason += "\n Did you mean to `re.escape()` the regex?"
- return False
- @abstractmethod
- def matches(
- self: AbstractRaises[BaseExcT_1], exception: BaseException
- ) -> TypeGuard[BaseExcT_1]:
- """Check if an exception matches the requirements of this AbstractRaises.
- If it fails, :meth:`AbstractRaises.fail_reason` should be set.
- """
- @final
- class RaisesExc(AbstractRaises[BaseExcT_co_default]):
- """
- .. versionadded:: 8.4
- This is the class constructed when calling :func:`pytest.raises`, but may be used
- directly as a helper class with :class:`RaisesGroup` when you want to specify
- requirements on sub-exceptions.
- You don't need this if you only want to specify the type, since :class:`RaisesGroup`
- accepts ``type[BaseException]``.
- :param type[BaseException] | tuple[type[BaseException]] | None expected_exception:
- The expected type, or one of several possible types.
- May be ``None`` in order to only make use of ``match`` and/or ``check``
- The type is checked with :func:`isinstance`, and does not need to be an exact match.
- If that is wanted you can use the ``check`` parameter.
- :kwparam str | Pattern[str] match:
- A regex to match.
- :kwparam Callable[[BaseException], bool] check:
- If specified, a callable that will be called with the exception as a parameter
- after checking the type and the match regex if specified.
- If it returns ``True`` it will be considered a match, if not it will
- be considered a failed match.
- :meth:`RaisesExc.matches` can also be used standalone to check individual exceptions.
- Examples::
- with RaisesGroup(RaisesExc(ValueError, match="string"))
- ...
- with RaisesGroup(RaisesExc(check=lambda x: x.args == (3, "hello"))):
- ...
- with RaisesGroup(RaisesExc(check=lambda x: type(x) is ValueError)):
- ...
- """
- # Trio bundled hypothesis monkeypatching, we will probably instead assume that
- # hypothesis will handle that in their pytest plugin by the time this is released.
- # Alternatively we could add a version of get_pretty_function_description ourselves
- # https://github.com/HypothesisWorks/hypothesis/blob/8ced2f59f5c7bea3344e35d2d53e1f8f8eb9fcd8/hypothesis-python/src/hypothesis/internal/reflection.py#L439
- # At least one of the three parameters must be passed.
- @overload
- def __init__(
- self,
- expected_exception: (
- type[BaseExcT_co_default] | tuple[type[BaseExcT_co_default], ...]
- ),
- /,
- *,
- match: str | Pattern[str] | None = ...,
- check: Callable[[BaseExcT_co_default], bool] | None = ...,
- ) -> None: ...
- @overload
- def __init__(
- self: RaisesExc[BaseException], # Give E a value.
- /,
- *,
- match: str | Pattern[str] | None,
- # If exception_type is not provided, check() must do any typechecks itself.
- check: Callable[[BaseException], bool] | None = ...,
- ) -> None: ...
- @overload
- def __init__(self, /, *, check: Callable[[BaseException], bool]) -> None: ...
- def __init__(
- self,
- expected_exception: (
- type[BaseExcT_co_default] | tuple[type[BaseExcT_co_default], ...] | None
- ) = None,
- /,
- *,
- match: str | Pattern[str] | None = None,
- check: Callable[[BaseExcT_co_default], bool] | None = None,
- ):
- super().__init__(match=match, check=check)
- if isinstance(expected_exception, tuple):
- expected_exceptions = expected_exception
- elif expected_exception is None:
- expected_exceptions = ()
- else:
- expected_exceptions = (expected_exception,)
- if (expected_exceptions == ()) and match is None and check is None:
- raise ValueError("You must specify at least one parameter to match on.")
- self.expected_exceptions = tuple(
- self._parse_exc(e, expected="a BaseException type")
- for e in expected_exceptions
- )
- self._just_propagate = False
- def matches(
- self,
- exception: BaseException | None,
- ) -> TypeGuard[BaseExcT_co_default]:
- """Check if an exception matches the requirements of this :class:`RaisesExc`.
- If it fails, :attr:`RaisesExc.fail_reason` will be set.
- Examples::
- assert RaisesExc(ValueError).matches(my_exception):
- # is equivalent to
- assert isinstance(my_exception, ValueError)
- # this can be useful when checking e.g. the ``__cause__`` of an exception.
- with pytest.raises(ValueError) as excinfo:
- ...
- assert RaisesExc(SyntaxError, match="foo").matches(excinfo.value.__cause__)
- # above line is equivalent to
- assert isinstance(excinfo.value.__cause__, SyntaxError)
- assert re.search("foo", str(excinfo.value.__cause__)
- """
- self._just_propagate = False
- if exception is None:
- self._fail_reason = "exception is None"
- return False
- if not self._check_type(exception):
- self._just_propagate = True
- return False
- if not self._check_match(exception):
- return False
- return self._check_check(exception)
- def __repr__(self) -> str:
- parameters = []
- if self.expected_exceptions:
- parameters.append(_exception_type_name(self.expected_exceptions))
- if self.match is not None:
- # If no flags were specified, discard the redundant re.compile() here.
- parameters.append(
- f"match={_match_pattern(self.match)!r}",
- )
- if self.check is not None:
- parameters.append(f"check={repr_callable(self.check)}")
- return f"RaisesExc({', '.join(parameters)})"
- def _check_type(self, exception: BaseException) -> TypeGuard[BaseExcT_co_default]:
- self._fail_reason = _check_raw_type(self.expected_exceptions, exception)
- return self._fail_reason is None
- def __enter__(self) -> ExceptionInfo[BaseExcT_co_default]:
- self.excinfo: ExceptionInfo[BaseExcT_co_default] = ExceptionInfo.for_later()
- return self.excinfo
- # TODO: move common code into superclass
- def __exit__(
- self,
- exc_type: type[BaseException] | None,
- exc_val: BaseException | None,
- exc_tb: types.TracebackType | None,
- ) -> bool:
- __tracebackhide__ = True
- if exc_type is None:
- if not self.expected_exceptions:
- fail("DID NOT RAISE any exception")
- if len(self.expected_exceptions) > 1:
- fail(f"DID NOT RAISE any of {self.expected_exceptions!r}")
- fail(f"DID NOT RAISE {self.expected_exceptions[0]!r}")
- assert self.excinfo is not None, (
- "Internal error - should have been constructed in __enter__"
- )
- if not self.matches(exc_val):
- if self._just_propagate:
- return False
- raise AssertionError(self._fail_reason)
- # Cast to narrow the exception type now that it's verified....
- # even though the TypeGuard in self.matches should be narrowing
- exc_info = cast(
- "tuple[type[BaseExcT_co_default], BaseExcT_co_default, types.TracebackType]",
- (exc_type, exc_val, exc_tb),
- )
- self.excinfo.fill_unfilled(exc_info)
- return True
- @final
- class RaisesGroup(AbstractRaises[BaseExceptionGroup[BaseExcT_co]]):
- """
- .. versionadded:: 8.4
- Contextmanager for checking for an expected :exc:`ExceptionGroup`.
- This works similar to :func:`pytest.raises`, but allows for specifying the structure of an :exc:`ExceptionGroup`.
- :meth:`ExceptionInfo.group_contains` also tries to handle exception groups,
- but it is very bad at checking that you *didn't* get unexpected exceptions.
- The catching behaviour differs from :ref:`except* <except_star>`, being much
- stricter about the structure by default.
- By using ``allow_unwrapped=True`` and ``flatten_subgroups=True`` you can match
- :ref:`except* <except_star>` fully when expecting a single exception.
- :param args:
- Any number of exception types, :class:`RaisesGroup` or :class:`RaisesExc`
- to specify the exceptions contained in this exception.
- All specified exceptions must be present in the raised group, *and no others*.
- If you expect a variable number of exceptions you need to use
- :func:`pytest.raises(ExceptionGroup) <pytest.raises>` and manually check
- the contained exceptions. Consider making use of :meth:`RaisesExc.matches`.
- It does not care about the order of the exceptions, so
- ``RaisesGroup(ValueError, TypeError)``
- is equivalent to
- ``RaisesGroup(TypeError, ValueError)``.
- :kwparam str | re.Pattern[str] | None match:
- If specified, a string containing a regular expression,
- or a regular expression object, that is tested against the string
- representation of the exception group and its :pep:`678` `__notes__`
- using :func:`re.search`.
- To match a literal string that may contain :ref:`special characters
- <re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
- Note that " (5 subgroups)" will be stripped from the ``repr`` before matching.
- :kwparam Callable[[E], bool] check:
- If specified, a callable that will be called with the group as a parameter
- after successfully matching the expected exceptions. If it returns ``True``
- it will be considered a match, if not it will be considered a failed match.
- :kwparam bool allow_unwrapped:
- If expecting a single exception or :class:`RaisesExc` it will match even
- if the exception is not inside an exceptiongroup.
- Using this together with ``match``, ``check`` or expecting multiple exceptions
- will raise an error.
- :kwparam bool flatten_subgroups:
- "flatten" any groups inside the raised exception group, extracting all exceptions
- inside any nested groups, before matching. Without this it expects you to
- fully specify the nesting structure by passing :class:`RaisesGroup` as expected
- parameter.
- Examples::
- with RaisesGroup(ValueError):
- raise ExceptionGroup("", (ValueError(),))
- # match
- with RaisesGroup(
- ValueError,
- ValueError,
- RaisesExc(TypeError, match="^expected int$"),
- match="^my group$",
- ):
- raise ExceptionGroup(
- "my group",
- [
- ValueError(),
- TypeError("expected int"),
- ValueError(),
- ],
- )
- # check
- with RaisesGroup(
- KeyboardInterrupt,
- match="^hello$",
- check=lambda x: isinstance(x.__cause__, ValueError),
- ):
- raise BaseExceptionGroup("hello", [KeyboardInterrupt()]) from ValueError
- # nested groups
- with RaisesGroup(RaisesGroup(ValueError)):
- raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
- # flatten_subgroups
- with RaisesGroup(ValueError, flatten_subgroups=True):
- raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
- # allow_unwrapped
- with RaisesGroup(ValueError, allow_unwrapped=True):
- raise ValueError
- :meth:`RaisesGroup.matches` can also be used directly to check a standalone exception group.
- The matching algorithm is greedy, which means cases such as this may fail::
- with RaisesGroup(ValueError, RaisesExc(ValueError, match="hello")):
- raise ExceptionGroup("", (ValueError("hello"), ValueError("goodbye")))
- even though it generally does not care about the order of the exceptions in the group.
- To avoid the above you should specify the first :exc:`ValueError` with a :class:`RaisesExc` as well.
- .. note::
- When raised exceptions don't match the expected ones, you'll get a detailed error
- message explaining why. This includes ``repr(check)`` if set, which in Python can be
- overly verbose, showing memory locations etc etc.
- If installed and imported (in e.g. ``conftest.py``), the ``hypothesis`` library will
- monkeypatch this output to provide shorter & more readable repr's.
- """
- # allow_unwrapped=True requires: singular exception, exception not being
- # RaisesGroup instance, match is None, check is None
- @overload
- def __init__(
- self,
- expected_exception: type[BaseExcT_co] | RaisesExc[BaseExcT_co],
- /,
- *,
- allow_unwrapped: Literal[True],
- flatten_subgroups: bool = False,
- ) -> None: ...
- # flatten_subgroups = True also requires no nested RaisesGroup
- @overload
- def __init__(
- self,
- expected_exception: type[BaseExcT_co] | RaisesExc[BaseExcT_co],
- /,
- *other_exceptions: type[BaseExcT_co] | RaisesExc[BaseExcT_co],
- flatten_subgroups: Literal[True],
- match: str | Pattern[str] | None = None,
- check: Callable[[BaseExceptionGroup[BaseExcT_co]], bool] | None = None,
- ) -> None: ...
- # simplify the typevars if possible (the following 3 are equivalent but go simpler->complicated)
- # ... the first handles RaisesGroup[ValueError], the second RaisesGroup[ExceptionGroup[ValueError]],
- # the third RaisesGroup[ValueError | ExceptionGroup[ValueError]].
- # ... otherwise, we will get results like RaisesGroup[ValueError | ExceptionGroup[Never]] (I think)
- # (technically correct but misleading)
- @overload
- def __init__(
- self: RaisesGroup[ExcT_1],
- expected_exception: type[ExcT_1] | RaisesExc[ExcT_1],
- /,
- *other_exceptions: type[ExcT_1] | RaisesExc[ExcT_1],
- match: str | Pattern[str] | None = None,
- check: Callable[[ExceptionGroup[ExcT_1]], bool] | None = None,
- ) -> None: ...
- @overload
- def __init__(
- self: RaisesGroup[ExceptionGroup[ExcT_2]],
- expected_exception: RaisesGroup[ExcT_2],
- /,
- *other_exceptions: RaisesGroup[ExcT_2],
- match: str | Pattern[str] | None = None,
- check: Callable[[ExceptionGroup[ExceptionGroup[ExcT_2]]], bool] | None = None,
- ) -> None: ...
- @overload
- def __init__(
- self: RaisesGroup[ExcT_1 | ExceptionGroup[ExcT_2]],
- expected_exception: type[ExcT_1] | RaisesExc[ExcT_1] | RaisesGroup[ExcT_2],
- /,
- *other_exceptions: type[ExcT_1] | RaisesExc[ExcT_1] | RaisesGroup[ExcT_2],
- match: str | Pattern[str] | None = None,
- check: (
- Callable[[ExceptionGroup[ExcT_1 | ExceptionGroup[ExcT_2]]], bool] | None
- ) = None,
- ) -> None: ...
- # same as the above 3 but handling BaseException
- @overload
- def __init__(
- self: RaisesGroup[BaseExcT_1],
- expected_exception: type[BaseExcT_1] | RaisesExc[BaseExcT_1],
- /,
- *other_exceptions: type[BaseExcT_1] | RaisesExc[BaseExcT_1],
- match: str | Pattern[str] | None = None,
- check: Callable[[BaseExceptionGroup[BaseExcT_1]], bool] | None = None,
- ) -> None: ...
- @overload
- def __init__(
- self: RaisesGroup[BaseExceptionGroup[BaseExcT_2]],
- expected_exception: RaisesGroup[BaseExcT_2],
- /,
- *other_exceptions: RaisesGroup[BaseExcT_2],
- match: str | Pattern[str] | None = None,
- check: (
- Callable[[BaseExceptionGroup[BaseExceptionGroup[BaseExcT_2]]], bool] | None
- ) = None,
- ) -> None: ...
- @overload
- def __init__(
- self: RaisesGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]],
- expected_exception: type[BaseExcT_1]
- | RaisesExc[BaseExcT_1]
- | RaisesGroup[BaseExcT_2],
- /,
- *other_exceptions: type[BaseExcT_1]
- | RaisesExc[BaseExcT_1]
- | RaisesGroup[BaseExcT_2],
- match: str | Pattern[str] | None = None,
- check: (
- Callable[
- [BaseExceptionGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]]],
- bool,
- ]
- | None
- ) = None,
- ) -> None: ...
- def __init__(
- self: RaisesGroup[ExcT_1 | BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]],
- expected_exception: type[BaseExcT_1]
- | RaisesExc[BaseExcT_1]
- | RaisesGroup[BaseExcT_2],
- /,
- *other_exceptions: type[BaseExcT_1]
- | RaisesExc[BaseExcT_1]
- | RaisesGroup[BaseExcT_2],
- allow_unwrapped: bool = False,
- flatten_subgroups: bool = False,
- match: str | Pattern[str] | None = None,
- check: (
- Callable[[BaseExceptionGroup[BaseExcT_1]], bool]
- | Callable[[ExceptionGroup[ExcT_1]], bool]
- | None
- ) = None,
- ):
- # The type hint on the `self` and `check` parameters uses different formats
- # that are *very* hard to reconcile while adhering to the overloads, so we cast
- # it to avoid an error when passing it to super().__init__
- check = cast(
- "Callable[[BaseExceptionGroup[ExcT_1|BaseExcT_1|BaseExceptionGroup[BaseExcT_2]]], bool]",
- check,
- )
- super().__init__(match=match, check=check)
- self.allow_unwrapped = allow_unwrapped
- self.flatten_subgroups: bool = flatten_subgroups
- self.is_baseexception = False
- if allow_unwrapped and other_exceptions:
- raise ValueError(
- "You cannot specify multiple exceptions with `allow_unwrapped=True.`"
- " If you want to match one of multiple possible exceptions you should"
- " use a `RaisesExc`."
- " E.g. `RaisesExc(check=lambda e: isinstance(e, (...)))`",
- )
- if allow_unwrapped and isinstance(expected_exception, RaisesGroup):
- raise ValueError(
- "`allow_unwrapped=True` has no effect when expecting a `RaisesGroup`."
- " You might want it in the expected `RaisesGroup`, or"
- " `flatten_subgroups=True` if you don't care about the structure.",
- )
- if allow_unwrapped and (match is not None or check is not None):
- raise ValueError(
- "`allow_unwrapped=True` bypasses the `match` and `check` parameters"
- " if the exception is unwrapped. If you intended to match/check the"
- " exception you should use a `RaisesExc` object. If you want to match/check"
- " the exceptiongroup when the exception *is* wrapped you need to"
- " do e.g. `if isinstance(exc.value, ExceptionGroup):"
- " assert RaisesGroup(...).matches(exc.value)` afterwards.",
- )
- self.expected_exceptions: tuple[
- type[BaseExcT_co] | RaisesExc[BaseExcT_co] | RaisesGroup[BaseException], ...
- ] = tuple(
- self._parse_excgroup(e, "a BaseException type, RaisesExc, or RaisesGroup")
- for e in (
- expected_exception,
- *other_exceptions,
- )
- )
- def _parse_excgroup(
- self,
- exc: (
- type[BaseExcT_co]
- | types.GenericAlias
- | RaisesExc[BaseExcT_1]
- | RaisesGroup[BaseExcT_2]
- ),
- expected: str,
- ) -> type[BaseExcT_co] | RaisesExc[BaseExcT_1] | RaisesGroup[BaseExcT_2]:
- # verify exception type and set `self.is_baseexception`
- if isinstance(exc, RaisesGroup):
- if self.flatten_subgroups:
- raise ValueError(
- "You cannot specify a nested structure inside a RaisesGroup with"
- " `flatten_subgroups=True`. The parameter will flatten subgroups"
- " in the raised exceptiongroup before matching, which would never"
- " match a nested structure.",
- )
- self.is_baseexception |= exc.is_baseexception
- exc._nested = True
- return exc
- elif isinstance(exc, RaisesExc):
- self.is_baseexception |= exc.is_baseexception
- exc._nested = True
- return exc
- elif isinstance(exc, tuple):
- raise TypeError(
- f"Expected {expected}, but got {type(exc).__name__!r}.\n"
- "RaisesGroup does not support tuples of exception types when expecting one of "
- "several possible exception types like RaisesExc.\n"
- "If you meant to expect a group with multiple exceptions, list them as separate arguments."
- )
- else:
- return super()._parse_exc(exc, expected)
- @overload
- def __enter__(
- self: RaisesGroup[ExcT_1],
- ) -> ExceptionInfo[ExceptionGroup[ExcT_1]]: ...
- @overload
- def __enter__(
- self: RaisesGroup[BaseExcT_1],
- ) -> ExceptionInfo[BaseExceptionGroup[BaseExcT_1]]: ...
- def __enter__(self) -> ExceptionInfo[BaseExceptionGroup[BaseException]]:
- self.excinfo: ExceptionInfo[BaseExceptionGroup[BaseExcT_co]] = (
- ExceptionInfo.for_later()
- )
- return self.excinfo
- def __repr__(self) -> str:
- reqs = [
- e.__name__ if isinstance(e, type) else repr(e)
- for e in self.expected_exceptions
- ]
- if self.allow_unwrapped:
- reqs.append(f"allow_unwrapped={self.allow_unwrapped}")
- if self.flatten_subgroups:
- reqs.append(f"flatten_subgroups={self.flatten_subgroups}")
- if self.match is not None:
- # If no flags were specified, discard the redundant re.compile() here.
- reqs.append(f"match={_match_pattern(self.match)!r}")
- if self.check is not None:
- reqs.append(f"check={repr_callable(self.check)}")
- return f"RaisesGroup({', '.join(reqs)})"
- def _unroll_exceptions(
- self,
- exceptions: Sequence[BaseException],
- ) -> Sequence[BaseException]:
- """Used if `flatten_subgroups=True`."""
- res: list[BaseException] = []
- for exc in exceptions:
- if isinstance(exc, BaseExceptionGroup):
- res.extend(self._unroll_exceptions(exc.exceptions))
- else:
- res.append(exc)
- return res
- @overload
- def matches(
- self: RaisesGroup[ExcT_1],
- exception: BaseException | None,
- ) -> TypeGuard[ExceptionGroup[ExcT_1]]: ...
- @overload
- def matches(
- self: RaisesGroup[BaseExcT_1],
- exception: BaseException | None,
- ) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ...
- def matches(
- self,
- exception: BaseException | None,
- ) -> bool:
- """Check if an exception matches the requirements of this RaisesGroup.
- If it fails, `RaisesGroup.fail_reason` will be set.
- Example::
- with pytest.raises(TypeError) as excinfo:
- ...
- assert RaisesGroup(ValueError).matches(excinfo.value.__cause__)
- # the above line is equivalent to
- myexc = excinfo.value.__cause
- assert isinstance(myexc, BaseExceptionGroup)
- assert len(myexc.exceptions) == 1
- assert isinstance(myexc.exceptions[0], ValueError)
- """
- self._fail_reason = None
- if exception is None:
- self._fail_reason = "exception is None"
- return False
- if not isinstance(exception, BaseExceptionGroup):
- # we opt to only print type of the exception here, as the repr would
- # likely be quite long
- not_group_msg = f"`{type(exception).__name__}()` is not an exception group"
- if len(self.expected_exceptions) > 1:
- self._fail_reason = not_group_msg
- return False
- # if we have 1 expected exception, check if it would work even if
- # allow_unwrapped is not set
- res = self._check_expected(self.expected_exceptions[0], exception)
- if res is None and self.allow_unwrapped:
- return True
- if res is None:
- self._fail_reason = (
- f"{not_group_msg}, but would match with `allow_unwrapped=True`"
- )
- elif self.allow_unwrapped:
- self._fail_reason = res
- else:
- self._fail_reason = not_group_msg
- return False
- actual_exceptions: Sequence[BaseException] = exception.exceptions
- if self.flatten_subgroups:
- actual_exceptions = self._unroll_exceptions(actual_exceptions)
- if not self._check_match(exception):
- self._fail_reason = cast(str, self._fail_reason)
- old_reason = self._fail_reason
- if (
- len(actual_exceptions) == len(self.expected_exceptions) == 1
- and isinstance(expected := self.expected_exceptions[0], type)
- and isinstance(actual := actual_exceptions[0], expected)
- and self._check_match(actual)
- ):
- assert self.match is not None, "can't be None if _check_match failed"
- assert self._fail_reason is old_reason is not None
- self._fail_reason += (
- f"\n"
- f" but matched the expected `{self._repr_expected(expected)}`.\n"
- f" You might want "
- f"`RaisesGroup(RaisesExc({expected.__name__}, match={_match_pattern(self.match)!r}))`"
- )
- else:
- self._fail_reason = old_reason
- return False
- # do the full check on expected exceptions
- if not self._check_exceptions(
- exception,
- actual_exceptions,
- ):
- self._fail_reason = cast(str, self._fail_reason)
- assert self._fail_reason is not None
- old_reason = self._fail_reason
- # if we're not expecting a nested structure, and there is one, do a second
- # pass where we try flattening it
- if (
- not self.flatten_subgroups
- and not any(
- isinstance(e, RaisesGroup) for e in self.expected_exceptions
- )
- and any(isinstance(e, BaseExceptionGroup) for e in actual_exceptions)
- and self._check_exceptions(
- exception,
- self._unroll_exceptions(exception.exceptions),
- )
- ):
- # only indent if it's a single-line reason. In a multi-line there's already
- # indented lines that this does not belong to.
- indent = " " if "\n" not in self._fail_reason else ""
- self._fail_reason = (
- old_reason
- + f"\n{indent}Did you mean to use `flatten_subgroups=True`?"
- )
- else:
- self._fail_reason = old_reason
- return False
- # Only run `self.check` once we know `exception` is of the correct type.
- if not self._check_check(exception):
- reason = (
- cast(str, self._fail_reason) + f" on the {type(exception).__name__}"
- )
- if (
- len(actual_exceptions) == len(self.expected_exceptions) == 1
- and isinstance(expected := self.expected_exceptions[0], type)
- # we explicitly break typing here :)
- and self._check_check(actual_exceptions[0]) # type: ignore[arg-type]
- ):
- self._fail_reason = reason + (
- f", but did return True for the expected {self._repr_expected(expected)}."
- f" You might want RaisesGroup(RaisesExc({expected.__name__}, check=<...>))"
- )
- else:
- self._fail_reason = reason
- return False
- return True
- @staticmethod
- def _check_expected(
- expected_type: (
- type[BaseException] | RaisesExc[BaseException] | RaisesGroup[BaseException]
- ),
- exception: BaseException,
- ) -> str | None:
- """Helper method for `RaisesGroup.matches` and `RaisesGroup._check_exceptions`
- to check one of potentially several expected exceptions."""
- if isinstance(expected_type, type):
- return _check_raw_type(expected_type, exception)
- res = expected_type.matches(exception)
- if res:
- return None
- assert expected_type.fail_reason is not None
- if expected_type.fail_reason.startswith("\n"):
- return f"\n{expected_type!r}: {indent(expected_type.fail_reason, ' ')}"
- return f"{expected_type!r}: {expected_type.fail_reason}"
- @staticmethod
- def _repr_expected(e: type[BaseException] | AbstractRaises[BaseException]) -> str:
- """Get the repr of an expected type/RaisesExc/RaisesGroup, but we only want
- the name if it's a type"""
- if isinstance(e, type):
- return _exception_type_name(e)
- return repr(e)
- @overload
- def _check_exceptions(
- self: RaisesGroup[ExcT_1],
- _exception: Exception,
- actual_exceptions: Sequence[Exception],
- ) -> TypeGuard[ExceptionGroup[ExcT_1]]: ...
- @overload
- def _check_exceptions(
- self: RaisesGroup[BaseExcT_1],
- _exception: BaseException,
- actual_exceptions: Sequence[BaseException],
- ) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ...
- def _check_exceptions(
- self,
- _exception: BaseException,
- actual_exceptions: Sequence[BaseException],
- ) -> bool:
- """Helper method for RaisesGroup.matches that attempts to pair up expected and actual exceptions"""
- # The _exception parameter is not used, but necessary for the TypeGuard
- # full table with all results
- results = ResultHolder(self.expected_exceptions, actual_exceptions)
- # (indexes of) raised exceptions that haven't (yet) found an expected
- remaining_actual = list(range(len(actual_exceptions)))
- # (indexes of) expected exceptions that haven't found a matching raised
- failed_expected: list[int] = []
- # successful greedy matches
- matches: dict[int, int] = {}
- # loop over expected exceptions first to get a more predictable result
- for i_exp, expected in enumerate(self.expected_exceptions):
- for i_rem in remaining_actual:
- res = self._check_expected(expected, actual_exceptions[i_rem])
- results.set_result(i_exp, i_rem, res)
- if res is None:
- remaining_actual.remove(i_rem)
- matches[i_exp] = i_rem
- break
- else:
- failed_expected.append(i_exp)
- # All exceptions matched up successfully
- if not remaining_actual and not failed_expected:
- return True
- # in case of a single expected and single raised we simplify the output
- if 1 == len(actual_exceptions) == len(self.expected_exceptions):
- assert not matches
- self._fail_reason = res
- return False
- # The test case is failing, so we can do a slow and exhaustive check to find
- # duplicate matches etc that will be helpful in debugging
- for i_exp, expected in enumerate(self.expected_exceptions):
- for i_actual, actual in enumerate(actual_exceptions):
- if results.has_result(i_exp, i_actual):
- continue
- results.set_result(
- i_exp, i_actual, self._check_expected(expected, actual)
- )
- successful_str = (
- f"{len(matches)} matched exception{'s' if len(matches) > 1 else ''}. "
- if matches
- else ""
- )
- # all expected were found
- if not failed_expected and results.no_match_for_actual(remaining_actual):
- self._fail_reason = (
- f"{successful_str}Unexpected exception(s):"
- f" {[actual_exceptions[i] for i in remaining_actual]!r}"
- )
- return False
- # all raised exceptions were expected
- if not remaining_actual and results.no_match_for_expected(failed_expected):
- no_match_for_str = ", ".join(
- self._repr_expected(self.expected_exceptions[i])
- for i in failed_expected
- )
- self._fail_reason = f"{successful_str}Too few exceptions raised, found no match for: [{no_match_for_str}]"
- return False
- # if there's only one remaining and one failed, and the unmatched didn't match anything else,
- # we elect to only print why the remaining and the failed didn't match.
- if (
- 1 == len(remaining_actual) == len(failed_expected)
- and results.no_match_for_actual(remaining_actual)
- and results.no_match_for_expected(failed_expected)
- ):
- self._fail_reason = f"{successful_str}{results.get_result(failed_expected[0], remaining_actual[0])}"
- return False
- # there's both expected and raised exceptions without matches
- s = ""
- if matches:
- s += f"\n{successful_str}"
- indent_1 = " " * 2
- indent_2 = " " * 4
- if not remaining_actual:
- s += "\nToo few exceptions raised!"
- elif not failed_expected:
- s += "\nUnexpected exception(s)!"
- if failed_expected:
- s += "\nThe following expected exceptions did not find a match:"
- rev_matches = {v: k for k, v in matches.items()}
- for i_failed in failed_expected:
- s += (
- f"\n{indent_1}{self._repr_expected(self.expected_exceptions[i_failed])}"
- )
- for i_actual, actual in enumerate(actual_exceptions):
- if results.get_result(i_exp, i_actual) is None:
- # we print full repr of match target
- s += (
- f"\n{indent_2}It matches {backquote(repr(actual))} which was paired with "
- + backquote(
- self._repr_expected(
- self.expected_exceptions[rev_matches[i_actual]]
- )
- )
- )
- if remaining_actual:
- s += "\nThe following raised exceptions did not find a match"
- for i_actual in remaining_actual:
- s += f"\n{indent_1}{actual_exceptions[i_actual]!r}:"
- for i_exp, expected in enumerate(self.expected_exceptions):
- res = results.get_result(i_exp, i_actual)
- if i_exp in failed_expected:
- assert res is not None
- if res[0] != "\n":
- s += "\n"
- s += indent(res, indent_2)
- if res is None:
- # we print full repr of match target
- s += (
- f"\n{indent_2}It matches {backquote(self._repr_expected(expected))} "
- f"which was paired with {backquote(repr(actual_exceptions[matches[i_exp]]))}"
- )
- if len(self.expected_exceptions) == len(actual_exceptions) and possible_match(
- results
- ):
- s += (
- "\nThere exist a possible match when attempting an exhaustive check,"
- " but RaisesGroup uses a greedy algorithm. "
- "Please make your expected exceptions more stringent with `RaisesExc` etc"
- " so the greedy algorithm can function."
- )
- self._fail_reason = s
- return False
- def __exit__(
- self,
- exc_type: type[BaseException] | None,
- exc_val: BaseException | None,
- exc_tb: types.TracebackType | None,
- ) -> bool:
- __tracebackhide__ = True
- if exc_type is None:
- fail(f"DID NOT RAISE any exception, expected `{self.expected_type()}`")
- assert self.excinfo is not None, (
- "Internal error - should have been constructed in __enter__"
- )
- # group_str is the only thing that differs between RaisesExc and RaisesGroup...
- # I might just scrap it? Or make it part of fail_reason
- group_str = (
- "(group)"
- if self.allow_unwrapped and not issubclass(exc_type, BaseExceptionGroup)
- else "group"
- )
- if not self.matches(exc_val):
- fail(f"Raised exception {group_str} did not match: {self._fail_reason}")
- # Cast to narrow the exception type now that it's verified....
- # even though the TypeGuard in self.matches should be narrowing
- exc_info = cast(
- "tuple[type[BaseExceptionGroup[BaseExcT_co]], BaseExceptionGroup[BaseExcT_co], types.TracebackType]",
- (exc_type, exc_val, exc_tb),
- )
- self.excinfo.fill_unfilled(exc_info)
- return True
- def expected_type(self) -> str:
- subexcs = []
- for e in self.expected_exceptions:
- if isinstance(e, RaisesExc):
- subexcs.append(repr(e))
- elif isinstance(e, RaisesGroup):
- subexcs.append(e.expected_type())
- elif isinstance(e, type):
- subexcs.append(e.__name__)
- else: # pragma: no cover
- raise AssertionError("unknown type")
- group_type = "Base" if self.is_baseexception else ""
- return f"{group_type}ExceptionGroup({', '.join(subexcs)})"
- @final
- class NotChecked:
- """Singleton for unchecked values in ResultHolder"""
- class ResultHolder:
- """Container for results of checking exceptions.
- Used in RaisesGroup._check_exceptions and possible_match.
- """
- def __init__(
- self,
- expected_exceptions: tuple[
- type[BaseException] | AbstractRaises[BaseException], ...
- ],
- actual_exceptions: Sequence[BaseException],
- ) -> None:
- self.results: list[list[str | type[NotChecked] | None]] = [
- [NotChecked for _ in expected_exceptions] for _ in actual_exceptions
- ]
- def set_result(self, expected: int, actual: int, result: str | None) -> None:
- self.results[actual][expected] = result
- def get_result(self, expected: int, actual: int) -> str | None:
- res = self.results[actual][expected]
- assert res is not NotChecked
- # mypy doesn't support identity checking against anything but None
- return res # type: ignore[return-value]
- def has_result(self, expected: int, actual: int) -> bool:
- return self.results[actual][expected] is not NotChecked
- def no_match_for_expected(self, expected: list[int]) -> bool:
- for i in expected:
- for actual_results in self.results:
- assert actual_results[i] is not NotChecked
- if actual_results[i] is None:
- return False
- return True
- def no_match_for_actual(self, actual: list[int]) -> bool:
- for i in actual:
- for res in self.results[i]:
- assert res is not NotChecked
- if res is None:
- return False
- return True
- def possible_match(results: ResultHolder, used: set[int] | None = None) -> bool:
- if used is None:
- used = set()
- curr_row = len(used)
- if curr_row == len(results.results):
- return True
- return any(
- val is None and i not in used and possible_match(results, used | {i})
- for (i, val) in enumerate(results.results[curr_row])
- )
|