pytest/testing/typing_raises_group.py

244 lines
8.1 KiB
Python

from __future__ import annotations
import sys
from typing import Callable
from typing import Union
from typing_extensions import assert_type
from _pytest.main import Failed as main_Failed
from _pytest.outcomes import Failed
from pytest import raises
from pytest import RaisesExc
from pytest import RaisesGroup
# does not work
assert_type(raises.Exception, Failed) # type: ignore[assert-type, attr-defined]
# FIXME: these are different for some reason(?)
assert Failed is not main_Failed # type: ignore[comparison-overlap]
if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup
from exceptiongroup import ExceptionGroup
# split into functions to isolate the different scopes
def check_raisesexc_typevar_default(e: RaisesExc) -> None:
assert e.expected_exceptions is not None
_exc: type[BaseException] | tuple[type[BaseException], ...] = e.expected_exceptions
# this would previously pass, as the type would be `Any`
e.exception_type().blah() # type: ignore
def check_basic_contextmanager() -> None:
with RaisesGroup(ValueError) as e:
raise ExceptionGroup("foo", (ValueError(),))
assert_type(e.value, ExceptionGroup[ValueError])
def check_basic_matches() -> None:
# check that matches gets rid of the naked ValueError in the union
exc: ExceptionGroup[ValueError] | ValueError = ExceptionGroup("", (ValueError(),))
if RaisesGroup(ValueError).matches(exc):
assert_type(exc, ExceptionGroup[ValueError])
# also check that BaseExceptionGroup shows up for BaseExceptions
if RaisesGroup(KeyboardInterrupt).matches(exc):
assert_type(exc, BaseExceptionGroup[KeyboardInterrupt])
def check_matches_with_different_exception_type() -> None:
e: BaseExceptionGroup[KeyboardInterrupt] = BaseExceptionGroup(
"",
(KeyboardInterrupt(),),
)
# note: it might be tempting to have this warn.
# however, that isn't possible with current typing
if RaisesGroup(ValueError).matches(e):
assert_type(e, ExceptionGroup[ValueError])
def check_raisesexc_init() -> None:
def check_exc(exc: BaseException) -> bool:
return isinstance(exc, ValueError)
# Check various combinations of constructor signatures.
# At least 1 arg must be provided.
RaisesExc() # type: ignore
RaisesExc(ValueError)
RaisesExc(ValueError, "regex")
RaisesExc(ValueError, "regex", check_exc)
RaisesExc(expected_exception=ValueError)
RaisesExc(match="regex")
RaisesExc(check=check_exc)
RaisesExc(ValueError, match="regex")
RaisesExc(match="regex", check=check_exc)
def check_filenotfound(exc: FileNotFoundError) -> bool:
return not exc.filename.endswith(".tmp")
# If exception_type is provided, that narrows the `check` method's argument.
RaisesExc(FileNotFoundError, check=check_filenotfound)
RaisesExc(ValueError, check=check_filenotfound) # type: ignore
RaisesExc(check=check_filenotfound) # type: ignore
RaisesExc(FileNotFoundError, match="regex", check=check_filenotfound)
def raisesgroup_check_type_narrowing() -> None:
"""Check type narrowing on the `check` argument to `RaisesGroup`.
All `type: ignore`s are correctly pointing out type errors.
"""
def handle_exc(e: BaseExceptionGroup[BaseException]) -> bool:
return True
def handle_kbi(e: BaseExceptionGroup[KeyboardInterrupt]) -> bool:
return True
def handle_value(e: BaseExceptionGroup[ValueError]) -> bool:
return True
RaisesGroup(BaseException, check=handle_exc)
RaisesGroup(BaseException, check=handle_kbi) # type: ignore
RaisesGroup(Exception, check=handle_exc)
RaisesGroup(Exception, check=handle_value) # type: ignore
RaisesGroup(KeyboardInterrupt, check=handle_exc)
RaisesGroup(KeyboardInterrupt, check=handle_kbi)
RaisesGroup(KeyboardInterrupt, check=handle_value) # type: ignore
RaisesGroup(ValueError, check=handle_exc)
RaisesGroup(ValueError, check=handle_kbi) # type: ignore
RaisesGroup(ValueError, check=handle_value)
RaisesGroup(ValueError, KeyboardInterrupt, check=handle_exc)
RaisesGroup(ValueError, KeyboardInterrupt, check=handle_kbi) # type: ignore
RaisesGroup(ValueError, KeyboardInterrupt, check=handle_value) # type: ignore
def raisesgroup_narrow_baseexceptiongroup() -> None:
"""Check type narrowing specifically for the container exceptiongroup."""
def handle_group(e: ExceptionGroup[Exception]) -> bool:
return True
def handle_group_value(e: ExceptionGroup[ValueError]) -> bool:
return True
RaisesGroup(ValueError, check=handle_group_value)
RaisesGroup(Exception, check=handle_group)
def check_raisesexc_transparent() -> None:
with RaisesGroup(RaisesExc(ValueError)) as e:
...
_: BaseExceptionGroup[ValueError] = e.value
assert_type(e.value, ExceptionGroup[ValueError])
def check_nested_raisesgroups_contextmanager() -> None:
with RaisesGroup(RaisesGroup(ValueError)) as excinfo:
raise ExceptionGroup("foo", (ValueError(),))
_: BaseExceptionGroup[BaseExceptionGroup[ValueError]] = excinfo.value
assert_type(
excinfo.value,
ExceptionGroup[ExceptionGroup[ValueError]],
)
assert_type(
excinfo.value.exceptions[0],
# this union is because of how typeshed defines .exceptions
Union[
ExceptionGroup[ValueError],
ExceptionGroup[ExceptionGroup[ValueError]],
],
)
def check_nested_raisesgroups_matches() -> None:
"""Check nested RaisesGroup with .matches"""
exc: ExceptionGroup[ExceptionGroup[ValueError]] = ExceptionGroup(
"",
(ExceptionGroup("", (ValueError(),)),),
)
if RaisesGroup(RaisesGroup(ValueError)).matches(exc):
assert_type(exc, ExceptionGroup[ExceptionGroup[ValueError]])
def check_multiple_exceptions_1() -> None:
a = RaisesGroup(ValueError, ValueError)
b = RaisesGroup(RaisesExc(ValueError), RaisesExc(ValueError))
c = RaisesGroup(ValueError, RaisesExc(ValueError))
d: RaisesGroup[ValueError]
d = a
d = b
d = c
assert d
def check_multiple_exceptions_2() -> None:
# This previously failed due to lack of covariance in the TypeVar
a = RaisesGroup(RaisesExc(ValueError), RaisesExc(TypeError))
b = RaisesGroup(RaisesExc(ValueError), TypeError)
c = RaisesGroup(ValueError, TypeError)
d: RaisesGroup[Exception]
d = a
d = b
d = c
assert d
def check_raisesgroup_overloads() -> None:
# allow_unwrapped=True does not allow:
# multiple exceptions
RaisesGroup(ValueError, TypeError, allow_unwrapped=True) # type: ignore
# nested RaisesGroup
RaisesGroup(RaisesGroup(ValueError), allow_unwrapped=True) # type: ignore
# specifying match
RaisesGroup(ValueError, match="foo", allow_unwrapped=True) # type: ignore
# specifying check
RaisesGroup(ValueError, check=bool, allow_unwrapped=True) # type: ignore
# allowed variants
RaisesGroup(ValueError, allow_unwrapped=True)
RaisesGroup(ValueError, allow_unwrapped=True, flatten_subgroups=True)
RaisesGroup(RaisesExc(ValueError), allow_unwrapped=True)
# flatten_subgroups=True does not allow nested RaisesGroup
RaisesGroup(RaisesGroup(ValueError), flatten_subgroups=True) # type: ignore
# but rest is plenty fine
RaisesGroup(ValueError, TypeError, flatten_subgroups=True)
RaisesGroup(ValueError, match="foo", flatten_subgroups=True)
RaisesGroup(ValueError, check=bool, flatten_subgroups=True)
RaisesGroup(ValueError, flatten_subgroups=True)
RaisesGroup(RaisesExc(ValueError), flatten_subgroups=True)
# if they're both false we can of course specify nested raisesgroup
RaisesGroup(RaisesGroup(ValueError))
def check_triple_nested_raisesgroup() -> None:
with RaisesGroup(RaisesGroup(RaisesGroup(ValueError))) as e:
assert_type(e.value, ExceptionGroup[ExceptionGroup[ExceptionGroup[ValueError]]])
def check_check_typing() -> None:
# `BaseExceptiongroup` should perhaps be `ExceptionGroup`, but close enough
assert_type(
RaisesGroup(ValueError).check,
Union[
Callable[[BaseExceptionGroup[ValueError]], bool],
None,
],
)