updates
This commit is contained in:
@@ -1,31 +1,53 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import pprint
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Sized
|
||||
from decimal import Decimal
|
||||
import math
|
||||
from numbers import Complex
|
||||
import pprint
|
||||
import sys
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import ContextManager
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray
|
||||
|
||||
|
||||
import _pytest._code
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import STRING_TYPES
|
||||
from _pytest.compat import overload
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
|
||||
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
|
||||
at_str = f" at {at}" if at else ""
|
||||
return TypeError(
|
||||
"cannot make approximate comparisons to non-numeric values: {!r} {}".format(
|
||||
value, at_str
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _compare_approx(
|
||||
full_object: object,
|
||||
message_data: Sequence[tuple[str, str, str]],
|
||||
message_data: Sequence[Tuple[str, str, str]],
|
||||
number_of_elements: int,
|
||||
different_ids: Sequence[object],
|
||||
max_abs_diff: float,
|
||||
max_rel_diff: float,
|
||||
) -> list[str]:
|
||||
) -> List[str]:
|
||||
message_list = list(message_data)
|
||||
message_list.insert(0, ("Index", "Obtained", "Expected"))
|
||||
max_sizes = [0, 0, 0]
|
||||
@@ -66,7 +88,7 @@ class ApproxBase:
|
||||
def __repr__(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def _repr_compare(self, other_side: Any) -> list[str]:
|
||||
def _repr_compare(self, other_side: Any) -> List[str]:
|
||||
return [
|
||||
"comparison failed",
|
||||
f"Obtained: {other_side}",
|
||||
@@ -90,7 +112,7 @@ class ApproxBase:
|
||||
def __ne__(self, actual) -> bool:
|
||||
return not (actual == self)
|
||||
|
||||
def _approx_scalar(self, x) -> ApproxScalar:
|
||||
def _approx_scalar(self, x) -> "ApproxScalar":
|
||||
if isinstance(x, Decimal):
|
||||
return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
|
||||
return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
|
||||
@@ -113,11 +135,9 @@ class ApproxBase:
|
||||
|
||||
def _recursive_sequence_map(f, x):
|
||||
"""Recursively map a function over a sequence of arbitrary depth"""
|
||||
if isinstance(x, list | tuple):
|
||||
if isinstance(x, (list, tuple)):
|
||||
seq_type = type(x)
|
||||
return seq_type(_recursive_sequence_map(f, xi) for xi in x)
|
||||
elif _is_sequence_like(x):
|
||||
return [_recursive_sequence_map(f, xi) for xi in x]
|
||||
else:
|
||||
return f(x)
|
||||
|
||||
@@ -131,12 +151,12 @@ class ApproxNumpy(ApproxBase):
|
||||
)
|
||||
return f"approx({list_scalars!r})"
|
||||
|
||||
def _repr_compare(self, other_side: ndarray | list[Any]) -> list[str]:
|
||||
def _repr_compare(self, other_side: "ndarray") -> List[str]:
|
||||
import itertools
|
||||
import math
|
||||
|
||||
def get_value_from_nested_list(
|
||||
nested_list: list[Any], nd_index: tuple[Any, ...]
|
||||
nested_list: List[Any], nd_index: Tuple[Any, ...]
|
||||
) -> Any:
|
||||
"""
|
||||
Helper function to get the value out of a nested list, given an n-dimensional index.
|
||||
@@ -152,14 +172,10 @@ class ApproxNumpy(ApproxBase):
|
||||
self._approx_scalar, self.expected.tolist()
|
||||
)
|
||||
|
||||
# convert other_side to numpy array to ensure shape attribute is available
|
||||
other_side_as_array = _as_numpy_array(other_side)
|
||||
assert other_side_as_array is not None
|
||||
|
||||
if np_array_shape != other_side_as_array.shape:
|
||||
if np_array_shape != other_side.shape:
|
||||
return [
|
||||
"Impossible to compare arrays with different shapes.",
|
||||
f"Shapes: {np_array_shape} and {other_side_as_array.shape}",
|
||||
f"Shapes: {np_array_shape} and {other_side.shape}",
|
||||
]
|
||||
|
||||
number_of_elements = self.expected.size
|
||||
@@ -168,7 +184,7 @@ class ApproxNumpy(ApproxBase):
|
||||
different_ids = []
|
||||
for index in itertools.product(*(range(i) for i in np_array_shape)):
|
||||
approx_value = get_value_from_nested_list(approx_side_as_seq, index)
|
||||
other_value = get_value_from_nested_list(other_side_as_array, index)
|
||||
other_value = get_value_from_nested_list(other_side, index)
|
||||
if approx_value != other_value:
|
||||
abs_diff = abs(approx_value.expected - other_value)
|
||||
max_abs_diff = max(max_abs_diff, abs_diff)
|
||||
@@ -181,7 +197,7 @@ class ApproxNumpy(ApproxBase):
|
||||
message_data = [
|
||||
(
|
||||
str(index),
|
||||
str(get_value_from_nested_list(other_side_as_array, index)),
|
||||
str(get_value_from_nested_list(other_side, index)),
|
||||
str(get_value_from_nested_list(approx_side_as_seq, index)),
|
||||
)
|
||||
for index in different_ids
|
||||
@@ -231,23 +247,13 @@ class ApproxMapping(ApproxBase):
|
||||
with numeric values (the keys can be anything)."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"approx({ ({k: self._approx_scalar(v) for k, v in self.expected.items()})!r})"
|
||||
return "approx({!r})".format(
|
||||
{k: self._approx_scalar(v) for k, v in self.expected.items()}
|
||||
)
|
||||
|
||||
def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]:
|
||||
def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:
|
||||
import math
|
||||
|
||||
if len(self.expected) != len(other_side):
|
||||
return [
|
||||
"Impossible to compare mappings with different sizes.",
|
||||
f"Lengths: {len(self.expected)} and {len(other_side)}",
|
||||
]
|
||||
|
||||
if set(self.expected.keys()) != set(other_side.keys()):
|
||||
return [
|
||||
"comparison failed.",
|
||||
f"Mappings has different keys: expected {self.expected.keys()} but got {other_side.keys()}",
|
||||
]
|
||||
|
||||
approx_side_as_map = {
|
||||
k: self._approx_scalar(v) for k, v in self.expected.items()
|
||||
}
|
||||
@@ -257,26 +263,23 @@ class ApproxMapping(ApproxBase):
|
||||
max_rel_diff = -math.inf
|
||||
different_ids = []
|
||||
for (approx_key, approx_value), other_value in zip(
|
||||
approx_side_as_map.items(), other_side.values(), strict=True
|
||||
approx_side_as_map.items(), other_side.values()
|
||||
):
|
||||
if approx_value != other_value:
|
||||
if approx_value.expected is not None and other_value is not None:
|
||||
try:
|
||||
max_abs_diff = max(
|
||||
max_abs_diff, abs(approx_value.expected - other_value)
|
||||
max_abs_diff = max(
|
||||
max_abs_diff, abs(approx_value.expected - other_value)
|
||||
)
|
||||
if approx_value.expected == 0.0:
|
||||
max_rel_diff = math.inf
|
||||
else:
|
||||
max_rel_diff = max(
|
||||
max_rel_diff,
|
||||
abs(
|
||||
(approx_value.expected - other_value)
|
||||
/ approx_value.expected
|
||||
),
|
||||
)
|
||||
if approx_value.expected == 0.0:
|
||||
max_rel_diff = math.inf
|
||||
else:
|
||||
max_rel_diff = max(
|
||||
max_rel_diff,
|
||||
abs(
|
||||
(approx_value.expected - other_value)
|
||||
/ approx_value.expected
|
||||
),
|
||||
)
|
||||
except ZeroDivisionError:
|
||||
pass
|
||||
different_ids.append(approx_key)
|
||||
|
||||
message_data = [
|
||||
@@ -321,9 +324,11 @@ class ApproxSequenceLike(ApproxBase):
|
||||
seq_type = type(self.expected)
|
||||
if seq_type not in (tuple, list):
|
||||
seq_type = list
|
||||
return f"approx({seq_type(self._approx_scalar(x) for x in self.expected)!r})"
|
||||
return "approx({!r})".format(
|
||||
seq_type(self._approx_scalar(x) for x in self.expected)
|
||||
)
|
||||
|
||||
def _repr_compare(self, other_side: Sequence[float]) -> list[str]:
|
||||
def _repr_compare(self, other_side: Sequence[float]) -> List[str]:
|
||||
import math
|
||||
|
||||
if len(self.expected) != len(other_side):
|
||||
@@ -339,21 +344,17 @@ class ApproxSequenceLike(ApproxBase):
|
||||
max_rel_diff = -math.inf
|
||||
different_ids = []
|
||||
for i, (approx_value, other_value) in enumerate(
|
||||
zip(approx_side_as_map, other_side, strict=True)
|
||||
zip(approx_side_as_map, other_side)
|
||||
):
|
||||
if approx_value != other_value:
|
||||
try:
|
||||
abs_diff = abs(approx_value.expected - other_value)
|
||||
max_abs_diff = max(max_abs_diff, abs_diff)
|
||||
# Ignore non-numbers for the diff calculations (#13012).
|
||||
except TypeError:
|
||||
pass
|
||||
abs_diff = abs(approx_value.expected - other_value)
|
||||
max_abs_diff = max(max_abs_diff, abs_diff)
|
||||
if other_value == 0.0:
|
||||
max_rel_diff = math.inf
|
||||
else:
|
||||
if other_value == 0.0:
|
||||
max_rel_diff = math.inf
|
||||
else:
|
||||
max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
|
||||
max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
|
||||
different_ids.append(i)
|
||||
|
||||
message_data = [
|
||||
(str(i), str(other_side[i]), str(approx_side_as_map[i]))
|
||||
for i in different_ids
|
||||
@@ -377,7 +378,7 @@ class ApproxSequenceLike(ApproxBase):
|
||||
return super().__eq__(actual)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
return zip(actual, self.expected, strict=True)
|
||||
return zip(actual, self.expected)
|
||||
|
||||
def _check_type(self) -> None:
|
||||
__tracebackhide__ = True
|
||||
@@ -392,8 +393,8 @@ class ApproxScalar(ApproxBase):
|
||||
|
||||
# Using Real should be better than this Union, but not possible yet:
|
||||
# https://github.com/python/typeshed/pull/3108
|
||||
DEFAULT_ABSOLUTE_TOLERANCE: float | Decimal = 1e-12
|
||||
DEFAULT_RELATIVE_TOLERANCE: float | Decimal = 1e-6
|
||||
DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12
|
||||
DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string communicating both the expected value and the
|
||||
@@ -404,21 +405,15 @@ class ApproxScalar(ApproxBase):
|
||||
# Don't show a tolerance for values that aren't compared using
|
||||
# tolerances, i.e. non-numerics and infinities. Need to call abs to
|
||||
# handle complex numbers, e.g. (inf + 1j).
|
||||
if (
|
||||
isinstance(self.expected, bool)
|
||||
or (not isinstance(self.expected, Complex | Decimal))
|
||||
or math.isinf(abs(self.expected) or isinstance(self.expected, bool))
|
||||
if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf(
|
||||
abs(self.expected) # type: ignore[arg-type]
|
||||
):
|
||||
return str(self.expected)
|
||||
|
||||
# If a sensible tolerance can't be calculated, self.tolerance will
|
||||
# raise a ValueError. In this case, display '???'.
|
||||
try:
|
||||
if 1e-3 <= self.tolerance < 1e3:
|
||||
vetted_tolerance = f"{self.tolerance:n}"
|
||||
else:
|
||||
vetted_tolerance = f"{self.tolerance:.1e}"
|
||||
|
||||
vetted_tolerance = f"{self.tolerance:.1e}"
|
||||
if (
|
||||
isinstance(self.expected, Complex)
|
||||
and self.expected.imag
|
||||
@@ -433,42 +428,30 @@ class ApproxScalar(ApproxBase):
|
||||
def __eq__(self, actual) -> bool:
|
||||
"""Return whether the given value is equal to the expected value
|
||||
within the pre-specified tolerance."""
|
||||
|
||||
def is_bool(val: Any) -> bool:
|
||||
# Check if `val` is a native bool or numpy bool.
|
||||
if isinstance(val, bool):
|
||||
return True
|
||||
if np := sys.modules.get("numpy"):
|
||||
return isinstance(val, np.bool_)
|
||||
return False
|
||||
|
||||
asarray = _as_numpy_array(actual)
|
||||
if asarray is not None:
|
||||
# Call ``__eq__()`` manually to prevent infinite-recursion with
|
||||
# numpy<1.13. See #3748.
|
||||
return all(self.__eq__(a) for a in asarray.flat)
|
||||
|
||||
# Short-circuit exact equality, except for bool and np.bool_
|
||||
if is_bool(self.expected) and not is_bool(actual):
|
||||
return False
|
||||
elif actual == self.expected:
|
||||
# Short-circuit exact equality.
|
||||
if actual == self.expected:
|
||||
return True
|
||||
|
||||
# If either type is non-numeric, fall back to strict equality.
|
||||
# NB: we need Complex, rather than just Number, to ensure that __abs__,
|
||||
# __sub__, and __float__ are defined. Also, consider bool to be
|
||||
# non-numeric, even though it has the required arithmetic.
|
||||
if is_bool(self.expected) or not (
|
||||
isinstance(self.expected, Complex | Decimal)
|
||||
and isinstance(actual, Complex | Decimal)
|
||||
# __sub__, and __float__ are defined.
|
||||
if not (
|
||||
isinstance(self.expected, (Complex, Decimal))
|
||||
and isinstance(actual, (Complex, Decimal))
|
||||
):
|
||||
return False
|
||||
|
||||
# Allow the user to control whether NaNs are considered equal to each
|
||||
# other or not. The abs() calls are for compatibility with complex
|
||||
# numbers.
|
||||
if math.isnan(abs(self.expected)):
|
||||
return self.nan_ok and math.isnan(abs(actual))
|
||||
if math.isnan(abs(self.expected)): # type: ignore[arg-type]
|
||||
return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type]
|
||||
|
||||
# Infinity shouldn't be approximately equal to anything but itself, but
|
||||
# if there's a relative tolerance, it will be infinite and infinity
|
||||
@@ -476,14 +459,15 @@ class ApproxScalar(ApproxBase):
|
||||
# case would have been short circuited above, so here we can just
|
||||
# return false if the expected value is infinite. The abs() call is
|
||||
# for compatibility with complex numbers.
|
||||
if math.isinf(abs(self.expected)):
|
||||
if math.isinf(abs(self.expected)): # type: ignore[arg-type]
|
||||
return False
|
||||
|
||||
# Return true if the two numbers are within the tolerance.
|
||||
result: bool = abs(self.expected - actual) <= self.tolerance
|
||||
return result
|
||||
|
||||
__hash__ = None
|
||||
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
||||
__hash__ = None # type: ignore
|
||||
|
||||
@property
|
||||
def tolerance(self):
|
||||
@@ -539,25 +523,6 @@ class ApproxDecimal(ApproxScalar):
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
|
||||
DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if isinstance(self.rel, float):
|
||||
rel = Decimal.from_float(self.rel)
|
||||
else:
|
||||
rel = self.rel
|
||||
|
||||
if isinstance(self.abs, float):
|
||||
abs_ = Decimal.from_float(self.abs)
|
||||
else:
|
||||
abs_ = self.abs
|
||||
|
||||
tol_str = "???"
|
||||
if rel is not None and Decimal("1e-3") <= rel <= Decimal("1e3"):
|
||||
tol_str = f"{rel:.1e}"
|
||||
elif abs_ is not None:
|
||||
tol_str = f"{abs_:.1e}"
|
||||
|
||||
return f"{self.expected} ± {tol_str}"
|
||||
|
||||
|
||||
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
"""Assert that two numbers (or two ordered sequences of numbers) are equal to each other
|
||||
@@ -659,10 +624,8 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
>>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
|
||||
True
|
||||
|
||||
**Non-numeric types**
|
||||
|
||||
You can also use ``approx`` to compare non-numeric types, or dicts and
|
||||
sequences containing non-numeric types, in which case it falls back to
|
||||
You can also use ``approx`` to compare nonnumeric types, or dicts and
|
||||
sequences containing nonnumeric types, in which case it falls back to
|
||||
strict equality. This can be useful for comparing dicts and sequences that
|
||||
can contain optional values::
|
||||
|
||||
@@ -719,15 +682,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
from the
|
||||
`re_assert package <https://github.com/asottile/re-assert>`_.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Unlike built-in equality, this function considers
|
||||
booleans unequal to numeric zero or one. For example::
|
||||
|
||||
>>> 1 == approx(True)
|
||||
False
|
||||
|
||||
.. warning::
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
@@ -746,12 +700,13 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
|
||||
.. versionchanged:: 3.7.1
|
||||
``approx`` raises ``TypeError`` when it encounters a dict value or
|
||||
sequence element of non-numeric type.
|
||||
sequence element of nonnumeric type.
|
||||
|
||||
.. versionchanged:: 6.1.0
|
||||
``approx`` falls back to strict equality for non-numeric types instead
|
||||
``approx`` falls back to strict equality for nonnumeric types instead
|
||||
of raising ``TypeError``.
|
||||
"""
|
||||
|
||||
# Delegate the comparison to a class that knows how to deal with the type
|
||||
# of the expected value (e.g. int, float, list, dict, numpy.array, etc).
|
||||
#
|
||||
@@ -770,16 +725,25 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
__tracebackhide__ = True
|
||||
|
||||
if isinstance(expected, Decimal):
|
||||
cls: type[ApproxBase] = ApproxDecimal
|
||||
cls: Type[ApproxBase] = ApproxDecimal
|
||||
elif isinstance(expected, Mapping):
|
||||
cls = ApproxMapping
|
||||
elif _is_numpy_array(expected):
|
||||
expected = _as_numpy_array(expected)
|
||||
cls = ApproxNumpy
|
||||
elif _is_sequence_like(expected):
|
||||
elif (
|
||||
hasattr(expected, "__getitem__")
|
||||
and isinstance(expected, Sized)
|
||||
# Type ignored because the error is wrong -- not unreachable.
|
||||
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
|
||||
):
|
||||
cls = ApproxSequenceLike
|
||||
elif isinstance(expected, Collection) and not isinstance(expected, str | bytes):
|
||||
msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}"
|
||||
elif (
|
||||
isinstance(expected, Collection)
|
||||
# Type ignored because the error is wrong -- not unreachable.
|
||||
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
|
||||
):
|
||||
msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
@@ -787,14 +751,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
return cls(expected, rel, abs, nan_ok)
|
||||
|
||||
|
||||
def _is_sequence_like(expected: object) -> bool:
|
||||
return (
|
||||
hasattr(expected, "__getitem__")
|
||||
and isinstance(expected, Sized)
|
||||
and not isinstance(expected, str | bytes)
|
||||
)
|
||||
|
||||
|
||||
def _is_numpy_array(obj: object) -> bool:
|
||||
"""
|
||||
Return true if the given object is implicitly convertible to ndarray,
|
||||
@@ -803,11 +759,13 @@ def _is_numpy_array(obj: object) -> bool:
|
||||
return _as_numpy_array(obj) is not None
|
||||
|
||||
|
||||
def _as_numpy_array(obj: object) -> ndarray | None:
|
||||
def _as_numpy_array(obj: object) -> Optional["ndarray"]:
|
||||
"""
|
||||
Return an ndarray if the given object is implicitly convertible to ndarray,
|
||||
and numpy is already imported, otherwise None.
|
||||
"""
|
||||
import sys
|
||||
|
||||
np: Any = sys.modules.get("numpy")
|
||||
if np is not None:
|
||||
# avoid infinite recursion on numpy scalars, which have __array__
|
||||
@@ -818,3 +776,221 @@ def _as_numpy_array(obj: object) -> ndarray | None:
|
||||
elif hasattr(obj, "__array__") or hasattr("obj", "__array_interface__"):
|
||||
return np.asarray(obj)
|
||||
return None
|
||||
|
||||
|
||||
# builtin pytest.raises helper
|
||||
|
||||
E = TypeVar("E", bound=BaseException)
|
||||
|
||||
|
||||
@overload
|
||||
def raises(
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||
*,
|
||||
match: Optional[Union[str, Pattern[str]]] = ...,
|
||||
) -> "RaisesContext[E]":
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def raises( # noqa: F811
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||
func: Callable[..., Any],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> _pytest._code.ExceptionInfo[E]:
|
||||
...
|
||||
|
||||
|
||||
def raises( # noqa: F811
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
|
||||
) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
|
||||
r"""Assert that a code block/function call raises an exception.
|
||||
|
||||
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
|
||||
The expected exception type, or a tuple if one of multiple possible
|
||||
exception types are expected.
|
||||
:kwparam str | typing.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 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 :py:func:`pytest.raises` is used as a context manager,
|
||||
and passed through to the function otherwise.
|
||||
When using :py:func:`pytest.raises` as a function, you can use:
|
||||
``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
|
||||
|
||||
.. currentmodule:: _pytest._code
|
||||
|
||||
Use ``pytest.raises`` as a context manager, which will capture the exception of the given
|
||||
type::
|
||||
|
||||
>>> import pytest
|
||||
>>> with pytest.raises(ZeroDivisionError):
|
||||
... 1/0
|
||||
|
||||
If the code block does not raise the expected exception (``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 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"
|
||||
|
||||
.. 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
|
||||
|
||||
**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.
|
||||
|
||||
**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 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'."
|
||||
)
|
||||
if isinstance(expected_exception, type):
|
||||
expected_exceptions: Tuple[Type[E], ...] = (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]
|
||||
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
|
||||
raise TypeError(msg.format(not_a))
|
||||
|
||||
message = f"DID NOT RAISE {expected_exception}"
|
||||
|
||||
if not args:
|
||||
match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None)
|
||||
if kwargs:
|
||||
msg = "Unexpected keyword arguments passed to pytest.raises: "
|
||||
msg += ", ".join(sorted(kwargs))
|
||||
msg += "\nUse context-manager form instead?"
|
||||
raise TypeError(msg)
|
||||
return RaisesContext(expected_exception, message, match)
|
||||
else:
|
||||
func = args[0]
|
||||
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:
|
||||
return _pytest._code.ExceptionInfo.from_exception(e)
|
||||
fail(message)
|
||||
|
||||
|
||||
# This doesn't work with mypy for now. Use fail.Exception instead.
|
||||
raises.Exception = fail.Exception # type: ignore
|
||||
|
||||
|
||||
@final
|
||||
class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
|
||||
def __init__(
|
||||
self,
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||
message: str,
|
||||
match_expr: Optional[Union[str, Pattern[str]]] = None,
|
||||
) -> None:
|
||||
self.expected_exception = expected_exception
|
||||
self.message = message
|
||||
self.match_expr = match_expr
|
||||
self.excinfo: Optional[_pytest._code.ExceptionInfo[E]] = None
|
||||
|
||||
def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
|
||||
self.excinfo = _pytest._code.ExceptionInfo.for_later()
|
||||
return self.excinfo
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> bool:
|
||||
__tracebackhide__ = True
|
||||
if exc_type is None:
|
||||
fail(self.message)
|
||||
assert self.excinfo is not None
|
||||
if 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)
|
||||
if self.match_expr is not None:
|
||||
self.excinfo.match(self.match_expr)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user