From 9a48173a6a4f0c6e046e54da902622ef3b987acd Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 1 Dec 2023 17:45:50 +0100
Subject: [PATCH 1/4] draft implementation of ExpectedExceptionGroup
---
src/_pytest/python_api.py | 235 +++++++++++++++++-
testing/python/expected_exception_group.py | 262 +++++++++++++++++++++
testing/python/raises.py | 15 +-
3 files changed, 496 insertions(+), 16 deletions(-)
create mode 100644 testing/python/expected_exception_group.py
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 07db0f234d4..4646540507f 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -1,5 +1,6 @@
import math
import pprint
+import re
from collections.abc import Collection
from collections.abc import Sized
from decimal import Decimal
@@ -10,6 +11,8 @@
from typing import cast
from typing import ContextManager
from typing import final
+from typing import Generic
+from typing import Iterable
from typing import List
from typing import Mapping
from typing import Optional
@@ -22,6 +25,10 @@
from typing import TypeVar
from typing import Union
+if TYPE_CHECKING:
+ from typing_extensions import TypeAlias, TypeGuard
+
+
import _pytest._code
from _pytest.compat import STRING_TYPES
from _pytest.outcomes import fail
@@ -780,6 +787,149 @@ def _as_numpy_array(obj: object) -> Optional["ndarray"]:
# builtin pytest.raises helper
E = TypeVar("E", bound=BaseException)
+E2 = TypeVar("E2", bound=BaseException)
+
+
+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
+
+
+# TODO: rename if kept, EEE[E] looks like gibberish
+EEE: "TypeAlias" = Union[Matcher[E], Type[E], "ExpectedExceptionGroup[E]"]
+
+if TYPE_CHECKING:
+ SuperClass = BaseExceptionGroup
+else:
+ SuperClass = Generic
+
+
+# it's unclear if
+# `ExpectedExceptionGroup(ValueError, strict=False).matches(ValueError())`
+# should return True. It matches the behaviour of expect*, but is maybe better handled
+# by the end user doing pytest.raises((ValueError, ExpectedExceptionGroup(ValueError)))
+@final
+class ExpectedExceptionGroup(SuperClass[E]):
+ # TODO: overload to disallow nested exceptiongroup with strict=False
+ # @overload
+ # def __init__(self, exceptions: Union[Matcher[E], Type[E]], *args: Union[Matcher[E],
+ # Type[E]], strict: Literal[False]): ...
+
+ # @overload
+ # def __init__(self, exceptions: EEE[E], *args: EEE[E], strict: bool = True): ...
+
+ def __init__(
+ self,
+ exceptions: Union[Type[E], E, Matcher[E]],
+ *args: Union[Type[E], E, Matcher[E]],
+ strict: bool = True,
+ ):
+ # could add parameter `notes: Optional[Tuple[str, Pattern[str]]] = None`
+ self.expected_exceptions = (exceptions, *args)
+ self.strict = strict
+
+ for exc in self.expected_exceptions:
+ if not isinstance(exc, (Matcher, ExpectedExceptionGroup)) and not (
+ isinstance(exc, type) and issubclass(exc, BaseException)
+ ):
+ raise ValueError(
+ "Invalid argument {exc} must be exception type, Matcher, or ExpectedExceptionGroup."
+ )
+ if isinstance(exc, ExpectedExceptionGroup) and not strict:
+ raise ValueError(
+ "You cannot specify a nested structure inside an ExpectedExceptionGroup with strict=False"
+ )
+
+ def _unroll_exceptions(
+ self, exceptions: Iterable[BaseException]
+ ) -> Iterable[BaseException]:
+ res: list[BaseException] = []
+ for exc in exceptions:
+ if isinstance(exc, BaseExceptionGroup):
+ res.extend(self._unroll_exceptions(exc.exceptions))
+
+ else:
+ res.append(exc)
+ return res
+
+ def matches(
+ self,
+ exc_val: Optional[BaseException],
+ ) -> "TypeGuard[BaseExceptionGroup[E]]":
+ if exc_val is None:
+ return False
+ if not isinstance(exc_val, BaseExceptionGroup):
+ return False
+ if not len(exc_val.exceptions) == len(self.expected_exceptions):
+ return False
+ remaining_exceptions = list(self.expected_exceptions)
+ actual_exceptions: Iterable[BaseException] = exc_val.exceptions
+ if not self.strict:
+ actual_exceptions = self._unroll_exceptions(actual_exceptions)
+
+ # it should be possible to get ExpectedExceptionGroup.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, ExpectedExceptionGroup)
+ 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
+
+ # def __str__(self) -> str:
+ # return f"ExceptionGroup{self.expected_exceptions}"
+ # str(tuple(...)) seems to call repr
+ def __repr__(self) -> str:
+ # TODO: [Base]ExceptionGroup
+ return f"ExceptionGroup{self.expected_exceptions}"
+
+
+@overload
+def raises(
+ expected_exception: Union[
+ ExpectedExceptionGroup[E], Tuple[ExpectedExceptionGroup[E], ...]
+ ],
+ *,
+ match: Optional[Union[str, Pattern[str]]] = ...,
+) -> "RaisesContext[ExpectedExceptionGroup[E]]":
+ ...
@overload
@@ -791,6 +941,17 @@ def raises(
...
+#
+#
+# @overload
+# def raises(
+# expected_exception: Tuple[Union[Type[E], ExpectedExceptionGroup[E2]], ...],
+# *,
+# match: Optional[Union[str, Pattern[str]]] = ...,
+# ) -> "RaisesContext[Union[E, BaseExceptionGroup[E2]]]":
+# ...
+
+
@overload
def raises( # noqa: F811
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
@@ -801,9 +962,20 @@ def raises( # noqa: F811
...
-def raises( # noqa: F811
- expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
-) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
+def raises(
+ expected_exception: Union[
+ Type[E],
+ ExpectedExceptionGroup[E2],
+ Tuple[Union[Type[E], ExpectedExceptionGroup[E2]], ...],
+ ],
+ *args: Any,
+ **kwargs: Any,
+) -> Union[
+ "RaisesContext[E]",
+ "RaisesContext[BaseExceptionGroup[E2]]",
+ "RaisesContext[Union[E, BaseExceptionGroup[E2]]]",
+ _pytest._code.ExceptionInfo[E],
+]:
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
@@ -952,13 +1124,20 @@ def raises( # noqa: F811
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'."
)
- if isinstance(expected_exception, type):
- expected_exceptions: Tuple[Type[E], ...] = (expected_exception,)
+ if isinstance(expected_exception, (type, ExpectedExceptionGroup)):
+ expected_exception_tuple: Tuple[
+ Union[Type[E], ExpectedExceptionGroup[E2]], ...
+ ] = (expected_exception,)
else:
- expected_exceptions = expected_exception
- for exc in expected_exceptions:
- if not isinstance(exc, type) or not issubclass(exc, BaseException):
- msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
+ expected_exception_tuple = expected_exception
+ for exc in expected_exception_tuple:
+ if (
+ not isinstance(exc, type) or not issubclass(exc, BaseException)
+ ) and not isinstance(exc, ExpectedExceptionGroup):
+ msg = ( # type: ignore[unreachable]
+ "expected exception must be a BaseException "
+ "type or ExpectedExceptionGroup instance, not {}"
+ )
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
raise TypeError(msg.format(not_a))
@@ -971,14 +1150,23 @@ def raises( # noqa: F811
msg += ", ".join(sorted(kwargs))
msg += "\nUse context-manager form instead?"
raise TypeError(msg)
- return RaisesContext(expected_exception, message, match)
+ # the ExpectedExceptionGroup -> BaseExceptionGroup swap necessitates an ignore
+ return RaisesContext(expected_exception, message, match) # type: ignore[misc]
else:
func = args[0]
+
+ for exc in expected_exception_tuple:
+ if isinstance(exc, ExpectedExceptionGroup):
+ raise TypeError(
+ "Only contextmanager form is supported for ExpectedExceptionGroup"
+ )
+
if not callable(func):
raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
try:
func(*args[1:], **kwargs)
- except expected_exception as e:
+ except expected_exception as e: # type: ignore[misc] # TypeError raised for any ExpectedExceptionGroup
+
return _pytest._code.ExceptionInfo.from_exception(e)
fail(message)
@@ -987,11 +1175,14 @@ def raises( # noqa: F811
raises.Exception = fail.Exception # type: ignore
+EE: "TypeAlias" = Union[Type[E], "ExpectedExceptionGroup[E]"]
+
+
@final
class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
def __init__(
self,
- expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+ expected_exception: Union[EE[E], Tuple[EE[E], ...]],
message: str,
match_expr: Optional[Union[str, Pattern[str]]] = None,
) -> None:
@@ -1014,8 +1205,26 @@ def __exit__(
if exc_type is None:
fail(self.message)
assert self.excinfo is not None
- if not issubclass(exc_type, self.expected_exception):
+
+ if isinstance(self.expected_exception, ExpectedExceptionGroup):
+ if not self.expected_exception.matches(exc_val):
+ return False
+ elif isinstance(self.expected_exception, tuple):
+ for expected_exc in self.expected_exception:
+ if (
+ isinstance(expected_exc, ExpectedExceptionGroup)
+ and expected_exc.matches(exc_val)
+ ) or (
+ isinstance(expected_exc, type)
+ and issubclass(exc_type, expected_exc)
+ ):
+ break
+ else: # pragma: no cover
+ # this would've been caught on initialization of pytest.raises()
+ return False
+ elif not issubclass(exc_type, self.expected_exception):
return False
+
# Cast to narrow the exception type now that it's verified.
exc_info = cast(Tuple[Type[E], E, TracebackType], (exc_type, exc_val, exc_tb))
self.excinfo.fill_unfilled(exc_info)
diff --git a/testing/python/expected_exception_group.py b/testing/python/expected_exception_group.py
new file mode 100644
index 00000000000..a3d9641235f
--- /dev/null
+++ b/testing/python/expected_exception_group.py
@@ -0,0 +1,262 @@
+from typing import TYPE_CHECKING
+
+import pytest
+from _pytest.python_api import ExpectedExceptionGroup
+from _pytest.python_api import Matcher
+
+# TODO: make a public export
+
+if TYPE_CHECKING:
+ from typing_extensions import assert_type
+
+
+class TestExpectedExceptionGroup:
+ def test_expected_exception_group(self) -> None:
+ with pytest.raises(
+ ValueError,
+ match="^Invalid argument {exc} must be exception type, Matcher, or ExpectedExceptionGroup.$",
+ ):
+ ExpectedExceptionGroup(ValueError())
+
+ with pytest.raises(ExpectedExceptionGroup(ValueError)):
+ raise ExceptionGroup("foo", (ValueError(),))
+
+ with pytest.raises(ExpectedExceptionGroup(SyntaxError)):
+ with pytest.raises(ExpectedExceptionGroup(ValueError)):
+ raise ExceptionGroup("foo", (SyntaxError(),))
+
+ # multiple exceptions
+ with pytest.raises(ExpectedExceptionGroup(ValueError, SyntaxError)):
+ raise ExceptionGroup("foo", (ValueError(), SyntaxError()))
+
+ # order doesn't matter
+ with pytest.raises(ExpectedExceptionGroup(SyntaxError, ValueError)):
+ raise ExceptionGroup("foo", (ValueError(), SyntaxError()))
+
+ # nested exceptions
+ with pytest.raises(ExpectedExceptionGroup(ExpectedExceptionGroup(ValueError))):
+ raise ExceptionGroup("foo", (ExceptionGroup("bar", (ValueError(),)),))
+
+ with pytest.raises(
+ ExpectedExceptionGroup(
+ SyntaxError,
+ ExpectedExceptionGroup(ValueError),
+ ExpectedExceptionGroup(RuntimeError),
+ )
+ ):
+ raise ExceptionGroup(
+ "foo",
+ (
+ SyntaxError(),
+ ExceptionGroup("bar", (ValueError(),)),
+ ExceptionGroup("", (RuntimeError(),)),
+ ),
+ )
+
+ # will error if there's excess exceptions
+ with pytest.raises(ExceptionGroup):
+ with pytest.raises(ExpectedExceptionGroup(ValueError)):
+ raise ExceptionGroup("", (ValueError(), ValueError()))
+
+ with pytest.raises(ExceptionGroup):
+ with pytest.raises(ExpectedExceptionGroup(ValueError)):
+ raise ExceptionGroup("", (RuntimeError(), ValueError()))
+
+ # will error if there's missing exceptions
+ with pytest.raises(ExceptionGroup):
+ with pytest.raises(ExpectedExceptionGroup(ValueError, ValueError)):
+ raise ExceptionGroup("", (ValueError(),))
+
+ with pytest.raises(ExceptionGroup):
+ with pytest.raises(ExpectedExceptionGroup(ValueError, SyntaxError)):
+ raise ExceptionGroup("", (ValueError(),))
+
+ # loose semantics, as with expect*
+ with pytest.raises(ExpectedExceptionGroup(ValueError, strict=False)):
+ raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
+
+ # mixed loose is possible if you want it to be at least N deep
+ with pytest.raises(
+ ExpectedExceptionGroup(ExpectedExceptionGroup(ValueError, strict=True))
+ ):
+ raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
+ with pytest.raises(
+ ExpectedExceptionGroup(ExpectedExceptionGroup(ValueError, strict=False))
+ ):
+ raise ExceptionGroup(
+ "", (ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)),)
+ )
+
+ # but not the other way around
+ with pytest.raises(
+ ValueError,
+ match="^You cannot specify a nested structure inside an ExpectedExceptionGroup with strict=False$",
+ ):
+ ExpectedExceptionGroup(ExpectedExceptionGroup(ValueError), strict=False)
+
+ # currently not fully identical in behaviour to expect*, which would also catch an unwrapped exception
+ with pytest.raises(ValueError):
+ with pytest.raises(ExpectedExceptionGroup(ValueError, strict=False)):
+ raise ValueError
+
+ def test_match(self) -> None:
+ # supports match string
+ with pytest.raises(ExpectedExceptionGroup(ValueError), match="bar"):
+ raise ExceptionGroup("bar", (ValueError(),))
+
+ try:
+ with pytest.raises(ExpectedExceptionGroup(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_ExpectedExceptionGroup_matches(self) -> None:
+ eeg = ExpectedExceptionGroup(ValueError)
+ assert not eeg.matches(None)
+ assert not eeg.matches(ValueError())
+ assert eeg.matches(ExceptionGroup("", (ValueError(),)))
+
+ def test_message(self) -> None:
+
+ try:
+ with pytest.raises(ExpectedExceptionGroup(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 pytest.raises(
+ ExpectedExceptionGroup(ExpectedExceptionGroup(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 pytest.raises(ExpectedExceptionGroup(Matcher(ValueError))):
+ raise ExceptionGroup("", (ValueError(),))
+ try:
+ with pytest.raises(ExpectedExceptionGroup(Matcher(TypeError))):
+ raise ExceptionGroup("", (ValueError(),))
+ except ExceptionGroup:
+ pass
+ else:
+ assert False, "Expected pytest.raises.Exception"
+
+ def test_matcher_match(self) -> None:
+ with pytest.raises(ExpectedExceptionGroup(Matcher(ValueError, "foo"))):
+ raise ExceptionGroup("", (ValueError("foo"),))
+ try:
+ with pytest.raises(ExpectedExceptionGroup(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 pytest.raises(ExpectedExceptionGroup(Matcher(match="foo"))):
+ raise ExceptionGroup("", (ValueError("foo"),))
+ try:
+ with pytest.raises(ExpectedExceptionGroup(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 pytest.raises(
+ ExpectedExceptionGroup(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 pytest.raises(
+ ExpectedExceptionGroup(Matcher(OSError, check=check_errno_is_5))
+ ):
+ raise ExceptionGroup("", (OSError(5, ""),))
+
+ try:
+ with pytest.raises(
+ ExpectedExceptionGroup(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 ExpectedExceptionGroup 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] = ExpectedExceptionGroup(ValueError)
+ _ = ExpectedExceptionGroup(ExpectedExceptionGroup(ValueError)) # type: ignore[arg-type]
+ a: BaseExceptionGroup[BaseExceptionGroup[ValueError]]
+ a = ExpectedExceptionGroup(ExpectedExceptionGroup(ValueError))
+ a = BaseExceptionGroup("", (BaseExceptionGroup("", (ValueError(),)),))
+ assert a
+
+ def test_types_1(self) -> None:
+ with pytest.raises(ExpectedExceptionGroup(ValueError)) as e:
+ raise ExceptionGroup("foo", (ValueError(),))
+ assert_type(e.value, ExpectedExceptionGroup[ValueError])
+
+ def test_types_2(self) -> None:
+ exc: ExceptionGroup[ValueError] | ValueError = ExceptionGroup(
+ "", (ValueError(),)
+ )
+ if ExpectedExceptionGroup(ValueError).matches(exc):
+ assert_type(exc, BaseExceptionGroup[ValueError])
+
+ def test_types_3(self) -> None:
+ e: BaseExceptionGroup[KeyboardInterrupt] = BaseExceptionGroup(
+ "", (KeyboardInterrupt(),)
+ )
+ if ExpectedExceptionGroup(ValueError).matches(e):
+ assert_type(e, BaseExceptionGroup[ValueError])
+
+ def test_types_4(self) -> None:
+ with pytest.raises(ExpectedExceptionGroup(Matcher(ValueError))) as e:
+ ...
+ _: BaseExceptionGroup[ValueError] = e.value
+ assert_type(e.value, ExpectedExceptionGroup[ValueError])
+
+ def test_types_5(self) -> None:
+ with pytest.raises(
+ ExpectedExceptionGroup(ExpectedExceptionGroup(ValueError))
+ ) as excinfo:
+ raise ExceptionGroup("foo", (ValueError(),))
+ _: BaseExceptionGroup[BaseExceptionGroup[ValueError]] = excinfo.value
+ assert_type(
+ excinfo.value,
+ ExpectedExceptionGroup[ExpectedExceptionGroup[ValueError]],
+ )
+ print(excinfo.value.exceptions[0].exceptions[0])
+
+ def test_types_6(self) -> None:
+ exc: ExceptionGroup[ExceptionGroup[ValueError]] = ... # type: ignore[assignment]
+ if ExpectedExceptionGroup(ExpectedExceptionGroup(ValueError)).matches(exc): # type: ignore[arg-type]
+ # ugly
+ assert_type(exc, BaseExceptionGroup[ExpectedExceptionGroup[ValueError]])
diff --git a/testing/python/raises.py b/testing/python/raises.py
index 3dcec31eb1f..9e5ce69c47a 100644
--- a/testing/python/raises.py
+++ b/testing/python/raises.py
@@ -287,7 +287,10 @@ def test_expected_exception_is_not_a_baseexception(self) -> None:
with pytest.raises(TypeError) as excinfo:
with pytest.raises("hello"): # type: ignore[call-overload]
pass # pragma: no cover
- assert "must be a BaseException type, not str" in str(excinfo.value)
+ assert (
+ "must be a BaseException type or ExpectedExceptionGroup instance, not str"
+ in str(excinfo.value)
+ )
class NotAnException:
pass
@@ -295,9 +298,15 @@ class NotAnException:
with pytest.raises(TypeError) as excinfo:
with pytest.raises(NotAnException): # type: ignore[type-var]
pass # pragma: no cover
- assert "must be a BaseException type, not NotAnException" in str(excinfo.value)
+ assert (
+ "must be a BaseException type or ExpectedExceptionGroup instance, not NotAnException"
+ in str(excinfo.value)
+ )
with pytest.raises(TypeError) as excinfo:
with pytest.raises(("hello", NotAnException)): # type: ignore[arg-type]
pass # pragma: no cover
- assert "must be a BaseException type, not str" in str(excinfo.value)
+ assert (
+ "must be a BaseException type or ExpectedExceptionGroup instance, not str"
+ in str(excinfo.value)
+ )
From 91084644a0531d1aabd9556318cc0ea56bcf0f3d Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Fri, 1 Dec 2023 17:07:38 +0000
Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
src/_pytest/python_api.py | 1 -
testing/python/expected_exception_group.py | 1 -
2 files changed, 2 deletions(-)
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 4646540507f..72973e2cf5d 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -1166,7 +1166,6 @@ def raises(
try:
func(*args[1:], **kwargs)
except expected_exception as e: # type: ignore[misc] # TypeError raised for any ExpectedExceptionGroup
-
return _pytest._code.ExceptionInfo.from_exception(e)
fail(message)
diff --git a/testing/python/expected_exception_group.py b/testing/python/expected_exception_group.py
index a3d9641235f..5d74b73619e 100644
--- a/testing/python/expected_exception_group.py
+++ b/testing/python/expected_exception_group.py
@@ -119,7 +119,6 @@ def test_ExpectedExceptionGroup_matches(self) -> None:
assert eeg.matches(ExceptionGroup("", (ValueError(),)))
def test_message(self) -> None:
-
try:
with pytest.raises(ExpectedExceptionGroup(ValueError)):
...
From e577541b4bdcab19f68418c8a98c1a6d2e33999f Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 1 Dec 2023 18:19:15 +0100
Subject: [PATCH 3/4] import exceptiongroup on <3.11
---
src/_pytest/python_api.py | 9 +++++----
testing/python/expected_exception_group.py | 4 ++++
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 72973e2cf5d..82fde59dee1 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -1,6 +1,7 @@
import math
import pprint
import re
+import sys
from collections.abc import Collection
from collections.abc import Sized
from decimal import Decimal
@@ -25,16 +26,16 @@
from typing import TypeVar
from typing import Union
-if TYPE_CHECKING:
- from typing_extensions import TypeAlias, TypeGuard
-
-
import _pytest._code
from _pytest.compat import STRING_TYPES
from _pytest.outcomes import fail
if TYPE_CHECKING:
from numpy import ndarray
+ from typing_extensions import TypeAlias, TypeGuard
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
diff --git a/testing/python/expected_exception_group.py b/testing/python/expected_exception_group.py
index 5d74b73619e..c33e9d048c4 100644
--- a/testing/python/expected_exception_group.py
+++ b/testing/python/expected_exception_group.py
@@ -1,3 +1,4 @@
+import sys
from typing import TYPE_CHECKING
import pytest
@@ -9,6 +10,9 @@
if TYPE_CHECKING:
from typing_extensions import assert_type
+if sys.version_info < (3, 11):
+ from exceptiongroup import ExceptionGroup
+
class TestExpectedExceptionGroup:
def test_expected_exception_group(self) -> None:
From b2f659780ab753516e7b502a57eb4f8cb080321b Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 1 Dec 2023 18:28:12 +0100
Subject: [PATCH 4/4] type->Type
---
src/_pytest/python_api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 82fde59dee1..608b0c0b2d0 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -794,7 +794,7 @@ def _as_numpy_array(obj: object) -> Optional["ndarray"]:
class Matcher(Generic[E]):
def __init__(
self,
- exception_type: Optional[type[E]] = None,
+ exception_type: Optional[Type[E]] = None,
match: Optional[Union[str, Pattern[str]]] = None,
check: Optional[Callable[[E], bool]] = None,
):