/
niceSOFT
/
python3-pytest
Обзор
Документация
Войти
/
niceSOFT
/
python3-pytest
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
testing/python/approx.py
1 518 строк
54 KB
Madan kumar
Merge pull request #14754, handle infinite relative tolerance for timedelta values
02 авг 2026, 09:33
Не верифицирован
02 авг 2026, 09:33
224e9ef
Код
Авторство
О чём код?
# mypy: allow-untyped-defs from __future__ import annotations from contextlib import contextmanager import datetime import decimal from decimal import Decimal from fractions import Fraction from math import inf from math import nan from math import sqrt import operator from operator import eq from operator import ne import re from _pytest.approx import _recursive_sequence_map from _pytest.pytester import Pytester import pytest from pytest import approx @pytest.fixture def mocked_doctest_runner(monkeypatch): import doctest class MockedPdb: def __init__(self, out): pass def set_trace(self): raise NotImplementedError("not used") def reset(self): pass def set_continue(self): pass monkeypatch.setattr("doctest._OutputRedirectingPdb", MockedPdb) class MyDocTestRunner(doctest.DocTestRunner): def report_failure(self, out, test, example, got): raise AssertionError( f"'{example.source.strip()}' evaluates to '{got.strip()}', not '{example.want.strip()}'" ) return MyDocTestRunner() @contextmanager def temporary_verbosity(config, verbosity=0): original_verbosity = config.getoption("verbose") config.option.verbose = verbosity try: yield finally: config.option.verbose = original_verbosity @pytest.fixture def assert_approx_raises_regex(pytestconfig): def do_assert(lhs, rhs, expected_message, verbosity_level=0): import re with temporary_verbosity(pytestconfig, verbosity_level): with pytest.raises(AssertionError) as e: assert lhs == approx(rhs) nl = "\n" obtained_message = str(e.value).splitlines()[1:] assert len(obtained_message) == len(expected_message), ( "Regex message length doesn't match obtained.\n" "Obtained:\n" f"{nl.join(obtained_message)}\n\n" "Expected regex:\n" f"{nl.join(expected_message)}\n\n" ) for i, (obtained_line, expected_line) in enumerate( zip(obtained_message, expected_message, strict=True) ): regex = re.compile(expected_line) assert regex.match(obtained_line) is not None, ( "Unexpected error message:\n" f"{nl.join(obtained_message)}\n\n" "Did not match regex:\n" f"{nl.join(expected_message)}\n\n" f"With verbosity level = {verbosity_level}, on line {i}" ) return do_assert SOME_FLOAT = r"[+-]?((?:([0-9]*[.])?[0-9]+(e-?[0-9]+)?)|inf|nan)\s*" SOME_INT = r"[0-9]+\s*" SOME_TOLERANCE = rf"({SOME_FLOAT}|[+-]?[0-9]+(\.[0-9]+)?[eE][+-]?[0-9]+\s*)" class TestApprox: def test_error_messages_native_dtypes(self, assert_approx_raises_regex): # Treat bool exactly. assert_approx_raises_regex( {"a": 1.0, "b": True}, {"a": 1.0, "b": False}, [ "", " comparison failed. Mismatched elements: 1 / 2:", f" Max absolute difference: {SOME_FLOAT}", f" Max relative difference: {SOME_FLOAT}", r" Index\s+\| Obtained\s+\| Expected", r".*(True|False)\s+", ], ) assert_approx_raises_regex( 2.0, 1.0, [ "", " comparison failed", f" Obtained: {SOME_FLOAT}", f" Expected: {SOME_FLOAT} ± {SOME_TOLERANCE}", ], ) assert_approx_raises_regex( {"a": 1.0, "b": 1000.0, "c": 1000000.0}, { "a": 2.0, "b": 1000.0, "c": 3000000.0, }, [ r"", r" comparison failed. Mismatched elements: 2 / 3:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", r" Index \| Obtained\s+\| Expected\s+", rf" a \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_TOLERANCE}", rf" c \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_TOLERANCE}", ], ) assert_approx_raises_regex( {"a": 1.0, "b": None, "c": None}, { "a": None, "b": 1000.0, "c": None, }, [ r"", r" comparison failed. Mismatched elements: 2 / 3:", r" Max absolute difference: -inf", r" Max relative difference: -inf", r" Index \| Obtained\s+\| Expected\s+", rf" a \| {SOME_FLOAT} \| None", rf" b \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}", ], ) assert_approx_raises_regex( [1.0, 2.0, 3.0, 4.0], [1.0, 3.0, 3.0, 5.0], [ r"", r" comparison failed. Mismatched elements: 2 / 4:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", r" Index \| Obtained\s+\| Expected ", rf" 1 \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", rf" 3 \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", ], ) assert_approx_raises_regex( (1, 2.2, 4), (1, 3.2, 4), [ r"", r" comparison failed. Mismatched elements: 1 / 3:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", r" Index \| Obtained\s+\| Expected ", rf" 1 \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", ], ) # Specific test for comparison with 0.0 (relative diff will be 'inf') assert_approx_raises_regex( [0.0], [1.0], [ r"", r" comparison failed. Mismatched elements: 1 / 1:", rf" Max absolute difference: {SOME_FLOAT}", r" Max relative difference: inf", r" Index \| Obtained\s+\| Expected ", rf"\s*0\s*\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", ], ) def test_error_messages_numpy_dtypes(self, assert_approx_raises_regex): np = pytest.importorskip("numpy") a = np.linspace(0, 100, 20) b = np.linspace(0, 100, 20) a[10] += 0.5 assert_approx_raises_regex( a, b, [ r"", r" comparison failed. Mismatched elements: 1 / 20:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", r" Index \| Obtained\s+\| Expected", rf" \(10,\) \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", ], ) assert_approx_raises_regex( np.array( [ [[1.1987311, 12412342.3], [3.214143244, 1423412423415.677]], [[1, 2], [3, 219371297321973]], ] ), np.array( [ [[1.12313, 12412342.3], [3.214143244, 534523542345.677]], [[1, 2], [3, 7]], ] ), [ r"", r" comparison failed. Mismatched elements: 3 / 8:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", r" Index\s+\| Obtained\s+\| Expected\s+", rf" \(0, 0, 0\) \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", rf" \(0, 1, 1\) \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", rf" \(1, 1, 1\) \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", ], ) # Specific test for comparison with 0.0 (relative diff will be 'inf') assert_approx_raises_regex( np.array([0.0]), np.array([1.0]), [ r"", r" comparison failed. Mismatched elements: 1 / 1:", rf" Max absolute difference: {SOME_FLOAT}", r" Max relative difference: inf", r" Index \| Obtained\s+\| Expected ", rf"\s*\(0,\)\s*\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", ], ) def test_error_messages_invalid_args(self, assert_approx_raises_regex): np = pytest.importorskip("numpy") with pytest.raises(AssertionError) as e: assert np.array([[1.2, 3.4], [4.0, 5.0]]) == pytest.approx( np.array([[4.0], [5.0]]) ) message = "\n".join(str(e.value).split("\n")[1:]) assert message == "\n".join( [ " ", " Impossible to compare arrays with different shapes.", " Shapes: (2, 1) and (2, 2)", ] ) with pytest.raises(AssertionError) as e: assert [1.0, 2.0, 3.0] == pytest.approx([4.0, 5.0]) message = "\n".join(str(e.value).split("\n")[1:]) assert message == "\n".join( [ " ", " Impossible to compare lists with different sizes.", " Lengths: 2 and 3", ] ) def test_error_messages_with_different_verbosity(self, assert_approx_raises_regex): np = pytest.importorskip("numpy") for v in [0, 1, 2]: # Verbosity level doesn't affect the error message for scalars assert_approx_raises_regex( 2.0, 1.0, [ "", " comparison failed", f" Obtained: {SOME_FLOAT}", f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}", ], verbosity_level=v, ) a = np.linspace(1, 101, 20) b = np.linspace(2, 102, 20) assert_approx_raises_regex( a, b, [ r"^ $", r"^ comparison failed. Mismatched elements: 20 / 20:$", rf"^ Max absolute difference: {SOME_FLOAT}$", rf"^ Max relative difference: {SOME_FLOAT}$", r"^ Index \| Obtained\s+\| Expected\s+$", rf"^ \(0,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}e-{SOME_INT}$", rf"^ \(1,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}e-{SOME_INT}\.\.\.$", "^ $", r"^ ...Full output truncated, use '-vv' to show$", ], verbosity_level=0, ) assert_approx_raises_regex( a, b, [ r" ", r" comparison failed. Mismatched elements: 20 / 20:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", r" Index \| Obtained\s+\| Expected", ] + [ rf" \({i},\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}" for i in range(20) ], verbosity_level=2, ) def test_repr_string(self): assert repr(approx(1.0)) == "1.0 ± 1.0e-06" assert repr(approx([1.0, 2.0])) == "approx([1.0 ± 1.0e-06, 2.0 ± 2.0e-06])" assert repr(approx((1.0, 2.0))) == "approx((1.0 ± 1.0e-06, 2.0 ± 2.0e-06))" assert repr(approx(inf)) == "inf" assert repr(approx(1.0, rel=nan)) == "1.0 ± ???" assert repr(approx(1.0, rel=inf)) == "1.0 ± inf" # Dictionaries aren't ordered, so we need to check both orders. assert repr(approx({"a": 1.0, "b": 2.0})) in ( "approx({'a': 1.0 ± 1.0e-06, 'b': 2.0 ± 2.0e-06})", "approx({'b': 2.0 ± 2.0e-06, 'a': 1.0 ± 1.0e-06})", ) assert repr(approx(42, abs=1)) == "42 ± 1" assert repr(approx(5, rel=0.01)) == "5 ± 0.05" assert repr(approx(24000, abs=500)) == "24000 ± 500" assert repr(approx(1500, abs=555)) == "1500 ± 555" def test_repr_complex_numbers(self): assert repr(approx(inf + 1j)) == "(inf+1j)" assert repr(approx(1.0j, rel=inf)) == "1j ± inf" # can't compute a sensible tolerance assert repr(approx(nan + 1j)) == "(nan+1j) ± ???" assert repr(approx(1.0j)) == "1j ± 1.0e-06 ∠ ±180°" # relative tolerance is scaled to |3+4j| = 5 assert repr(approx(3 + 4 * 1j)) == "(3+4j) ± 5.0e-06 ∠ ±180°" # absolute tolerance is not scaled assert repr(approx(3.3 + 4.4 * 1j, abs=0.02)) == "(3.3+4.4j) ± 0.02 ∠ ±180°" @pytest.mark.parametrize( "value, expected_repr_string", [ (5.0, "approx(5.0 ± 5.0e-06)"), ([5.0], "approx([5.0 ± 5.0e-06])"), ([[5.0]], "approx([[5.0 ± 5.0e-06]])"), ([[5.0, 6.0]], "approx([[5.0 ± 5.0e-06, 6.0 ± 6.0e-06]])"), ([[5.0], [6.0]], "approx([[5.0 ± 5.0e-06], [6.0 ± 6.0e-06]])"), ], ) def test_repr_nd_array(self, value, expected_repr_string): """Make sure that arrays of all different dimensions are repr'd correctly.""" np = pytest.importorskip("numpy") np_array = np.array(value) assert repr(approx(np_array)) == expected_repr_string def test_bool(self): with pytest.raises(AssertionError) as err: assert approx(1) assert err.match(r"approx\(\) is not supported in a boolean context") def test_mixed_sequence(self, assert_approx_raises_regex) -> None: """Approx should work on sequences that also contain non-numbers (#13010).""" assert_approx_raises_regex( [1.1, 2, "word"], [1.0, 2, "different"], [ "", r" comparison failed. Mismatched elements: 2 / 3:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", r" Index \| Obtained\s+\| Expected\s+", r"\s*0\s*\|\s*1\.1\s*\|\s*1\.0\s*±\s*1\.0e\-06\s*", r"\s*2\s*\|\s*word\s*\|\s*different\s*", ], verbosity_level=2, ) assert_approx_raises_regex( [1.1, 2, "word"], [1.0, 2, "word"], [ "", r" comparison failed. Mismatched elements: 1 / 3:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", r" Index \| Obtained\s+\| Expected\s+", r"\s*0\s*\|\s*1\.1\s*\|\s*1\.0\s*±\s*1\.0e\-06\s*", ], verbosity_level=2, ) assert [1.1, 2, "word"] == pytest.approx([1.1, 2, "word"]) def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12) assert not (1 != approx(1, rel=1e-6, abs=1e-12)) assert 10 != approx(1, rel=1e-6, abs=1e-12) assert not (10 == approx(1, rel=1e-6, abs=1e-12)) def test_exactly_equal(self): examples = [ (2.0, 2.0), (0.1e200, 0.1e200), (1.123e-300, 1.123e-300), (12345, 12345.0), (0.0, -0.0), (345678, 345678), (Decimal("1.0001"), Decimal("1.0001")), (Fraction(1, 3), Fraction(-1, -3)), ] for a, x in examples: assert a == approx(x) def test_opposite_sign(self): examples = [(eq, 1e-100, -1e-100), (ne, 1e100, -1e100)] for op, a, x in examples: assert op(a, approx(x)) def test_zero_tolerance(self): within_1e10 = [(1.1e-100, 1e-100), (-1.1e-100, -1e-100)] for a, x in within_1e10: assert x == approx(x, rel=0.0, abs=0.0) assert a != approx(x, rel=0.0, abs=0.0) assert a == approx(x, rel=0.0, abs=5e-101) assert a != approx(x, rel=0.0, abs=5e-102) assert a == approx(x, rel=5e-1, abs=0.0) assert a != approx(x, rel=5e-2, abs=0.0) @pytest.mark.parametrize( ("rel", "abs"), [ (-1e100, None), (None, -1e100), (1e100, -1e100), (-1e100, 1e100), (-1e100, -1e100), ], ) def test_negative_tolerance(self, rel: float | None, abs: float | None) -> None: # Negative tolerances are not allowed. with pytest.raises(ValueError): 1.1 == approx(1, rel, abs) def test_negative_tolerance_message(self): # Error message for negative tolerance should include the value. with pytest.raises(ValueError, match="-3"): 0 == approx(1, abs=-3) with pytest.raises(ValueError, match="-3"): 0 == approx(1, rel=-3) def test_inf_tolerance(self): # Everything should be equal if the tolerance is infinite. large_diffs = [(1, 1000), (1e-50, 1e50), (-1.0, -1e300), (0.0, 10)] for a, x in large_diffs: assert a != approx(x, rel=0.0, abs=0.0) assert a == approx(x, rel=inf, abs=0.0) assert a == approx(x, rel=0.0, abs=inf) assert a == approx(x, rel=inf, abs=inf) def test_inf_tolerance_expecting_zero(self) -> None: # If the relative tolerance is zero but the expected value is infinite, # the actual tolerance is a NaN, which should be an error. with pytest.raises(ValueError): 1 == approx(0, rel=inf, abs=0.0) with pytest.raises(ValueError): 1 == approx(0, rel=inf, abs=inf) def test_nan_tolerance(self) -> None: with pytest.raises(ValueError): 1.1 == approx(1, rel=nan) with pytest.raises(ValueError): 1.1 == approx(1, abs=nan) with pytest.raises(ValueError): 1.1 == approx(1, rel=nan, abs=nan) def test_reasonable_defaults(self): # Whatever the defaults are, they should work for numbers close to 1 # than have a small amount of floating-point error. assert 0.1 + 0.2 == approx(0.3) def test_default_tolerances(self): # This tests the defaults as they are currently set. If you change the # defaults, this test will fail but you should feel free to change it. # None of the other tests (except the doctests) should be affected by # the choice of defaults. examples = [ # Relative tolerance used. (eq, 1e100 + 1e94, 1e100), (ne, 1e100 + 2e94, 1e100), (eq, 1e0 + 1e-6, 1e0), (ne, 1e0 + 2e-6, 1e0), # Absolute tolerance used. (eq, 1e-100, +1e-106), (eq, 1e-100, +2e-106), (eq, 1e-100, 0), ] for op, a, x in examples: assert op(a, approx(x)) def test_custom_tolerances(self): assert 1e8 + 1e0 == approx(1e8, rel=5e-8, abs=5e0) assert 1e8 + 1e0 == approx(1e8, rel=5e-9, abs=5e0) assert 1e8 + 1e0 == approx(1e8, rel=5e-8, abs=5e-1) assert 1e8 + 1e0 != approx(1e8, rel=5e-9, abs=5e-1) assert 1e0 + 1e-8 == approx(1e0, rel=5e-8, abs=5e-8) assert 1e0 + 1e-8 == approx(1e0, rel=5e-9, abs=5e-8) assert 1e0 + 1e-8 == approx(1e0, rel=5e-8, abs=5e-9) assert 1e0 + 1e-8 != approx(1e0, rel=5e-9, abs=5e-9) assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-8, abs=5e-16) assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-9, abs=5e-16) assert 1e-8 + 1e-16 == approx(1e-8, rel=5e-8, abs=5e-17) assert 1e-8 + 1e-16 != approx(1e-8, rel=5e-9, abs=5e-17) def test_relative_tolerance(self): within_1e8_rel = [(1e8 + 1e0, 1e8), (1e0 + 1e-8, 1e0), (1e-8 + 1e-16, 1e-8)] for a, x in within_1e8_rel: assert a == approx(x, rel=5e-8, abs=0.0) assert a != approx(x, rel=5e-9, abs=0.0) def test_absolute_tolerance(self): within_1e8_abs = [(1e8 + 9e-9, 1e8), (1e0 + 9e-9, 1e0), (1e-8 + 9e-9, 1e-8)] for a, x in within_1e8_abs: assert a == approx(x, rel=0, abs=5e-8) assert a != approx(x, rel=0, abs=5e-9) def test_expecting_zero(self): examples = [ (ne, 1e-6, 0.0), (ne, -1e-6, 0.0), (eq, 1e-12, 0.0), (eq, -1e-12, 0.0), (ne, 2e-12, 0.0), (ne, -2e-12, 0.0), (ne, inf, 0.0), (ne, nan, 0.0), ] for op, a, x in examples: assert op(a, approx(x, rel=0.0, abs=1e-12)) assert op(a, approx(x, rel=1e-6, abs=1e-12)) def test_expecting_inf(self): examples = [ (eq, inf, inf), (eq, -inf, -inf), (ne, inf, -inf), (ne, 0.0, inf), (ne, nan, inf), ] for op, a, x in examples: assert op(a, approx(x)) def test_expecting_nan(self): examples = [ (eq, nan, nan), (eq, -nan, -nan), (eq, nan, -nan), (ne, 0.0, nan), (ne, inf, nan), ] for op, a, x in examples: # Nothing is equal to NaN by default. assert a != approx(x) # If ``nan_ok=True``, then NaN is equal to NaN. assert op(a, approx(x, nan_ok=True)) def test_int(self): within_1e6 = [(1000001, 1000000), (-1000001, -1000000)] for a, x in within_1e6: assert a == approx(x, rel=5e-6, abs=0) assert a != approx(x, rel=5e-7, abs=0) assert approx(x, rel=5e-6, abs=0) == a assert approx(x, rel=5e-7, abs=0) != a def test_decimal(self): within_1e6 = [ (Decimal("1.000001"), Decimal("1.0")), (Decimal("-1.000001"), Decimal("-1.0")), ] for a, x in within_1e6: assert a == approx(x) assert a == approx(x, rel=Decimal("5e-6"), abs=0) assert a != approx(x, rel=Decimal("5e-7"), abs=0) assert approx(x, rel=Decimal("5e-6"), abs=0) == a assert approx(x, rel=Decimal("5e-7"), abs=0) != a assert approx(x, rel=0, abs=Decimal("5e-3")) == a assert approx(x, rel=0, abs=Decimal("5e-7")) != a def test_fraction(self): within_1e6 = [ (1 + Fraction(1, 1000000), Fraction(1)), (-1 - Fraction(-1, 1000000), Fraction(-1)), ] for a, x in within_1e6: assert a == approx(x, rel=5e-6, abs=0) assert a != approx(x, rel=5e-7, abs=0) assert approx(x, rel=5e-6, abs=0) == a assert approx(x, rel=5e-7, abs=0) != a def test_complex(self): within_1e6 = [ (1.000001 + 1.0j, 1.0 + 1.0j), (1.0 + 1.000001j, 1.0 + 1.0j), (-1.000001 + 1.0j, -1.0 + 1.0j), (1.0 - 1.000001j, 1.0 - 1.0j), ] for a, x in within_1e6: assert a == approx(x, rel=5e-6, abs=0) assert a != approx(x, rel=5e-7, abs=0) assert approx(x, rel=5e-6, abs=0) == a assert approx(x, rel=5e-7, abs=0) != a def test_expecting_bool(self) -> None: assert True == approx(True) # noqa: E712 assert False == approx(False) # noqa: E712 assert True != approx(False) # noqa: E712 assert True != approx(False, abs=2) # noqa: E712 assert 1 != approx(True) def test_expecting_bool_numpy(self) -> None: """Check approx comparing with numpy.bool (#13047).""" np = pytest.importorskip("numpy") assert np.False_ != approx(True) assert np.True_ != approx(False) assert np.True_ == approx(True) assert np.False_ == approx(False) assert np.True_ != approx(False, abs=2) def test_list(self): actual = [1 + 1e-7, 2 + 1e-8] expected = [1, 2] # Return false if any element is outside the tolerance. assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual def test_list_decimal(self): actual = [Decimal("1.000001"), Decimal("2.000001")] expected = [Decimal("1"), Decimal("2")] assert actual == approx(expected) def test_list_wrong_len(self): assert [1, 2] != approx([1]) assert [1, 2] != approx([1, 2, 3]) def test_tuple(self): actual = (1 + 1e-7, 2 + 1e-8) expected = (1, 2) # Return false if any element is outside the tolerance. assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual def test_tuple_wrong_len(self): assert (1, 2) != approx((1,)) assert (1, 2) != approx((1, 2, 3)) def test_tuple_vs_other(self): assert 1 != approx((1,)) def test_dict(self): actual = {"a": 1 + 1e-7, "b": 2 + 1e-8} # Dictionaries became ordered in python3.6, so switch up the order here # to make sure it doesn't matter. expected = {"b": 2, "a": 1} # Return false if any element is outside the tolerance. assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual def test_dict_decimal(self): actual = {"a": Decimal("1.000001"), "b": Decimal("2.000001")} # Dictionaries became ordered in python3.6, so switch up the order here # to make sure it doesn't matter. expected = {"b": Decimal("2"), "a": Decimal("1")} assert actual == approx(expected) def test_dict_wrong_len(self): assert {"a": 1, "b": 2} != approx({"a": 1}) assert {"a": 1, "b": 2} != approx({"a": 1, "c": 2}) assert {"a": 1, "b": 2} != approx({"a": 1, "b": 2, "c": 3}) def test_dict_nonnumeric(self): assert {"a": 1.0, "b": None} == pytest.approx({"a": 1.0, "b": None}) assert {"a": 1.0, "b": 1} != pytest.approx({"a": 1.0, "b": None}) assert {"a": 1.0, "b": True} != pytest.approx({"a": 1.0, "b": False}, abs=2) def test_dict_vs_other(self): assert 1 != approx({"a": 0}) def test_dict_for_div_by_zero(self, assert_approx_raises_regex): assert_approx_raises_regex( {"foo": 42.0}, {"foo": 0.0}, [ r"", r" comparison failed. Mismatched elements: 1 / 1:", rf" Max absolute difference: {SOME_FLOAT}", r" Max relative difference: inf", r" Index \| Obtained\s+\| Expected ", rf" foo | {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", ], ) def test_dict_differing_lengths(self, assert_approx_raises_regex): assert_approx_raises_regex( {"a": 0}, {"a": 0, "b": 1}, [ " ", r" Impossible to compare mappings with different sizes\.", r" Lengths: 2 and 1", ], ) def test_numpy_array(self): np = pytest.importorskip("numpy") actual = np.array([1 + 1e-7, 2 + 1e-8]) expected = np.array([1, 2]) # Return false if any element is outside the tolerance. assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == expected assert approx(expected, rel=5e-8, abs=0) != actual # Should be able to compare lists with numpy arrays. assert list(actual) == approx(expected, rel=5e-7, abs=0) assert list(actual) != approx(expected, rel=5e-8, abs=0) assert actual == approx(list(expected), rel=5e-7, abs=0) assert actual != approx(list(expected), rel=5e-8, abs=0) def test_numpy_tolerance_args(self): """ Check that numpy rel/abs args are handled correctly for comparison against an np.array Check both sides of the operator, hopefully it doesn't impact things. Test all permutations of where the approx and np.array() can show up """ np = pytest.importorskip("numpy") expected = 100.0 actual = 99.0 abs_diff = expected - actual rel_diff = (expected - actual) / expected tests = [ (eq, abs_diff, 0), (eq, 0, rel_diff), (ne, 0, rel_diff / 2.0), # rel diff fail (ne, abs_diff / 2.0, 0), # abs diff fail ] for op, _abs, _rel in tests: assert op(np.array(actual), approx(expected, abs=_abs, rel=_rel)) # a, b assert op(approx(expected, abs=_abs, rel=_rel), np.array(actual)) # b, a assert op(actual, approx(np.array(expected), abs=_abs, rel=_rel)) # a, b assert op(approx(np.array(expected), abs=_abs, rel=_rel), actual) # b, a assert op(np.array(actual), approx(np.array(expected), abs=_abs, rel=_rel)) assert op(approx(np.array(expected), abs=_abs, rel=_rel), np.array(actual)) def test_numpy_expecting_nan(self): np = pytest.importorskip("numpy") examples = [ (eq, nan, nan), (eq, -nan, -nan), (eq, nan, -nan), (ne, 0.0, nan), (ne, inf, nan), ] for op, a, x in examples: # Nothing is equal to NaN by default. assert np.array(a) != approx(x) assert a != approx(np.array(x)) # If ``nan_ok=True``, then NaN is equal to NaN. assert op(np.array(a), approx(x, nan_ok=True)) assert op(a, approx(np.array(x), nan_ok=True)) def test_numpy_expecting_inf(self): np = pytest.importorskip("numpy") examples = [ (eq, inf, inf), (eq, -inf, -inf), (ne, inf, -inf), (ne, 0.0, inf), (ne, nan, inf), ] for op, a, x in examples: assert op(np.array(a), approx(x)) assert op(a, approx(np.array(x))) assert op(np.array(a), approx(np.array(x))) def test_numpy_array_wrong_shape(self): np = pytest.importorskip("numpy") a12 = np.array([[1, 2]]) a21 = np.array([[1], [2]]) assert a12 != approx(a21) assert a21 != approx(a12) def test_numpy_array_implicit_conversion(self): np = pytest.importorskip("numpy") class ImplicitArray: """Type which is implicitly convertible to a numpy array.""" def __init__(self, vals): self.vals = vals def __array__(self, dtype=None, copy=None): return np.array(self.vals) vec1 = ImplicitArray([1.0, 2.0, 3.0]) vec2 = ImplicitArray([1.0, 2.0, 4.0]) # see issue #12114 for test case assert vec1 != approx(vec2) def test_numpy_array_protocol(self): """ array-like objects such as tensorflow's DeviceArray are handled like ndarray. See issue #8132 """ np = pytest.importorskip("numpy") class DeviceArray: def __init__(self, value, size): self.value = value self.size = size def __array__(self): return self.value * np.ones(self.size) class DeviceScalar: def __init__(self, value): self.value = value def __array__(self): return np.array(self.value) expected = 1 actual = 1 + 1e-6 assert approx(expected) == DeviceArray(actual, size=1) assert approx(expected) == DeviceArray(actual, size=2) assert approx(expected) == DeviceScalar(actual) assert approx(DeviceScalar(expected)) == actual assert approx(DeviceScalar(expected)) == DeviceScalar(actual) def test_doctests(self, mocked_doctest_runner) -> None: import doctest parser = doctest.DocTestParser() assert approx.__doc__ is not None test = parser.get_doctest( approx.__doc__, {"approx": approx}, approx.__name__, None, None ) mocked_doctest_runner.run(test) def test_unicode_plus_minus(self, pytester: Pytester) -> None: """ Comparing approx instances inside lists should not produce an error in the detailed diff. Integration test for issue #2111. """ pytester.makepyfile( """ import pytest def test_foo(): assert [3] == [pytest.approx(4)] """ ) expected = "4.0e-06" result = pytester.runpytest() result.stdout.fnmatch_lines( [f"*At index 0 diff: 3 != 4 ± {expected}", "=* 1 failed in *="] ) @pytest.mark.parametrize( "x, name", [ pytest.param([[1]], "data structures", id="nested-list"), pytest.param({"key": {"key": 1}}, "dictionaries", id="nested-dict"), ], ) def test_expected_value_type_error(self, x, name): with pytest.raises( TypeError, match=rf"pytest.approx\(\) does not support nested {name}:", ): approx(x) @pytest.mark.parametrize( "x", [ pytest.param(None), pytest.param("string"), pytest.param(["string"], id="nested-str"), pytest.param({"key": "string"}, id="dict-with-string"), ], ) def test_nonnumeric_okay_if_equal(self, x): assert x == approx(x) @pytest.mark.parametrize( "x", [ pytest.param("string"), pytest.param(["string"], id="nested-str"), pytest.param({"key": "string"}, id="dict-with-string"), ], ) def test_nonnumeric_false_if_unequal(self, x): """For non-numeric types, x != pytest.approx(y) reduces to x != y""" assert "ab" != approx("abc") assert ["ab"] != approx(["abc"]) # in particular, both of these should return False assert {"a": 1.0} != approx({"a": None}) assert {"a": None} != approx({"a": 1.0}) assert 1.0 != approx(None) assert None != approx(1.0) # noqa: E711 assert 1.0 != approx([None]) assert None != approx([1.0]) # noqa: E711 def test_nonnumeric_dict_repr(self): """Dicts with non-numerics and infinites have no tolerances""" x1 = {"foo": 1.0000005, "bar": None, "foobar": inf} assert ( repr(approx(x1)) == "approx({'foo': 1.0000005 ± 1.0e-06, 'bar': None, 'foobar': inf})" ) def test_nonnumeric_list_repr(self): """Lists with non-numerics and infinites have no tolerances""" x1 = [1.0000005, None, inf] assert repr(approx(x1)) == "approx([1.0000005 ± 1.0e-06, None, inf])" @pytest.mark.parametrize( "op", [ pytest.param(operator.le, id="<="), pytest.param(operator.lt, id="<"), pytest.param(operator.ge, id=">="), pytest.param(operator.gt, id=">"), ], ) def test_comparison_operator_type_error(self, op): """pytest.approx should raise TypeError for operators other than == and != (#2003).""" with pytest.raises(TypeError): op(1, approx(1, rel=1e-6, abs=1e-12)) def test_numpy_array_with_scalar(self): np = pytest.importorskip("numpy") actual = np.array([1 + 1e-7, 1 - 1e-8]) expected = 1.0 assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual def test_numpy_scalar_with_array(self): np = pytest.importorskip("numpy") actual = 1.0 expected = np.array([1 + 1e-7, 1 - 1e-8]) assert actual == approx(expected, rel=5e-7, abs=0) assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual def test_generic_ordered_sequence(self): class MySequence: def __getitem__(self, i): return [1, 2, 3, 4][i] def __len__(self): return 4 expected = MySequence() assert [1, 2, 3, 4] == approx(expected, abs=1e-4) expected_repr = "approx([1 ± 1.0e-06, 2 ± 2.0e-06, 3 ± 3.0e-06, 4 ± 4.0e-06])" assert repr(approx(expected)) == expected_repr def test_decimal_approx_repr(self, monkeypatch) -> None: monkeypatch.setitem(decimal.getcontext().traps, decimal.FloatOperation, True) approx_obj = pytest.approx(decimal.Decimal("2.60")) assert decimal.Decimal("2.600001") == approx_obj def test_decimal_approx_float_rel(self) -> None: approx_obj = pytest.approx(decimal.Decimal("2.60"), rel=0.01) assert decimal.Decimal("2.600001") == approx_obj assert repr(approx_obj) == "2.60 ± 1.0e-2" def test_decimal_approx_float_abs(self) -> None: approx_obj = pytest.approx(decimal.Decimal("2.60"), abs=0.01) assert decimal.Decimal("2.600001") == approx_obj assert repr(approx_obj) == "2.60 ± 1.0e-2" def test_allow_ordered_sequences_only(self) -> None: """pytest.approx() should raise an error on unordered sequences (#9692).""" with pytest.raises(TypeError, match="only supports ordered sequences"): assert {1, 2, 3} == approx({1, 2, 3}) def test_strange_sequence(self): """https://github.com/pytest-dev/pytest/issues/11797""" a = MyVec3(1, 2, 3) b = MyVec3(0, 1, 2) # this would trigger the error inside the test pytest.approx(a, abs=0.5)._repr_compare(b) assert b == pytest.approx(a, abs=2) assert b != pytest.approx(a, abs=0.5) def test_approx_dicts_with_mismatch_on_keys(self) -> None: """https://github.com/pytest-dev/pytest/issues/13816""" expected = {"a": 1, "b": 3} actual = {"a": 1, "c": 3} with pytest.raises( AssertionError, match=re.escape( "comparison failed.\n Mappings has different keys: " "expected dict_keys(['a', 'b']) but got dict_keys(['a', 'c'])" ), ): assert actual == approx(expected) def test_approx_on_unordered_mapping_with_mismatch( self, pytester: Pytester ) -> None: """https://github.com/pytest-dev/pytest/issues/12444""" pytester.makepyfile( """ import pytest def test_approx_on_unordered_mapping_with_mismatch(): expected = {"a": 1, "b": 2, "c": 3, "d": 4} actual = {"d": 4, "c": 5, "a": 8, "b": 2} assert actual == pytest.approx(expected) """ ) result = pytester.runpytest() result.assert_outcomes(failed=1) result.stdout.fnmatch_lines( [ "*comparison failed.**Mismatched elements: 2 / 4:*", "*Max absolute difference: 7*", "*Index | Obtained | Expected *", "* a * | 8 * | 1 *", "* c * | 5 * | 3 *", ] ) def test_approx_on_unordered_mapping_matching(self, pytester: Pytester) -> None: """https://github.com/pytest-dev/pytest/issues/12444""" pytester.makepyfile( """ import pytest def test_approx_on_unordered_mapping_matching(): expected = {"a": 1, "b": 2, "c": 3, "d": 4} actual = {"d": 4, "c": 3, "a": 1, "b": 2} assert actual == pytest.approx(expected) """ ) result = pytester.runpytest() result.assert_outcomes(passed=1) def test_assertion_rewriting_works_with_approx_on_lhs( self, pytestconfig: pytest.Config ) -> None: """Assertion rewriting works also when approx() is on the left-hand side.""" with temporary_verbosity(pytestconfig, verbosity=0): with pytest.raises(AssertionError) as e: assert pytest.approx(1) == 2 obtained_message = str(e.value).splitlines()[-2:] assert obtained_message == [ " Obtained: 2", " Expected: 1 ± 1.0e-06", ] def test_scalar_rel_type_validation(self) -> None: with pytest.raises( TypeError, match=r"relative tolerance for a scalar value must" ): pytest.approx(0, rel=datetime.timedelta(1)) def test_scalar_rel_abs_expected_validation(self) -> None: with pytest.raises( TypeError, match=re.escape("expected value must support abs(...) when relative"), ): pytest.approx(object(), rel=1) def test_scalar_abs_type_validation(self) -> None: with pytest.raises( TypeError, match=r"absolute tolerance for a scalar value must" ): pytest.approx(0, abs=datetime.timedelta(1)) class TestApproxDatetime: """Tests for datetime/timedelta support in approx (issue #8395).""" def test_datetime_exactly_equal(self): from datetime import datetime from datetime import timedelta dt = datetime(2024, 1, 1, 12, 0, 0) assert dt == approx(dt, abs=timedelta(seconds=1)) def test_datetime_within_tolerance(self): from datetime import datetime from datetime import timedelta dt1 = datetime(2024, 1, 1, 12, 0, 0) dt2 = datetime(2024, 1, 1, 12, 0, 0, 500000) # +0.5s assert dt1 == approx(dt2, abs=timedelta(seconds=1)) def test_datetime_outside_tolerance(self): from datetime import datetime from datetime import timedelta dt1 = datetime(2024, 1, 1, 12, 0, 0) dt2 = datetime(2024, 1, 1, 12, 0, 2) # +2s assert dt1 != approx(dt2, abs=timedelta(seconds=1)) def test_datetime_negative_difference(self): from datetime import datetime from datetime import timedelta dt1 = datetime(2024, 1, 1, 12, 0, 1) dt2 = datetime(2024, 1, 1, 12, 0, 0) # dt2 < dt1 assert dt1 == approx(dt2, abs=timedelta(seconds=2)) assert dt1 != approx(dt2, abs=timedelta(milliseconds=500)) def test_timedelta_within_tolerance(self): from datetime import timedelta td1 = timedelta(seconds=100) td2 = timedelta(seconds=100.5) assert td1 == approx(td2, abs=timedelta(seconds=1)) def test_timedelta_outside_tolerance(self): from datetime import timedelta td1 = timedelta(seconds=100) td2 = timedelta(seconds=102) assert td1 != approx(td2, abs=timedelta(seconds=1)) def test_timedelta_rel_within_tolerance(self): from datetime import timedelta td1 = timedelta(seconds=100) td2 = timedelta(seconds=100.5) assert td1 == approx(td2, rel=0.01) def test_timedelta_rel_outside_tolerance(self): from datetime import timedelta td1 = timedelta(seconds=100) td2 = timedelta(seconds=102) assert td1 != approx(td2, rel=0.01) def test_requires_tolerance(self): from datetime import datetime with pytest.raises(TypeError, match="requires an explicit tolerance"): approx(datetime(2024, 1, 1)) def test_datetime_rejects_rel(self): from datetime import datetime from datetime import timedelta with pytest.raises(TypeError, match="does not support relative tolerance"): approx(datetime(2024, 1, 1), rel=0.1, abs=timedelta(seconds=1)) with pytest.raises(TypeError, match="does not support relative tolerance"): approx(datetime(2024, 1, 1), rel=timedelta(seconds=1)) def test_abs_must_be_timedelta(self): from datetime import datetime with pytest.raises(TypeError, match="must be a timedelta"): approx(datetime(2024, 1, 1), abs=1.0) def test_timedelta_rel_must_be_number(self): from datetime import timedelta with pytest.raises(TypeError, match="must be a number"): approx(timedelta(seconds=1), rel=timedelta(seconds=1)) def test_timedelta_rel_must_be_non_negative(self): from datetime import timedelta with pytest.raises(ValueError, match="relative tolerance can't be negative"): approx(timedelta(seconds=1), rel=-0.1) def test_timedelta_rel_must_not_be_nan(self): from datetime import timedelta with pytest.raises(ValueError, match="relative tolerance can't be NaN"): approx(timedelta(seconds=1), rel=float("nan")) def test_timedelta_rel_must_not_be_infinite(self): from datetime import timedelta with pytest.raises(ValueError, match="relative tolerance can't be infinite"): approx(timedelta(seconds=1), rel=inf) def test_timedelta_abs_must_be_non_negative(self): from datetime import timedelta with pytest.raises(ValueError, match="absolute tolerance can't be negative"): approx(timedelta(seconds=1), abs=timedelta(seconds=-1)) def test_timedelta_rel_with_abs(self): from datetime import timedelta # rel=0.05 gives 5s tolerance, abs=timedelta(seconds=1) gives 1s. # max(1s, 5s) = 5s tolerance. td1 = timedelta(seconds=100) td2 = timedelta(seconds=104) assert td1 == approx(td2, rel=0.05, abs=timedelta(seconds=1)) def test_timedelta_rel_zero(self): from datetime import timedelta # rel=0 means exact match required (0 * expected = 0) td1 = timedelta(seconds=100) assert td1 == approx(td1, rel=0.0, abs=timedelta(seconds=0)) assert td1 != approx(timedelta(seconds=101), rel=0.0, abs=timedelta(seconds=0)) def test_timedelta_rel_scales_with_expected(self): from datetime import timedelta # Same rel=0.1, but different expected values. # 10% of 100s = 10s, 10% of 200s = 20s. assert timedelta(seconds=109) == approx(timedelta(seconds=100), rel=0.1) assert timedelta(seconds=218) == approx(timedelta(seconds=200), rel=0.1) # 11s is > 10% of 100s, but < 10% of 200s assert timedelta(seconds=111) != approx(timedelta(seconds=100), rel=0.1) assert timedelta(seconds=211) == approx(timedelta(seconds=200), rel=0.1) def test_rejects_nan_ok(self): from datetime import datetime from datetime import timedelta with pytest.raises(TypeError, match="does not support nan_ok"): approx(datetime(2024, 1, 1), abs=timedelta(seconds=1), nan_ok=True) def test_datetime_repr(self): from datetime import datetime from datetime import timedelta dt = datetime(2024, 1, 1, 12, 0, 0) result = repr(approx(dt, abs=timedelta(seconds=1))) assert "2024-01-01 12:00:00" in result assert "0:00:01" in result def test_timedelta_repr(self): from datetime import timedelta td = timedelta(seconds=100) result = repr(approx(td, abs=timedelta(seconds=1))) assert "0:01:40" in result # 100 seconds assert "0:00:01" in result # 1 second tolerance def test_datetime_symmetry(self): """Approx comparison should work on both sides of ==.""" from datetime import datetime from datetime import timedelta dt1 = datetime(2024, 1, 1, 12, 0, 0) dt2 = datetime(2024, 1, 1, 12, 0, 0, 500000) tol = timedelta(seconds=1) assert dt1 == approx(dt2, abs=tol) assert approx(dt2, abs=tol) == dt1 def test_datetime_ne_operator(self): from datetime import datetime from datetime import timedelta dt1 = datetime(2024, 1, 1, 12, 0, 0) dt2 = datetime(2024, 1, 1, 12, 0, 5) tol = timedelta(seconds=1) assert dt1 != approx(dt2, abs=tol) assert not (dt1 == approx(dt2, abs=tol)) def test_datetime_with_timezone(self): from datetime import datetime from datetime import timedelta from datetime import timezone tz = timezone.utc dt1 = datetime(2024, 1, 1, 12, 0, 0, tzinfo=tz) dt2 = datetime(2024, 1, 1, 12, 0, 0, 500000, tzinfo=tz) assert dt1 == approx(dt2, abs=timedelta(seconds=1)) def test_datetime_error_message(self): from datetime import datetime from datetime import timedelta dt1 = datetime(2024, 1, 1, 12, 0, 0) dt2 = datetime(2024, 1, 1, 12, 0, 5) # 5 seconds off with pytest.raises(AssertionError, match="comparison failed"): assert dt1 == approx(dt2, abs=timedelta(seconds=1)) def test_timedelta_zero(self): from datetime import timedelta td1 = timedelta(seconds=0) td2 = timedelta(seconds=0) assert td1 == approx(td2, abs=timedelta(seconds=1)) def test_datetime_boundary_exact(self): """Test that values exactly at the tolerance boundary are equal.""" from datetime import datetime from datetime import timedelta dt1 = datetime(2024, 1, 1, 12, 0, 0) dt2 = datetime(2024, 1, 1, 12, 0, 1) # exactly 1 second assert dt1 == approx(dt2, abs=timedelta(seconds=1)) def test_datetime_microsecond_tolerance(self): from datetime import datetime from datetime import timedelta dt1 = datetime(2024, 1, 1, 12, 0, 0, 0) dt2 = datetime(2024, 1, 1, 12, 0, 0, 100) # +100 microseconds assert dt1 == approx(dt2, abs=timedelta(microseconds=200)) assert dt1 != approx(dt2, abs=timedelta(microseconds=50)) def test_bool_context_raises(self): from datetime import datetime from datetime import timedelta with pytest.raises(AssertionError, match="boolean context"): bool(approx(datetime(2024, 1, 1), abs=timedelta(seconds=1))) def test_wrong_type_comparison(self): """Comparing a datetime approx with a non-datetime should return False.""" from datetime import datetime from datetime import timedelta assert 42 != approx(datetime(2024, 1, 1), abs=timedelta(seconds=1)) assert "string" != approx(datetime(2024, 1, 1), abs=timedelta(seconds=1)) def test_yield_comparisons(self): """Test that _yield_comparisons yields (actual, expected) pairs.""" from datetime import datetime from datetime import timedelta dt = datetime(2024, 1, 1, 12, 0, 0) a = approx(dt, abs=timedelta(seconds=1)) actual = datetime(2024, 1, 1, 12, 0, 0, 500000) pairs = list(a._yield_comparisons(actual)) assert pairs == [(actual, dt)] def test_repr_compare_with_incompatible_type(self): """_repr_compare handles TypeError when actual is not a datetime.""" from datetime import datetime from datetime import timedelta a = approx(datetime(2024, 1, 1), abs=timedelta(seconds=1)) result = a._repr_compare("not a datetime") assert "comparison failed" in result[0] assert "N/A" in result[3] def test_timedelta_in_sequence(self): from datetime import timedelta assert [timedelta(seconds=105)] == approx([timedelta(seconds=100)], rel=0.05) assert [timedelta(seconds=110)] != approx([timedelta(seconds=100)], rel=0.05) assert [timedelta(seconds=105)] == approx( [timedelta(seconds=100)], abs=timedelta(seconds=10) ) def test_timedelta_in_mapping(self): from datetime import timedelta assert {"x": timedelta(seconds=105)} == approx( {"x": timedelta(seconds=100)}, rel=0.05 ) assert {"x": timedelta(seconds=110)} != approx( {"x": timedelta(seconds=100)}, rel=0.05 ) assert {"x": timedelta(seconds=105)} == approx( {"x": timedelta(seconds=100)}, abs=timedelta(seconds=10) ) def test_datetime_in_sequence(self): from datetime import datetime from datetime import timedelta assert [datetime(2024, 1, 1, 12, 0, 0, 500_000)] == approx( [datetime(2024, 1, 1, 12, 0, 0)], abs=timedelta(seconds=1) ) assert [datetime(2024, 1, 1, 12, 0, 5)] != approx( [datetime(2024, 1, 1, 12, 0, 0)], abs=timedelta(seconds=1) ) def test_datetime_in_mapping(self): from datetime import datetime from datetime import timedelta assert {"t": datetime(2024, 1, 1, 12, 0, 0, 500_000)} == approx( {"t": datetime(2024, 1, 1, 12, 0, 0)}, abs=timedelta(seconds=1) ) assert {"t": datetime(2024, 1, 1, 12, 0, 5)} != approx( {"t": datetime(2024, 1, 1, 12, 0, 0)}, abs=timedelta(seconds=1) ) class MyVec3: # incomplete """sequence like""" _x: int _y: int _z: int def __init__(self, x: int, y: int, z: int): self._x, self._y, self._z = x, y, z def __repr__(self) -> str: return f"<MyVec3 {self._x} {self._y} {self._z}>" def __len__(self) -> int: return 3 def __getitem__(self, key: int) -> int: if key == 0: return self._x if key == 1: return self._y if key == 2: return self._z raise IndexError(key) class TestRecursiveSequenceMap: def test_map_over_scalar(self): assert _recursive_sequence_map(sqrt, 16) == 4 def test_map_over_empty_list(self): assert _recursive_sequence_map(sqrt, []) == [] def test_map_over_list(self): assert _recursive_sequence_map(sqrt, [4, 16, 25, 676]) == [2, 4, 5, 26] def test_map_over_tuple(self): assert _recursive_sequence_map(sqrt, (4, 16, 25, 676)) == (2, 4, 5, 26) def test_map_over_nested_lists(self): assert _recursive_sequence_map(sqrt, [4, [25, 64], [[49]]]) == [ 2, [5, 8], [[7]], ] def test_map_over_mixed_sequence(self): assert _recursive_sequence_map(sqrt, [4, (25, 64), [49]]) == [ 2, (5, 8), [7], ] def test_map_over_sequence_like(self): assert _recursive_sequence_map(int, MyVec3(1, 2, 3)) == [1, 2, 3]