1
1
import math
2
2
import sys
3
+ from numbers import Number
4
+ from decimal import Decimal
3
5
4
6
import py
5
7
from six .moves import zip , filterfalse
@@ -29,6 +31,9 @@ def _cmp_raises_type_error(self, other):
29
31
"Comparison operators other than == and != not supported by approx objects"
30
32
)
31
33
34
+ def _non_numeric_type_error (value ):
35
+ return TypeError ("cannot make approximate comparisons to non-numeric values, e.g. {}" .format (value ))
36
+
32
37
33
38
# builtin pytest.approx helper
34
39
@@ -39,7 +44,7 @@ class ApproxBase(object):
39
44
or sequences of numbers.
40
45
"""
41
46
42
- # Tell numpy to use our `__eq__` operator instead of its
47
+ # Tell numpy to use our `__eq__` operator instead of its.
43
48
__array_ufunc__ = None
44
49
__array_priority__ = 100
45
50
@@ -48,6 +53,7 @@ def __init__(self, expected, rel=None, abs=None, nan_ok=False):
48
53
self .abs = abs
49
54
self .rel = rel
50
55
self .nan_ok = nan_ok
56
+ self ._check_type ()
51
57
52
58
def __repr__ (self ):
53
59
raise NotImplementedError
@@ -75,6 +81,17 @@ def _yield_comparisons(self, actual):
75
81
"""
76
82
raise NotImplementedError
77
83
84
+ def _check_type (self ):
85
+ """
86
+ Raise a TypeError if the expected value is not a valid type.
87
+ """
88
+ # This is only a concern if the expected value is a sequence. In every
89
+ # other case, the approx() function ensures that the expected value has
90
+ # a numeric type. For this reason, the default is to do nothing. The
91
+ # classes that deal with sequences should reimplement this method to
92
+ # raise if there are any non-numeric elements in the sequence.
93
+ pass
94
+
78
95
79
96
class ApproxNumpy (ApproxBase ):
80
97
"""
@@ -151,6 +168,13 @@ def _yield_comparisons(self, actual):
151
168
for k in self .expected .keys ():
152
169
yield actual [k ], self .expected [k ]
153
170
171
+ def _check_type (self ):
172
+ for x in self .expected .values ():
173
+ if isinstance (x , type (self .expected )):
174
+ raise TypeError ("pytest.approx() does not support nested dictionaries, e.g. {}" .format (self .expected ))
175
+ elif not isinstance (x , Number ):
176
+ raise _non_numeric_type_error (self .expected )
177
+
154
178
155
179
class ApproxSequence (ApproxBase ):
156
180
"""
@@ -174,6 +198,13 @@ def __eq__(self, actual):
174
198
def _yield_comparisons (self , actual ):
175
199
return zip (actual , self .expected )
176
200
201
+ def _check_type (self ):
202
+ for x in self .expected :
203
+ if isinstance (x , type (self .expected )):
204
+ raise TypeError ("pytest.approx() does not support nested data structures, e.g. {}" .format (self .expected ))
205
+ elif not isinstance (x , Number ):
206
+ raise _non_numeric_type_error (self .expected )
207
+
177
208
178
209
class ApproxScalar (ApproxBase ):
179
210
"""
@@ -294,8 +325,6 @@ class ApproxDecimal(ApproxScalar):
294
325
"""
295
326
Perform approximate comparisons where the expected value is a decimal.
296
327
"""
297
- from decimal import Decimal
298
-
299
328
DEFAULT_ABSOLUTE_TOLERANCE = Decimal ("1e-12" )
300
329
DEFAULT_RELATIVE_TOLERANCE = Decimal ("1e-6" )
301
330
@@ -453,32 +482,33 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
453
482
__ https://docs.python.org/3/reference/datamodel.html#object.__ge__
454
483
"""
455
484
456
- from decimal import Decimal
457
-
458
485
# Delegate the comparison to a class that knows how to deal with the type
459
486
# of the expected value (e.g. int, float, list, dict, numpy.array, etc).
460
487
#
461
- # This architecture is really driven by the need to support numpy arrays.
462
- # The only way to override `==` for arrays without requiring that approx be
463
- # the left operand is to inherit the approx object from `numpy.ndarray`.
464
- # But that can't be a general solution, because it requires (1) numpy to be
465
- # installed and (2) the expected value to be a numpy array. So the general
466
- # solution is to delegate each type of expected value to a different class.
488
+ # The primary responsibility of these classes is to implement ``__eq__()``
489
+ # and ``__repr__()``. The former is used to actually check if some
490
+ # "actual" value is equivalent to the given expected value within the
491
+ # allowed tolerance. The latter is used to show the user the expected
492
+ # value and tolerance, in the case that a test failed.
467
493
#
468
- # This has the advantage that it made it easy to support mapping types
469
- # (i.e. dict). The old code accepted mapping types, but would only compare
470
- # their keys, which is probably not what most people would expect.
494
+ # The actual logic for making approximate comparisons can be found in
495
+ # ApproxScalar, which is used to compare individual numbers. All of the
496
+ # other Approx classes eventually delegate to this class. The ApproxBase
497
+ # class provides some convenient methods and overloads, but isn't really
498
+ # essential.
471
499
472
- if _is_numpy_array (expected ):
473
- cls = ApproxNumpy
500
+ if isinstance (expected , Decimal ):
501
+ cls = ApproxDecimal
502
+ elif isinstance (expected , Number ):
503
+ cls = ApproxScalar
474
504
elif isinstance (expected , Mapping ):
475
505
cls = ApproxMapping
476
506
elif isinstance (expected , Sequence ) and not isinstance (expected , STRING_TYPES ):
477
507
cls = ApproxSequence
478
- elif isinstance (expected , Decimal ):
479
- cls = ApproxDecimal
508
+ elif _is_numpy_array (expected ):
509
+ cls = ApproxNumpy
480
510
else :
481
- cls = ApproxScalar
511
+ raise _non_numeric_type_error ( expected )
482
512
483
513
return cls (expected , rel , abs , nan_ok )
484
514
0 commit comments