summaryrefslogtreecommitdiffstats
path: root/contrib/python/pytest/py3/_pytest/python_api.py
diff options
context:
space:
mode:
authorarcadia-devtools <[email protected]>2022-02-14 00:49:36 +0300
committerarcadia-devtools <[email protected]>2022-02-14 00:49:36 +0300
commit82cfd1b7cab2d843cdf5467d9737f72597a493bd (patch)
tree1dfdcfe81a1a6b193ceacc2a828c521b657a339b /contrib/python/pytest/py3/_pytest/python_api.py
parent3df7211d3e3691f8e33b0a1fb1764fe810d59302 (diff)
intermediate changes
ref:68b1302de4b5da30b6bdf02193f7a2604d8b5cf8
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/python_api.py')
-rw-r--r--contrib/python/pytest/py3/_pytest/python_api.py277
1 files changed, 226 insertions, 51 deletions
diff --git a/contrib/python/pytest/py3/_pytest/python_api.py b/contrib/python/pytest/py3/_pytest/python_api.py
index 81ce4f89539..cb72fde1e1f 100644
--- a/contrib/python/pytest/py3/_pytest/python_api.py
+++ b/contrib/python/pytest/py3/_pytest/python_api.py
@@ -1,7 +1,5 @@
import math
import pprint
-from collections.abc import Iterable
-from collections.abc import Mapping
from collections.abc import Sized
from decimal import Decimal
from numbers import Complex
@@ -10,9 +8,13 @@ from typing import Any
from typing import Callable
from typing import cast
from typing import Generic
+from typing import Iterable
+from typing import List
+from typing import Mapping
from typing import Optional
from typing import overload
from typing import Pattern
+from typing import Sequence
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
@@ -38,6 +40,32 @@ def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
)
+def _compare_approx(
+ full_object: object,
+ 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]:
+ message_list = list(message_data)
+ message_list.insert(0, ("Index", "Obtained", "Expected"))
+ max_sizes = [0, 0, 0]
+ for index, obtained, expected in message_list:
+ max_sizes[0] = max(max_sizes[0], len(index))
+ max_sizes[1] = max(max_sizes[1], len(obtained))
+ max_sizes[2] = max(max_sizes[2], len(expected))
+ explanation = [
+ f"comparison failed. Mismatched elements: {len(different_ids)} / {number_of_elements}:",
+ f"Max absolute difference: {max_abs_diff}",
+ f"Max relative difference: {max_rel_diff}",
+ ] + [
+ f"{indexes:<{max_sizes[0]}} | {obtained:<{max_sizes[1]}} | {expected:<{max_sizes[2]}}"
+ for indexes, obtained, expected in message_list
+ ]
+ return explanation
+
+
# builtin pytest.approx helper
@@ -60,11 +88,24 @@ class ApproxBase:
def __repr__(self) -> str:
raise NotImplementedError
+ def _repr_compare(self, other_side: Any) -> List[str]:
+ return [
+ "comparison failed",
+ f"Obtained: {other_side}",
+ f"Expected: {self}",
+ ]
+
def __eq__(self, actual) -> bool:
return all(
a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual)
)
+ def __bool__(self):
+ __tracebackhide__ = True
+ raise AssertionError(
+ "approx() is not supported in a boolean context.\nDid you mean: `assert a == approx(b)`?"
+ )
+
# Ignore type because of https://github.com/python/mypy/issues/4266.
__hash__ = None # type: ignore
@@ -72,6 +113,8 @@ class ApproxBase:
return not (actual == self)
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)
def _yield_comparisons(self, actual):
@@ -93,7 +136,7 @@ class ApproxBase:
def _recursive_list_map(f, x):
if isinstance(x, list):
- return list(_recursive_list_map(f, xi) for xi in x)
+ return [_recursive_list_map(f, xi) for xi in x]
else:
return f(x)
@@ -105,6 +148,66 @@ class ApproxNumpy(ApproxBase):
list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist())
return f"approx({list_scalars!r})"
+ 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, ...]
+ ) -> Any:
+ """
+ Helper function to get the value out of a nested list, given an n-dimensional index.
+ This mimics numpy's indexing, but for raw nested python lists.
+ """
+ value: Any = nested_list
+ for i in nd_index:
+ value = value[i]
+ return value
+
+ np_array_shape = self.expected.shape
+ approx_side_as_list = _recursive_list_map(
+ self._approx_scalar, self.expected.tolist()
+ )
+
+ if np_array_shape != other_side.shape:
+ return [
+ "Impossible to compare arrays with different shapes.",
+ f"Shapes: {np_array_shape} and {other_side.shape}",
+ ]
+
+ number_of_elements = self.expected.size
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ 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_list, 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)
+ if other_value == 0.0:
+ max_rel_diff = math.inf
+ else:
+ max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
+ different_ids.append(index)
+
+ message_data = [
+ (
+ str(index),
+ str(get_value_from_nested_list(other_side, index)),
+ str(get_value_from_nested_list(approx_side_as_list, index)),
+ )
+ for index in different_ids
+ ]
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
def __eq__(self, actual) -> bool:
import numpy as np
@@ -119,7 +222,7 @@ class ApproxNumpy(ApproxBase):
if not np.isscalar(actual) and actual.shape != self.expected.shape:
return False
- return ApproxBase.__eq__(self, actual)
+ return super().__eq__(actual)
def _yield_comparisons(self, actual):
import numpy as np
@@ -145,6 +248,44 @@ class ApproxMapping(ApproxBase):
{k: self._approx_scalar(v) for k, v in self.expected.items()}
)
+ def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:
+ import math
+
+ approx_side_as_map = {
+ k: self._approx_scalar(v) for k, v in self.expected.items()
+ }
+
+ number_of_elements = len(approx_side_as_map)
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for (approx_key, approx_value), other_value in zip(
+ approx_side_as_map.items(), other_side.values()
+ ):
+ if approx_value != other_value:
+ max_abs_diff = max(
+ max_abs_diff, abs(approx_value.expected - other_value)
+ )
+ max_rel_diff = max(
+ max_rel_diff,
+ abs((approx_value.expected - other_value) / approx_value.expected),
+ )
+ different_ids.append(approx_key)
+
+ message_data = [
+ (str(key), str(other_side[key]), str(approx_side_as_map[key]))
+ for key in different_ids
+ ]
+
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
def __eq__(self, actual) -> bool:
try:
if set(actual.keys()) != set(self.expected.keys()):
@@ -152,7 +293,7 @@ class ApproxMapping(ApproxBase):
except AttributeError:
return False
- return ApproxBase.__eq__(self, actual)
+ return super().__eq__(actual)
def _yield_comparisons(self, actual):
for k in self.expected.keys():
@@ -177,13 +318,55 @@ class ApproxSequencelike(ApproxBase):
seq_type(self._approx_scalar(x) for x in self.expected)
)
+ def _repr_compare(self, other_side: Sequence[float]) -> List[str]:
+ import math
+ import numpy as np
+
+ if len(self.expected) != len(other_side):
+ return [
+ "Impossible to compare lists with different sizes.",
+ f"Lengths: {len(self.expected)} and {len(other_side)}",
+ ]
+
+ approx_side_as_map = _recursive_list_map(self._approx_scalar, self.expected)
+
+ number_of_elements = len(approx_side_as_map)
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for i, (approx_value, other_value) in enumerate(
+ zip(approx_side_as_map, other_side)
+ ):
+ if approx_value != other_value:
+ 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 = np.inf
+ else:
+ 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
+ ]
+
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
def __eq__(self, actual) -> bool:
try:
if len(actual) != len(self.expected):
return False
except TypeError:
return False
- return ApproxBase.__eq__(self, actual)
+ return super().__eq__(actual)
def _yield_comparisons(self, actual):
return zip(actual, self.expected)
@@ -210,7 +393,6 @@ class ApproxScalar(ApproxBase):
For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
"""
-
# 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).
@@ -317,7 +499,7 @@ class ApproxScalar(ApproxBase):
if relative_tolerance < 0:
raise ValueError(
- f"relative tolerance can't be negative: {absolute_tolerance}"
+ f"relative tolerance can't be negative: {relative_tolerance}"
)
if math.isnan(relative_tolerance):
raise ValueError("relative tolerance can't be NaN.")
@@ -337,14 +519,12 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
"""Assert that two numbers (or two sets of numbers) are equal to each other
within some tolerance.
- Due to the `intricacies of floating-point arithmetic`__, numbers that we
+ Due to the :std:doc:`tutorial/floatingpoint`, numbers that we
would intuitively expect to be equal are not always so::
>>> 0.1 + 0.2 == 0.3
False
- __ https://docs.python.org/3/tutorial/floatingpoint.html
-
This problem is commonly encountered when writing tests, e.g. when making
sure that floating-point values are what you expect them to be. One way to
deal with this problem is to assert that two floating-point numbers are
@@ -449,27 +629,22 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor
``b`` is a "reference value"). You have to specify an absolute tolerance
if you want to compare to ``0.0`` because there is no tolerance by
- default. `More information...`__
-
- __ https://docs.python.org/3/library/math.html#math.isclose
+ default. More information: :py:func:`math.isclose`.
- ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference
between ``a`` and ``b`` is less that the sum of the relative tolerance
w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance
is only calculated w.r.t. ``b``, this test is asymmetric and you can
think of ``b`` as the reference value. Support for comparing sequences
- is provided by ``numpy.allclose``. `More information...`__
-
- __ https://numpy.org/doc/stable/reference/generated/numpy.isclose.html
+ is provided by :py:func:`numpy.allclose`. More information:
+ :std:doc:`numpy:reference/generated/numpy.isclose`.
- ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b``
are within an absolute tolerance of ``1e-7``. No relative tolerance is
- considered and the absolute tolerance cannot be changed, so this function
- is not appropriate for very large or very small numbers. Also, it's only
- available in subclasses of ``unittest.TestCase`` and it's ugly because it
- doesn't follow PEP8. `More information...`__
-
- __ https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertAlmostEqual
+ considered , so this function is not appropriate for very large or very
+ small numbers. Also, it's only available in subclasses of ``unittest.TestCase``
+ and it's ugly because it doesn't follow PEP8. More information:
+ :py:meth:`unittest.TestCase.assertAlmostEqual`.
- ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative
tolerance is met w.r.t. ``b`` or if the absolute tolerance is met.
@@ -478,11 +653,17 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
special case that you explicitly specify an absolute tolerance but not a
relative tolerance, only the absolute tolerance is considered.
+ .. note::
+
+ ``approx`` can handle numpy arrays, but we recommend the
+ specialised test helpers in :std:doc:`numpy:reference/routines.testing`
+ if you need support for comparisons, NaNs, or ULP-based tolerances.
+
.. warning::
.. versionchanged:: 3.2
- In order to avoid inconsistent behavior, ``TypeError`` is
+ In order to avoid inconsistent behavior, :py:exc:`TypeError` is
raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons.
The example below illustrates the problem::
@@ -492,9 +673,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)``
to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to
comparison. This is because the call hierarchy of rich comparisons
- follows a fixed behavior. `More information...`__
-
- __ https://docs.python.org/3/reference/datamodel.html#object.__ge__
+ follows a fixed behavior. More information: :py:meth:`object.__ge__`
.. versionchanged:: 3.7.1
``approx`` raises ``TypeError`` when it encounters a dict value or
@@ -571,48 +750,46 @@ def _as_numpy_array(obj: object) -> Optional["ndarray"]:
# builtin pytest.raises helper
-_E = TypeVar("_E", bound=BaseException)
+E = TypeVar("E", bound=BaseException)
@overload
def raises(
- expected_exception: Union[Type[_E], Tuple[Type[_E], ...]],
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]],
*,
match: Optional[Union[str, Pattern[str]]] = ...,
-) -> "RaisesContext[_E]":
+) -> "RaisesContext[E]":
...
@overload
def raises(
- expected_exception: Union[Type[_E], Tuple[Type[_E], ...]],
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]],
func: Callable[..., Any],
*args: Any,
**kwargs: Any,
-) -> _pytest._code.ExceptionInfo[_E]:
+) -> _pytest._code.ExceptionInfo[E]:
...
def raises(
- expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], *args: Any, **kwargs: Any
-) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]:
+ 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 ``expected_exception``
or raise a failure exception otherwise.
:kwparam 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 ``re.search``. To match a literal
- string that may contain `special characters`__, the pattern can
- first be escaped with ``re.escape``.
+ representation of the exception using :py:func:`re.search`. To match a literal
+ string that may contain :std:ref:`special characters <re-syntax>`, the pattern can
+ first be escaped with :py:func:`re.escape`.
- (This is only used when ``pytest.raises`` is used as a context manager,
+ (This is only used when :py:func:`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:
+ When using :py:func:`pytest.raises` as a function, you can use:
``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
- __ https://docs.python.org/3/library/re.html#regular-expression-syntax
-
.. currentmodule:: _pytest._code
Use ``pytest.raises`` as a context manager, which will capture the exception of the given
@@ -709,11 +886,11 @@ def raises(
__tracebackhide__ = True
if isinstance(expected_exception, type):
- excepted_exceptions: Tuple[Type[_E], ...] = (expected_exception,)
+ excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,)
else:
excepted_exceptions = expected_exception
for exc in excepted_exceptions:
- if not isinstance(exc, type) or not issubclass(exc, BaseException): # type: ignore[unreachable]
+ 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))
@@ -731,9 +908,7 @@ def raises(
else:
func = args[0]
if not callable(func):
- raise TypeError(
- "{!r} object (type: {}) must be callable".format(func, type(func))
- )
+ raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
try:
func(*args[1:], **kwargs)
except expected_exception as e:
@@ -750,19 +925,19 @@ raises.Exception = fail.Exception # type: ignore
@final
-class RaisesContext(Generic[_E]):
+class RaisesContext(Generic[E]):
def __init__(
self,
- expected_exception: Union[Type[_E], Tuple[Type[_E], ...]],
+ 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
+ self.excinfo: Optional[_pytest._code.ExceptionInfo[E]] = None
- def __enter__(self) -> _pytest._code.ExceptionInfo[_E]:
+ def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
self.excinfo = _pytest._code.ExceptionInfo.for_later()
return self.excinfo
@@ -779,7 +954,7 @@ class RaisesContext(Generic[_E]):
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))
+ 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)