4
4
from collections .abc import Mapping
5
5
from collections .abc import Sized
6
6
from decimal import Decimal
7
- from numbers import Number
7
+ from numbers import Complex
8
8
from types import TracebackType
9
9
from typing import Any
10
10
from typing import Callable
@@ -146,7 +146,10 @@ def __repr__(self) -> str:
146
146
)
147
147
148
148
def __eq__ (self , actual ) -> bool :
149
- if set (actual .keys ()) != set (self .expected .keys ()):
149
+ try :
150
+ if set (actual .keys ()) != set (self .expected .keys ()):
151
+ return False
152
+ except AttributeError :
150
153
return False
151
154
152
155
return ApproxBase .__eq__ (self , actual )
@@ -161,8 +164,6 @@ def _check_type(self) -> None:
161
164
if isinstance (value , type (self .expected )):
162
165
msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
163
166
raise TypeError (msg .format (key , value , pprint .pformat (self .expected )))
164
- elif not isinstance (value , Number ):
165
- raise _non_numeric_type_error (self .expected , at = "key={!r}" .format (key ))
166
167
167
168
168
169
class ApproxSequencelike (ApproxBase ):
@@ -177,7 +178,10 @@ def __repr__(self) -> str:
177
178
)
178
179
179
180
def __eq__ (self , actual ) -> bool :
180
- if len (actual ) != len (self .expected ):
181
+ try :
182
+ if len (actual ) != len (self .expected ):
183
+ return False
184
+ except TypeError :
181
185
return False
182
186
return ApproxBase .__eq__ (self , actual )
183
187
@@ -190,10 +194,6 @@ def _check_type(self) -> None:
190
194
if isinstance (x , type (self .expected )):
191
195
msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
192
196
raise TypeError (msg .format (x , index , pprint .pformat (self .expected )))
193
- elif not isinstance (x , Number ):
194
- raise _non_numeric_type_error (
195
- self .expected , at = "index {}" .format (index )
196
- )
197
197
198
198
199
199
class ApproxScalar (ApproxBase ):
@@ -211,16 +211,23 @@ def __repr__(self) -> str:
211
211
For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
212
212
"""
213
213
214
- # Infinities aren't compared using tolerances, so don't show a
215
- # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j).
216
- if math .isinf (abs (self .expected )):
214
+ # Don't show a tolerance for values that aren't compared using
215
+ # tolerances, i.e. non-numerics and infinities. Need to call abs to
216
+ # handle complex numbers, e.g. (inf + 1j).
217
+ if (not isinstance (self .expected , (Complex , Decimal ))) or math .isinf (
218
+ abs (self .expected )
219
+ ):
217
220
return str (self .expected )
218
221
219
222
# If a sensible tolerance can't be calculated, self.tolerance will
220
223
# raise a ValueError. In this case, display '???'.
221
224
try :
222
225
vetted_tolerance = "{:.1e}" .format (self .tolerance )
223
- if isinstance (self .expected , complex ) and not math .isinf (self .tolerance ):
226
+ if (
227
+ isinstance (self .expected , Complex )
228
+ and self .expected .imag
229
+ and not math .isinf (self .tolerance )
230
+ ):
224
231
vetted_tolerance += " ∠ ±180°"
225
232
except ValueError :
226
233
vetted_tolerance = "???"
@@ -239,6 +246,15 @@ def __eq__(self, actual) -> bool:
239
246
if actual == self .expected :
240
247
return True
241
248
249
+ # If either type is non-numeric, fall back to strict equality.
250
+ # NB: we need Complex, rather than just Number, to ensure that __abs__,
251
+ # __sub__, and __float__ are defined.
252
+ if not (
253
+ isinstance (self .expected , (Complex , Decimal ))
254
+ and isinstance (actual , (Complex , Decimal ))
255
+ ):
256
+ return False
257
+
242
258
# Allow the user to control whether NaNs are considered equal to each
243
259
# other or not. The abs() calls are for compatibility with complex
244
260
# numbers.
@@ -409,6 +425,18 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
409
425
>>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
410
426
True
411
427
428
+ You can also use ``approx`` to compare nonnumeric types, or dicts and
429
+ sequences containing nonnumeric types, in which case it falls back to
430
+ strict equality. This can be useful for comparing dicts and sequences that
431
+ can contain optional values::
432
+
433
+ >>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None})
434
+ True
435
+ >>> [None, 1.0000005] == approx([None,1])
436
+ True
437
+ >>> ["foo", 1.0000005] == approx([None,1])
438
+ False
439
+
412
440
If you're thinking about using ``approx``, then you might want to know how
413
441
it compares to other good ways of comparing floating-point numbers. All of
414
442
these algorithms are based on relative and absolute tolerances and should
@@ -466,6 +494,14 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
466
494
follows a fixed behavior. `More information...`__
467
495
468
496
__ https://docs.python.org/3/reference/datamodel.html#object.__ge__
497
+
498
+ .. versionchanged:: 3.7.1
499
+ ``approx`` raises ``TypeError`` when it encounters a dict value or
500
+ sequence element of nonnumeric type.
501
+
502
+ .. versionchanged:: 6.1.0
503
+ ``approx`` falls back to strict equality for nonnumeric types instead
504
+ of raising ``TypeError``.
469
505
"""
470
506
471
507
# Delegate the comparison to a class that knows how to deal with the type
@@ -487,8 +523,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
487
523
488
524
if isinstance (expected , Decimal ):
489
525
cls = ApproxDecimal # type: Type[ApproxBase]
490
- elif isinstance (expected , Number ):
491
- cls = ApproxScalar
492
526
elif isinstance (expected , Mapping ):
493
527
cls = ApproxMapping
494
528
elif _is_numpy_array (expected ):
@@ -501,7 +535,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
501
535
):
502
536
cls = ApproxSequencelike
503
537
else :
504
- raise _non_numeric_type_error ( expected , at = None )
538
+ cls = ApproxScalar
505
539
506
540
return cls (expected , rel , abs , nan_ok )
507
541
0 commit comments