Improve pytest.approx error messages readability (Pull request) (#8429)

Improve pytest.approx error messages readability (Pull request)
This commit is contained in:
Tarcísio Fischer
2021-04-30 07:36:56 -03:00
committed by GitHub
parent 992c403fc8
commit 9d9b84d175
4 changed files with 428 additions and 4 deletions

View File

@@ -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,6 +88,13 @@ 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)
@@ -107,6 +142,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
@@ -147,6 +242,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()):
@@ -179,6 +312,48 @@ 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):
@@ -212,7 +387,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).