|
1 | 1 | # ruff: noqa |
2 | 2 |
|
3 | 3 | import json |
| 4 | +import math |
4 | 5 | import os |
5 | 6 | import pathlib |
6 | 7 | from typing import Any |
|
34 | 35 |
|
35 | 36 |
|
36 | 37 | class Record: |
| 38 | + @staticmethod |
| 39 | + def _values_approx_equal(a: Any, b: Any, rel_tol: float = 1e-9) -> bool: |
| 40 | + """Recursively compare two JSON-parsed values using approximate |
| 41 | + comparison for floats. Everything else is compared by equality.""" |
| 42 | + if isinstance(a, float) and isinstance(b, float): |
| 43 | + return math.isclose(a, b, rel_tol=rel_tol) |
| 44 | + if isinstance(a, list) and isinstance(b, list): |
| 45 | + if len(a) != len(b): |
| 46 | + return False |
| 47 | + return all( |
| 48 | + Record._values_approx_equal(x, y, rel_tol=rel_tol) |
| 49 | + for x, y in zip(a, b) |
| 50 | + ) |
| 51 | + if isinstance(a, dict) and isinstance(b, dict): |
| 52 | + if a.keys() != b.keys(): |
| 53 | + return False |
| 54 | + return all( |
| 55 | + Record._values_approx_equal(a[k], b[k], rel_tol=rel_tol) |
| 56 | + for k in a |
| 57 | + ) |
| 58 | + return a == b |
| 59 | + |
| 60 | + @staticmethod |
| 61 | + def _json_strings_approx_equal( |
| 62 | + recorded: str, captured: str, rel_tol: float = 1e-9 |
| 63 | + ) -> bool: |
| 64 | + """Parse two JSON strings and compare with float tolerance.""" |
| 65 | + try: |
| 66 | + return Record._values_approx_equal( |
| 67 | + json.loads(recorded), json.loads(captured), rel_tol=rel_tol |
| 68 | + ) |
| 69 | + except (json.JSONDecodeError, ValueError): |
| 70 | + return recorded == captured |
| 71 | + |
37 | 72 | @staticmethod |
38 | 73 | def extract_string(data: Any, **kwargs) -> str: |
39 | 74 | if isinstance(data, tuple(EXTENSIONS_MATCHING["txt"])): |
@@ -74,15 +109,25 @@ def strip(self) -> bool: |
74 | 109 | @property |
75 | 110 | def record_changed(self) -> bool: |
76 | 111 | if self.__recorded is None: |
77 | | - changed = True |
78 | | - elif self.__strip and self.__recorded.strip() != self.__captured.strip(): |
79 | | - changed = True |
80 | | - elif not self.__strip and self.__recorded != self.__captured: |
81 | | - changed = True |
82 | | - else: |
83 | | - changed = False |
| 112 | + return True |
| 113 | + |
| 114 | + recorded = self.__recorded |
| 115 | + captured = self.__captured |
| 116 | + |
| 117 | + if self.__strip: |
| 118 | + recorded = recorded.strip() |
| 119 | + captured = captured.strip() |
| 120 | + |
| 121 | + if recorded == captured: |
| 122 | + return False |
| 123 | + |
| 124 | + # For JSON records, fall back to approximate float comparison so that |
| 125 | + # differences in float string representation (e.g. 0.011710000000000002 |
| 126 | + # vs 0.01171) don't cause spurious failures across Python versions. |
| 127 | + if self.__record_path.endswith(".json"): |
| 128 | + return not self._json_strings_approx_equal(recorded, captured) |
84 | 129 |
|
85 | | - return changed |
| 130 | + return True |
86 | 131 |
|
87 | 132 | @property |
88 | 133 | def record_exists(self) -> bool: |
|
0 commit comments