diff options
| author | arcadia-devtools <[email protected]> | 2022-02-14 00:49:36 +0300 |
|---|---|---|
| committer | arcadia-devtools <[email protected]> | 2022-02-14 00:49:36 +0300 |
| commit | 82cfd1b7cab2d843cdf5467d9737f72597a493bd (patch) | |
| tree | 1dfdcfe81a1a6b193ceacc2a828c521b657a339b /contrib/python/pytest/py3/_pytest/python_api.py | |
| parent | 3df7211d3e3691f8e33b0a1fb1764fe810d59302 (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.py | 277 |
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) |
