pare down implementation to mimimum viable, add assert_matches that has assertions with descriptive outputs for why a match failed
This commit is contained in:
parent
2023fa7de8
commit
43f62eb6b5
|
@ -1,6 +1,5 @@
|
||||||
import math
|
import math
|
||||||
import pprint
|
import pprint
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Collection
|
from collections.abc import Collection
|
||||||
from collections.abc import Sized
|
from collections.abc import Sized
|
||||||
|
@ -12,7 +11,6 @@ from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import ContextManager
|
from typing import ContextManager
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import Generic
|
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
@ -995,66 +993,39 @@ def raises( # noqa: F811
|
||||||
raises.Exception = fail.Exception # type: ignore
|
raises.Exception = fail.Exception # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Matcher(Generic[E]):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
exception_type: Optional[Type[E]] = None,
|
|
||||||
match: Optional[Union[str, Pattern[str]]] = None,
|
|
||||||
check: Optional[Callable[[E], bool]] = None,
|
|
||||||
):
|
|
||||||
if exception_type is None and match is None and check is None:
|
|
||||||
raise ValueError("You must specify at least one parameter to match on.")
|
|
||||||
self.exception_type = exception_type
|
|
||||||
self.match = match
|
|
||||||
self.check = check
|
|
||||||
|
|
||||||
def matches(self, exception: E) -> "TypeGuard[E]":
|
|
||||||
if self.exception_type is not None and not isinstance(
|
|
||||||
exception, self.exception_type
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
if self.match is not None and not re.search(self.match, str(exception)):
|
|
||||||
return False
|
|
||||||
if self.check is not None and not self.check(exception):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
SuperClass = BaseExceptionGroup
|
|
||||||
else:
|
|
||||||
SuperClass = Generic
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class RaisesGroup(
|
class RaisesGroup(ContextManager[_pytest._code.ExceptionInfo[BaseExceptionGroup[E]]]):
|
||||||
ContextManager[_pytest._code.ExceptionInfo[BaseExceptionGroup[E]]], SuperClass[E]
|
"""Helper for catching exceptions wrapped in an ExceptionGroup.
|
||||||
):
|
|
||||||
# My_T = TypeVar("My_T", bound=Union[Type[E], Matcher[E], "RaisesGroup[E]"])
|
Similar to pytest.raises, except:
|
||||||
|
* It requires that the exception is inside an exceptiongroup
|
||||||
|
* It is only able to be used as a contextmanager
|
||||||
|
* Due to the above, is not split into a caller function and a cm class
|
||||||
|
Similar to trio.RaisesGroup, except:
|
||||||
|
* does not handle multiple levels of nested groups.
|
||||||
|
* does not have trio.Matcher, to add matching on the sub-exception
|
||||||
|
* does not handle multiple exceptions in the exceptiongroup.
|
||||||
|
|
||||||
|
TODO: copy over docstring example usage from trio.RaisesGroup
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
exceptions: Union[Type[E], Matcher[E], E],
|
exception: Type[E],
|
||||||
*args: Union[Type[E], Matcher[E], E],
|
check: Optional[Callable[[BaseExceptionGroup[E]], bool]] = None,
|
||||||
strict: bool = True,
|
|
||||||
match: Optional[Union[str, Pattern[str]]] = None,
|
|
||||||
):
|
):
|
||||||
# could add parameter `notes: Optional[Tuple[str, Pattern[str]]] = None`
|
# copied from raises() above
|
||||||
self.expected_exceptions = (exceptions, *args)
|
if not isinstance(exception, type) or not issubclass(exception, BaseException):
|
||||||
self.strict = strict
|
msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
|
||||||
self.match_expr = match
|
not_a = (
|
||||||
self.message = f"DID NOT RAISE ExceptionGroup{repr(self.expected_exceptions)}" # type: ignore[misc]
|
exception.__name__
|
||||||
|
if isinstance(exception, type)
|
||||||
|
else type(exception).__name__
|
||||||
|
)
|
||||||
|
raise TypeError(msg.format(not_a))
|
||||||
|
|
||||||
for exc in self.expected_exceptions:
|
self.exception = exception
|
||||||
if not isinstance(exc, (Matcher, RaisesGroup)) and not (
|
self.check = check
|
||||||
isinstance(exc, type) and issubclass(exc, BaseException)
|
|
||||||
):
|
|
||||||
raise ValueError(
|
|
||||||
"Invalid argument {exc} must be exception type, Matcher, or RaisesGroup."
|
|
||||||
)
|
|
||||||
if isinstance(exc, RaisesGroup) and not strict: # type: ignore[unreachable]
|
|
||||||
raise ValueError(
|
|
||||||
"You cannot specify a nested structure inside a RaisesGroup with strict=False"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __enter__(self) -> _pytest._code.ExceptionInfo[BaseExceptionGroup[E]]:
|
def __enter__(self) -> _pytest._code.ExceptionInfo[BaseExceptionGroup[E]]:
|
||||||
self.excinfo: _pytest._code.ExceptionInfo[
|
self.excinfo: _pytest._code.ExceptionInfo[
|
||||||
|
@ -1078,41 +1049,33 @@ class RaisesGroup(
|
||||||
self,
|
self,
|
||||||
exc_val: Optional[BaseException],
|
exc_val: Optional[BaseException],
|
||||||
) -> "TypeGuard[BaseExceptionGroup[E]]":
|
) -> "TypeGuard[BaseExceptionGroup[E]]":
|
||||||
if exc_val is None:
|
return (
|
||||||
return False
|
exc_val is not None
|
||||||
if not isinstance(exc_val, BaseExceptionGroup):
|
and isinstance(exc_val, BaseExceptionGroup)
|
||||||
return False
|
and len(exc_val.exceptions) == 1
|
||||||
if not len(exc_val.exceptions) == len(self.expected_exceptions):
|
and isinstance(exc_val.exceptions[0], self.exception)
|
||||||
return False
|
and (self.check is None or self.check(exc_val))
|
||||||
remaining_exceptions = list(self.expected_exceptions)
|
)
|
||||||
actual_exceptions: Iterable[BaseException] = exc_val.exceptions
|
|
||||||
if not self.strict:
|
def assert_matches(
|
||||||
actual_exceptions = self._unroll_exceptions(actual_exceptions)
|
self,
|
||||||
|
exc_val: Optional[BaseException],
|
||||||
|
) -> "TypeGuard[BaseExceptionGroup[E]]":
|
||||||
|
assert (
|
||||||
|
exc_val is not None
|
||||||
|
), "Internal Error: exc_type is not None but exc_val is"
|
||||||
|
assert isinstance(
|
||||||
|
exc_val, BaseExceptionGroup
|
||||||
|
), f"Expected an ExceptionGroup, not {type(exc_val)}"
|
||||||
|
assert (
|
||||||
|
len(exc_val.exceptions) == 1
|
||||||
|
), f"Wrong number of exceptions: got {len(exc_val.exceptions)}, expected 1."
|
||||||
|
assert isinstance(
|
||||||
|
exc_val.exceptions[0], self.exception
|
||||||
|
), f"Wrong type in group: got {type(exc_val.exceptions[0])}, expected {self.exception}"
|
||||||
|
if self.check is not None:
|
||||||
|
assert self.check(exc_val), f"Check failed on {repr(exc_val)}."
|
||||||
|
|
||||||
# it should be possible to get RaisesGroup.matches typed so as not to
|
|
||||||
# need these type: ignores, but I'm not sure that's possible while also having it
|
|
||||||
# transparent for the end user.
|
|
||||||
for e in actual_exceptions:
|
|
||||||
for rem_e in remaining_exceptions:
|
|
||||||
# TODO: how to print string diff on mismatch?
|
|
||||||
# Probably accumulate them, and then if fail, print them
|
|
||||||
# Further QoL would be to print how the exception structure differs on non-match
|
|
||||||
if (
|
|
||||||
(isinstance(rem_e, type) and isinstance(e, rem_e))
|
|
||||||
or (
|
|
||||||
isinstance(e, BaseExceptionGroup)
|
|
||||||
and isinstance(rem_e, RaisesGroup)
|
|
||||||
and rem_e.matches(e)
|
|
||||||
)
|
|
||||||
or (
|
|
||||||
isinstance(rem_e, Matcher)
|
|
||||||
and rem_e.matches(e) # type: ignore[arg-type]
|
|
||||||
)
|
|
||||||
):
|
|
||||||
remaining_exceptions.remove(rem_e) # type: ignore[arg-type]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __exit__(
|
def __exit__(
|
||||||
|
@ -1123,11 +1086,10 @@ class RaisesGroup(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
if exc_type is None:
|
if exc_type is None:
|
||||||
fail(self.message)
|
fail("DID NOT RAISE ANY EXCEPTION, expected " + self.expected_type())
|
||||||
assert self.excinfo is not None
|
assert self.excinfo is not None, "__exit__ without __enter__"
|
||||||
|
|
||||||
if not self.matches(exc_val):
|
self.assert_matches(exc_val)
|
||||||
return False
|
|
||||||
|
|
||||||
# Cast to narrow the exception type now that it's verified.
|
# Cast to narrow the exception type now that it's verified.
|
||||||
exc_info = cast(
|
exc_info = cast(
|
||||||
|
@ -1135,13 +1097,14 @@ class RaisesGroup(
|
||||||
(exc_type, exc_val, exc_tb),
|
(exc_type, exc_val, exc_tb),
|
||||||
)
|
)
|
||||||
self.excinfo.fill_unfilled(exc_info)
|
self.excinfo.fill_unfilled(exc_info)
|
||||||
if self.match_expr is not None:
|
|
||||||
self.excinfo.match(self.match_expr)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def expected_type(self) -> str:
|
||||||
# TODO: [Base]ExceptionGroup
|
if not issubclass(self.exception, Exception):
|
||||||
return f"ExceptionGroup{self.expected_exceptions}"
|
base = "Base"
|
||||||
|
else:
|
||||||
|
base = ""
|
||||||
|
return f"{base}ExceptionGroup({self.exception})"
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
|
|
@ -57,6 +57,7 @@ from _pytest.python import Module
|
||||||
from _pytest.python import Package
|
from _pytest.python import Package
|
||||||
from _pytest.python_api import approx
|
from _pytest.python_api import approx
|
||||||
from _pytest.python_api import raises
|
from _pytest.python_api import raises
|
||||||
|
from _pytest.python_api import RaisesGroup
|
||||||
from _pytest.recwarn import deprecated_call
|
from _pytest.recwarn import deprecated_call
|
||||||
from _pytest.recwarn import WarningsRecorder
|
from _pytest.recwarn import WarningsRecorder
|
||||||
from _pytest.recwarn import warns
|
from _pytest.recwarn import warns
|
||||||
|
@ -146,6 +147,7 @@ __all__ = [
|
||||||
"PytestUnraisableExceptionWarning",
|
"PytestUnraisableExceptionWarning",
|
||||||
"PytestWarning",
|
"PytestWarning",
|
||||||
"raises",
|
"raises",
|
||||||
|
"RaisesGroup",
|
||||||
"RecordedHookCall",
|
"RecordedHookCall",
|
||||||
"register_assert_rewrite",
|
"register_assert_rewrite",
|
||||||
"RunResult",
|
"RunResult",
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.python_api import Matcher
|
from _pytest.outcomes import Failed
|
||||||
from _pytest.python_api import RaisesGroup
|
from pytest import RaisesGroup
|
||||||
|
|
||||||
# TODO: make a public export
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import assert_type
|
from typing_extensions import assert_type
|
||||||
|
@ -14,237 +13,137 @@ if sys.version_info < (3, 11):
|
||||||
from exceptiongroup import ExceptionGroup
|
from exceptiongroup import ExceptionGroup
|
||||||
|
|
||||||
|
|
||||||
class TestRaisesGroup:
|
def test_raises_group() -> None:
|
||||||
def test_raises_group(self) -> None:
|
# wrong type to constructor
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ValueError,
|
TypeError,
|
||||||
match="^Invalid argument {exc} must be exception type, Matcher, or RaisesGroup.$",
|
match="^expected exception must be a BaseException type, not ValueError$",
|
||||||
):
|
):
|
||||||
RaisesGroup(ValueError())
|
RaisesGroup(ValueError()) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
# working example
|
||||||
|
with RaisesGroup(ValueError):
|
||||||
|
raise ExceptionGroup("foo", (ValueError(),))
|
||||||
|
|
||||||
|
with RaisesGroup(ValueError, check=lambda x: True):
|
||||||
|
raise ExceptionGroup("foo", (ValueError(),))
|
||||||
|
|
||||||
|
# wrong subexception
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError,
|
||||||
|
match="Wrong type in group: got <class 'SyntaxError'>, expected <class 'ValueError'>",
|
||||||
|
):
|
||||||
with RaisesGroup(ValueError):
|
with RaisesGroup(ValueError):
|
||||||
|
raise ExceptionGroup("foo", (SyntaxError(),))
|
||||||
|
|
||||||
|
# will error if there's excess exceptions
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError, match="Wrong number of exceptions: got 2, expected 1"
|
||||||
|
):
|
||||||
|
with RaisesGroup(ValueError):
|
||||||
|
raise ExceptionGroup("", (ValueError(), ValueError()))
|
||||||
|
|
||||||
|
# double nested exceptions is not (currently) supported (contrary to expect*)
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError,
|
||||||
|
match="Wrong type in group: got <class '(exceptiongroup.)?ExceptionGroup'>, expected <class 'ValueError'>",
|
||||||
|
):
|
||||||
|
with RaisesGroup(ValueError):
|
||||||
|
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
|
||||||
|
|
||||||
|
# you'd need to write
|
||||||
|
with RaisesGroup(ExceptionGroup) as excinfo:
|
||||||
|
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
|
||||||
|
RaisesGroup(ValueError).assert_matches(excinfo.value.exceptions[0])
|
||||||
|
|
||||||
|
# unwrapped exceptions are not accepted (contrary to expect*)
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError, match="Expected an ExceptionGroup, not <class 'ValueError'."
|
||||||
|
):
|
||||||
|
with RaisesGroup(ValueError):
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
AssertionError,
|
||||||
|
match=re.escape("Check failed on ExceptionGroup('foo', (ValueError(),))."),
|
||||||
|
):
|
||||||
|
with RaisesGroup(ValueError, check=lambda x: False):
|
||||||
raise ExceptionGroup("foo", (ValueError(),))
|
raise ExceptionGroup("foo", (ValueError(),))
|
||||||
|
|
||||||
with RaisesGroup(SyntaxError):
|
|
||||||
with RaisesGroup(ValueError):
|
|
||||||
raise ExceptionGroup("foo", (SyntaxError(),))
|
|
||||||
|
|
||||||
# multiple exceptions
|
def test_RaisesGroup_matches() -> None:
|
||||||
with RaisesGroup(ValueError, SyntaxError):
|
eeg = RaisesGroup(ValueError)
|
||||||
raise ExceptionGroup("foo", (ValueError(), SyntaxError()))
|
# exc_val is None
|
||||||
|
assert not eeg.matches(None)
|
||||||
|
# exc_val is not an exceptiongroup
|
||||||
|
assert not eeg.matches(ValueError())
|
||||||
|
# wrong length
|
||||||
|
assert not eeg.matches(ExceptionGroup("", (ValueError(), ValueError())))
|
||||||
|
# wrong type
|
||||||
|
assert not eeg.matches(ExceptionGroup("", (TypeError(),)))
|
||||||
|
# check fails
|
||||||
|
assert not RaisesGroup(ValueError, check=lambda _: False).matches(
|
||||||
|
ExceptionGroup("", (ValueError(),))
|
||||||
|
)
|
||||||
|
# success
|
||||||
|
assert eeg.matches(ExceptionGroup("", (ValueError(),)))
|
||||||
|
|
||||||
# order doesn't matter
|
|
||||||
with RaisesGroup(SyntaxError, ValueError):
|
|
||||||
raise ExceptionGroup("foo", (ValueError(), SyntaxError()))
|
|
||||||
|
|
||||||
# nested exceptions
|
def test_RaisesGroup_assert_matches() -> None:
|
||||||
with RaisesGroup(RaisesGroup(ValueError)):
|
"""Check direct use of RaisesGroup.assert_matches, without a context manager"""
|
||||||
raise ExceptionGroup("foo", (ExceptionGroup("bar", (ValueError(),)),))
|
eeg = RaisesGroup(ValueError)
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
eeg.assert_matches(None)
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
eeg.assert_matches(ValueError())
|
||||||
|
eeg.assert_matches(ExceptionGroup("", (ValueError(),)))
|
||||||
|
|
||||||
with RaisesGroup(
|
|
||||||
SyntaxError,
|
|
||||||
RaisesGroup(ValueError),
|
|
||||||
RaisesGroup(RuntimeError),
|
|
||||||
):
|
|
||||||
raise ExceptionGroup(
|
|
||||||
"foo",
|
|
||||||
(
|
|
||||||
SyntaxError(),
|
|
||||||
ExceptionGroup("bar", (ValueError(),)),
|
|
||||||
ExceptionGroup("", (RuntimeError(),)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# will error if there's excess exceptions
|
def test_message() -> None:
|
||||||
with pytest.raises(ExceptionGroup):
|
with pytest.raises(
|
||||||
with RaisesGroup(ValueError):
|
Failed,
|
||||||
raise ExceptionGroup("", (ValueError(), ValueError()))
|
match=re.escape(
|
||||||
|
f"DID NOT RAISE ANY EXCEPTION, expected ExceptionGroup({repr(ValueError)})"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
with RaisesGroup(ValueError):
|
||||||
|
...
|
||||||
|
|
||||||
with pytest.raises(ExceptionGroup):
|
with pytest.raises(
|
||||||
with RaisesGroup(ValueError):
|
Failed,
|
||||||
raise ExceptionGroup("", (RuntimeError(), ValueError()))
|
match=re.escape(
|
||||||
|
f"DID NOT RAISE ANY EXCEPTION, expected BaseExceptionGroup({repr(KeyboardInterrupt)})"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
with RaisesGroup(KeyboardInterrupt):
|
||||||
|
...
|
||||||
|
|
||||||
# will error if there's missing exceptions
|
|
||||||
with pytest.raises(ExceptionGroup):
|
|
||||||
with RaisesGroup(ValueError, ValueError):
|
|
||||||
raise ExceptionGroup("", (ValueError(),))
|
|
||||||
|
|
||||||
with pytest.raises(ExceptionGroup):
|
if TYPE_CHECKING:
|
||||||
with RaisesGroup(ValueError, SyntaxError):
|
|
||||||
raise ExceptionGroup("", (ValueError(),))
|
|
||||||
|
|
||||||
# loose semantics, as with expect*
|
def test_types_1() -> None:
|
||||||
with RaisesGroup(ValueError, strict=False):
|
with RaisesGroup(ValueError) as e:
|
||||||
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
|
raise ExceptionGroup("foo", (ValueError(),))
|
||||||
|
assert_type(e.value, BaseExceptionGroup[ValueError])
|
||||||
|
|
||||||
# mixed loose is possible if you want it to be at least N deep
|
def test_types_2() -> None:
|
||||||
with RaisesGroup(RaisesGroup(ValueError, strict=True)):
|
exc: ExceptionGroup[ValueError] | ValueError = ExceptionGroup(
|
||||||
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
|
"", (ValueError(),)
|
||||||
with RaisesGroup(RaisesGroup(ValueError, strict=False)):
|
)
|
||||||
raise ExceptionGroup(
|
if RaisesGroup(ValueError).assert_matches(exc):
|
||||||
"", (ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)),)
|
assert_type(exc, BaseExceptionGroup[ValueError])
|
||||||
)
|
|
||||||
|
|
||||||
# but not the other way around
|
def test_types_3() -> None:
|
||||||
with pytest.raises(
|
e: BaseExceptionGroup[KeyboardInterrupt] = BaseExceptionGroup(
|
||||||
ValueError,
|
"", (KeyboardInterrupt(),)
|
||||||
match="^You cannot specify a nested structure inside a RaisesGroup with strict=False$",
|
)
|
||||||
):
|
if RaisesGroup(ValueError).matches(e):
|
||||||
RaisesGroup(RaisesGroup(ValueError), strict=False)
|
assert_type(e, BaseExceptionGroup[ValueError])
|
||||||
|
|
||||||
# currently not fully identical in behaviour to expect*, which would also catch an unwrapped exception
|
def test_types_4() -> None:
|
||||||
with pytest.raises(ValueError):
|
e: BaseExceptionGroup[KeyboardInterrupt] = BaseExceptionGroup(
|
||||||
with RaisesGroup(ValueError, strict=False):
|
"", (KeyboardInterrupt(),)
|
||||||
raise ValueError
|
)
|
||||||
|
# not currently possible: https://github.com/python/typing/issues/930
|
||||||
def test_match(self) -> None:
|
RaisesGroup(ValueError).assert_matches(e)
|
||||||
# supports match string
|
assert_type(e, BaseExceptionGroup[ValueError]) # type: ignore[assert-type]
|
||||||
with RaisesGroup(ValueError, match="bar"):
|
|
||||||
raise ExceptionGroup("bar", (ValueError(),))
|
|
||||||
|
|
||||||
try:
|
|
||||||
with RaisesGroup(ValueError, match="foo"):
|
|
||||||
raise ExceptionGroup("bar", (ValueError(),))
|
|
||||||
except AssertionError as e:
|
|
||||||
assert str(e).startswith("Regex pattern did not match.")
|
|
||||||
else:
|
|
||||||
raise AssertionError("Expected pytest.raises.Exception")
|
|
||||||
|
|
||||||
def test_RaisesGroup_matches(self) -> None:
|
|
||||||
eeg = RaisesGroup(ValueError)
|
|
||||||
assert not eeg.matches(None)
|
|
||||||
assert not eeg.matches(ValueError())
|
|
||||||
assert eeg.matches(ExceptionGroup("", (ValueError(),)))
|
|
||||||
|
|
||||||
def test_message(self) -> None:
|
|
||||||
try:
|
|
||||||
with RaisesGroup(ValueError):
|
|
||||||
...
|
|
||||||
except pytest.fail.Exception as e:
|
|
||||||
assert e.msg == f"DID NOT RAISE ExceptionGroup({repr(ValueError)},)"
|
|
||||||
else:
|
|
||||||
assert False, "Expected pytest.raises.Exception"
|
|
||||||
try:
|
|
||||||
with RaisesGroup(RaisesGroup(ValueError)):
|
|
||||||
...
|
|
||||||
except pytest.fail.Exception as e:
|
|
||||||
assert (
|
|
||||||
e.msg
|
|
||||||
== f"DID NOT RAISE ExceptionGroup(ExceptionGroup({repr(ValueError)},),)"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
assert False, "Expected pytest.raises.Exception"
|
|
||||||
|
|
||||||
def test_matcher(self) -> None:
|
|
||||||
with pytest.raises(
|
|
||||||
ValueError, match="^You must specify at least one parameter to match on.$"
|
|
||||||
):
|
|
||||||
Matcher()
|
|
||||||
|
|
||||||
with RaisesGroup(Matcher(ValueError)):
|
|
||||||
raise ExceptionGroup("", (ValueError(),))
|
|
||||||
try:
|
|
||||||
with RaisesGroup(Matcher(TypeError)):
|
|
||||||
raise ExceptionGroup("", (ValueError(),))
|
|
||||||
except ExceptionGroup:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
assert False, "Expected pytest.raises.Exception"
|
|
||||||
|
|
||||||
def test_matcher_match(self) -> None:
|
|
||||||
with RaisesGroup(Matcher(ValueError, "foo")):
|
|
||||||
raise ExceptionGroup("", (ValueError("foo"),))
|
|
||||||
try:
|
|
||||||
with RaisesGroup(Matcher(ValueError, "foo")):
|
|
||||||
raise ExceptionGroup("", (ValueError("bar"),))
|
|
||||||
except ExceptionGroup:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
assert False, "Expected pytest.raises.Exception"
|
|
||||||
|
|
||||||
# Can be used without specifying the type
|
|
||||||
with RaisesGroup(Matcher(match="foo")):
|
|
||||||
raise ExceptionGroup("", (ValueError("foo"),))
|
|
||||||
try:
|
|
||||||
with RaisesGroup(Matcher(match="foo")):
|
|
||||||
raise ExceptionGroup("", (ValueError("bar"),))
|
|
||||||
except ExceptionGroup:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
assert False, "Expected pytest.raises.Exception"
|
|
||||||
|
|
||||||
def test_Matcher_check(self) -> None:
|
|
||||||
def check_oserror_and_errno_is_5(e: BaseException) -> bool:
|
|
||||||
return isinstance(e, OSError) and e.errno == 5
|
|
||||||
|
|
||||||
with RaisesGroup(Matcher(check=check_oserror_and_errno_is_5)):
|
|
||||||
raise ExceptionGroup("", (OSError(5, ""),))
|
|
||||||
|
|
||||||
# specifying exception_type narrows the parameter type to the callable
|
|
||||||
def check_errno_is_5(e: OSError) -> bool:
|
|
||||||
return e.errno == 5
|
|
||||||
|
|
||||||
with RaisesGroup(Matcher(OSError, check=check_errno_is_5)):
|
|
||||||
raise ExceptionGroup("", (OSError(5, ""),))
|
|
||||||
|
|
||||||
try:
|
|
||||||
with RaisesGroup(Matcher(OSError, check=check_errno_is_5)):
|
|
||||||
raise ExceptionGroup("", (OSError(6, ""),))
|
|
||||||
except ExceptionGroup:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
assert False, "Expected pytest.raises.Exception"
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
# getting the typing working satisfactory is very tricky
|
|
||||||
# but with RaisesGroup being seen as a subclass of BaseExceptionGroup
|
|
||||||
# most end-user cases of checking excinfo.value.foobar should work fine now.
|
|
||||||
def test_types_0(self) -> None:
|
|
||||||
_: BaseExceptionGroup[ValueError] = RaisesGroup(ValueError)
|
|
||||||
_ = RaisesGroup(RaisesGroup(ValueError)) # type: ignore[arg-type]
|
|
||||||
a: BaseExceptionGroup[BaseExceptionGroup[ValueError]]
|
|
||||||
a = RaisesGroup(RaisesGroup(ValueError))
|
|
||||||
a = BaseExceptionGroup("", (BaseExceptionGroup("", (ValueError(),)),))
|
|
||||||
assert a
|
|
||||||
|
|
||||||
def test_types_1(self) -> None:
|
|
||||||
with RaisesGroup(ValueError) as e:
|
|
||||||
raise ExceptionGroup("foo", (ValueError(),))
|
|
||||||
assert_type(e.value, BaseExceptionGroup[ValueError])
|
|
||||||
# assert_type(e.value, RaisesGroup[ValueError])
|
|
||||||
|
|
||||||
def test_types_2(self) -> None:
|
|
||||||
exc: ExceptionGroup[ValueError] | ValueError = ExceptionGroup(
|
|
||||||
"", (ValueError(),)
|
|
||||||
)
|
|
||||||
if RaisesGroup(ValueError).matches(exc):
|
|
||||||
assert_type(exc, BaseExceptionGroup[ValueError])
|
|
||||||
|
|
||||||
def test_types_3(self) -> None:
|
|
||||||
e: BaseExceptionGroup[KeyboardInterrupt] = BaseExceptionGroup(
|
|
||||||
"", (KeyboardInterrupt(),)
|
|
||||||
)
|
|
||||||
if RaisesGroup(ValueError).matches(e):
|
|
||||||
assert_type(e, BaseExceptionGroup[ValueError])
|
|
||||||
|
|
||||||
def test_types_4(self) -> None:
|
|
||||||
with RaisesGroup(Matcher(ValueError)) as e:
|
|
||||||
...
|
|
||||||
_: BaseExceptionGroup[ValueError] = e.value
|
|
||||||
assert_type(e.value, BaseExceptionGroup[ValueError])
|
|
||||||
|
|
||||||
def test_types_5(self) -> None:
|
|
||||||
with RaisesGroup(RaisesGroup(ValueError)) as excinfo:
|
|
||||||
raise ExceptionGroup("foo", (ValueError(),))
|
|
||||||
_: BaseExceptionGroup[BaseExceptionGroup[ValueError]] = excinfo.value
|
|
||||||
assert_type(
|
|
||||||
excinfo.value,
|
|
||||||
BaseExceptionGroup[RaisesGroup[ValueError]],
|
|
||||||
)
|
|
||||||
print(excinfo.value.exceptions[0].exceptions[0])
|
|
||||||
|
|
||||||
def test_types_6(self) -> None:
|
|
||||||
exc: ExceptionGroup[ExceptionGroup[ValueError]] = ... # type: ignore[assignment]
|
|
||||||
if RaisesGroup(RaisesGroup(ValueError)).matches(exc): # type: ignore[arg-type]
|
|
||||||
# ugly
|
|
||||||
assert_type(exc, BaseExceptionGroup[RaisesGroup[ValueError]])
|
|
||||||
|
|
Loading…
Reference in New Issue