Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/python_api.py: 45%
337 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1import math
2import pprint
3from collections.abc import Collection
4from collections.abc import Sized
5from decimal import Decimal
6from numbers import Complex
7from types import TracebackType
8from typing import Any
9from typing import Callable
10from typing import cast
11from typing import ContextManager
12from typing import List
13from typing import Mapping
14from typing import Optional
15from typing import Pattern
16from typing import Sequence
17from typing import Tuple
18from typing import Type
19from typing import TYPE_CHECKING
20from typing import TypeVar
21from typing import Union
23if TYPE_CHECKING:
24 from numpy import ndarray
27import _pytest._code
28from _pytest.compat import final
29from _pytest.compat import STRING_TYPES
30from _pytest.compat import overload
31from _pytest.outcomes import fail
34def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
35 at_str = f" at {at}" if at else ""
36 return TypeError(
37 "cannot make approximate comparisons to non-numeric values: {!r} {}".format(
38 value, at_str
39 )
40 )
43def _compare_approx(
44 full_object: object,
45 message_data: Sequence[Tuple[str, str, str]],
46 number_of_elements: int,
47 different_ids: Sequence[object],
48 max_abs_diff: float,
49 max_rel_diff: float,
50) -> List[str]:
51 message_list = list(message_data)
52 message_list.insert(0, ("Index", "Obtained", "Expected"))
53 max_sizes = [0, 0, 0]
54 for index, obtained, expected in message_list:
55 max_sizes[0] = max(max_sizes[0], len(index))
56 max_sizes[1] = max(max_sizes[1], len(obtained))
57 max_sizes[2] = max(max_sizes[2], len(expected))
58 explanation = [
59 f"comparison failed. Mismatched elements: {len(different_ids)} / {number_of_elements}:",
60 f"Max absolute difference: {max_abs_diff}",
61 f"Max relative difference: {max_rel_diff}",
62 ] + [
63 f"{indexes:<{max_sizes[0]}} | {obtained:<{max_sizes[1]}} | {expected:<{max_sizes[2]}}"
64 for indexes, obtained, expected in message_list
65 ]
66 return explanation
69# builtin pytest.approx helper
72class ApproxBase:
73 """Provide shared utilities for making approximate comparisons between
74 numbers or sequences of numbers."""
76 # Tell numpy to use our `__eq__` operator instead of its.
77 __array_ufunc__ = None
78 __array_priority__ = 100
80 def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None:
81 __tracebackhide__ = True
82 self.expected = expected
83 self.abs = abs
84 self.rel = rel
85 self.nan_ok = nan_ok
86 self._check_type()
88 def __repr__(self) -> str:
89 raise NotImplementedError
91 def _repr_compare(self, other_side: Any) -> List[str]:
92 return [
93 "comparison failed",
94 f"Obtained: {other_side}",
95 f"Expected: {self}",
96 ]
98 def __eq__(self, actual) -> bool:
99 return all(
100 a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual)
101 )
103 def __bool__(self):
104 __tracebackhide__ = True
105 raise AssertionError(
106 "approx() is not supported in a boolean context.\nDid you mean: `assert a == approx(b)`?"
107 )
109 # Ignore type because of https://github.com/python/mypy/issues/4266.
110 __hash__ = None # type: ignore
112 def __ne__(self, actual) -> bool:
113 return not (actual == self)
115 def _approx_scalar(self, x) -> "ApproxScalar":
116 if isinstance(x, Decimal):
117 return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
118 return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
120 def _yield_comparisons(self, actual):
121 """Yield all the pairs of numbers to be compared.
123 This is used to implement the `__eq__` method.
124 """
125 raise NotImplementedError
127 def _check_type(self) -> None:
128 """Raise a TypeError if the expected value is not a valid type."""
129 # This is only a concern if the expected value is a sequence. In every
130 # other case, the approx() function ensures that the expected value has
131 # a numeric type. For this reason, the default is to do nothing. The
132 # classes that deal with sequences should reimplement this method to
133 # raise if there are any non-numeric elements in the sequence.
136def _recursive_sequence_map(f, x):
137 """Recursively map a function over a sequence of arbitrary depth"""
138 if isinstance(x, (list, tuple)):
139 seq_type = type(x)
140 return seq_type(_recursive_sequence_map(f, xi) for xi in x)
141 else:
142 return f(x)
145class ApproxNumpy(ApproxBase):
146 """Perform approximate comparisons where the expected value is numpy array."""
148 def __repr__(self) -> str:
149 list_scalars = _recursive_sequence_map(
150 self._approx_scalar, self.expected.tolist()
151 )
152 return f"approx({list_scalars!r})"
154 def _repr_compare(self, other_side: "ndarray") -> List[str]:
155 import itertools
156 import math
158 def get_value_from_nested_list(
159 nested_list: List[Any], nd_index: Tuple[Any, ...]
160 ) -> Any:
161 """
162 Helper function to get the value out of a nested list, given an n-dimensional index.
163 This mimics numpy's indexing, but for raw nested python lists.
164 """
165 value: Any = nested_list
166 for i in nd_index:
167 value = value[i]
168 return value
170 np_array_shape = self.expected.shape
171 approx_side_as_seq = _recursive_sequence_map(
172 self._approx_scalar, self.expected.tolist()
173 )
175 if np_array_shape != other_side.shape:
176 return [
177 "Impossible to compare arrays with different shapes.",
178 f"Shapes: {np_array_shape} and {other_side.shape}",
179 ]
181 number_of_elements = self.expected.size
182 max_abs_diff = -math.inf
183 max_rel_diff = -math.inf
184 different_ids = []
185 for index in itertools.product(*(range(i) for i in np_array_shape)):
186 approx_value = get_value_from_nested_list(approx_side_as_seq, index)
187 other_value = get_value_from_nested_list(other_side, index)
188 if approx_value != other_value:
189 abs_diff = abs(approx_value.expected - other_value)
190 max_abs_diff = max(max_abs_diff, abs_diff)
191 if other_value == 0.0:
192 max_rel_diff = math.inf
193 else:
194 max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
195 different_ids.append(index)
197 message_data = [
198 (
199 str(index),
200 str(get_value_from_nested_list(other_side, index)),
201 str(get_value_from_nested_list(approx_side_as_seq, index)),
202 )
203 for index in different_ids
204 ]
205 return _compare_approx(
206 self.expected,
207 message_data,
208 number_of_elements,
209 different_ids,
210 max_abs_diff,
211 max_rel_diff,
212 )
214 def __eq__(self, actual) -> bool:
215 import numpy as np
217 # self.expected is supposed to always be an array here.
219 if not np.isscalar(actual):
220 try:
221 actual = np.asarray(actual)
222 except Exception as e:
223 raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e
225 if not np.isscalar(actual) and actual.shape != self.expected.shape:
226 return False
228 return super().__eq__(actual)
230 def _yield_comparisons(self, actual):
231 import numpy as np
233 # `actual` can either be a numpy array or a scalar, it is treated in
234 # `__eq__` before being passed to `ApproxBase.__eq__`, which is the
235 # only method that calls this one.
237 if np.isscalar(actual):
238 for i in np.ndindex(self.expected.shape):
239 yield actual, self.expected[i].item()
240 else:
241 for i in np.ndindex(self.expected.shape):
242 yield actual[i].item(), self.expected[i].item()
245class ApproxMapping(ApproxBase):
246 """Perform approximate comparisons where the expected value is a mapping
247 with numeric values (the keys can be anything)."""
249 def __repr__(self) -> str:
250 return "approx({!r})".format(
251 {k: self._approx_scalar(v) for k, v in self.expected.items()}
252 )
254 def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:
255 import math
257 approx_side_as_map = {
258 k: self._approx_scalar(v) for k, v in self.expected.items()
259 }
261 number_of_elements = len(approx_side_as_map)
262 max_abs_diff = -math.inf
263 max_rel_diff = -math.inf
264 different_ids = []
265 for (approx_key, approx_value), other_value in zip(
266 approx_side_as_map.items(), other_side.values()
267 ):
268 if approx_value != other_value:
269 max_abs_diff = max(
270 max_abs_diff, abs(approx_value.expected - other_value)
271 )
272 if approx_value.expected == 0.0:
273 max_rel_diff = math.inf
274 else:
275 max_rel_diff = max(
276 max_rel_diff,
277 abs(
278 (approx_value.expected - other_value)
279 / approx_value.expected
280 ),
281 )
282 different_ids.append(approx_key)
284 message_data = [
285 (str(key), str(other_side[key]), str(approx_side_as_map[key]))
286 for key in different_ids
287 ]
289 return _compare_approx(
290 self.expected,
291 message_data,
292 number_of_elements,
293 different_ids,
294 max_abs_diff,
295 max_rel_diff,
296 )
298 def __eq__(self, actual) -> bool:
299 try:
300 if set(actual.keys()) != set(self.expected.keys()):
301 return False
302 except AttributeError:
303 return False
305 return super().__eq__(actual)
307 def _yield_comparisons(self, actual):
308 for k in self.expected.keys():
309 yield actual[k], self.expected[k]
311 def _check_type(self) -> None:
312 __tracebackhide__ = True
313 for key, value in self.expected.items():
314 if isinstance(value, type(self.expected)):
315 msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
316 raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
319class ApproxSequenceLike(ApproxBase):
320 """Perform approximate comparisons where the expected value is a sequence of numbers."""
322 def __repr__(self) -> str:
323 seq_type = type(self.expected)
324 if seq_type not in (tuple, list):
325 seq_type = list
326 return "approx({!r})".format(
327 seq_type(self._approx_scalar(x) for x in self.expected)
328 )
330 def _repr_compare(self, other_side: Sequence[float]) -> List[str]:
331 import math
333 if len(self.expected) != len(other_side):
334 return [
335 "Impossible to compare lists with different sizes.",
336 f"Lengths: {len(self.expected)} and {len(other_side)}",
337 ]
339 approx_side_as_map = _recursive_sequence_map(self._approx_scalar, self.expected)
341 number_of_elements = len(approx_side_as_map)
342 max_abs_diff = -math.inf
343 max_rel_diff = -math.inf
344 different_ids = []
345 for i, (approx_value, other_value) in enumerate(
346 zip(approx_side_as_map, other_side)
347 ):
348 if approx_value != other_value:
349 abs_diff = abs(approx_value.expected - other_value)
350 max_abs_diff = max(max_abs_diff, abs_diff)
351 if other_value == 0.0:
352 max_rel_diff = math.inf
353 else:
354 max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
355 different_ids.append(i)
357 message_data = [
358 (str(i), str(other_side[i]), str(approx_side_as_map[i]))
359 for i in different_ids
360 ]
362 return _compare_approx(
363 self.expected,
364 message_data,
365 number_of_elements,
366 different_ids,
367 max_abs_diff,
368 max_rel_diff,
369 )
371 def __eq__(self, actual) -> bool:
372 try:
373 if len(actual) != len(self.expected):
374 return False
375 except TypeError:
376 return False
377 return super().__eq__(actual)
379 def _yield_comparisons(self, actual):
380 return zip(actual, self.expected)
382 def _check_type(self) -> None:
383 __tracebackhide__ = True
384 for index, x in enumerate(self.expected):
385 if isinstance(x, type(self.expected)):
386 msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
387 raise TypeError(msg.format(x, index, pprint.pformat(self.expected)))
390class ApproxScalar(ApproxBase):
391 """Perform approximate comparisons where the expected value is a single number."""
393 # Using Real should be better than this Union, but not possible yet:
394 # https://github.com/python/typeshed/pull/3108
395 DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12
396 DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6
398 def __repr__(self) -> str:
399 """Return a string communicating both the expected value and the
400 tolerance for the comparison being made.
402 For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
403 """
404 # Don't show a tolerance for values that aren't compared using
405 # tolerances, i.e. non-numerics and infinities. Need to call abs to
406 # handle complex numbers, e.g. (inf + 1j).
407 if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf(
408 abs(self.expected) # type: ignore[arg-type]
409 ):
410 return str(self.expected)
412 # If a sensible tolerance can't be calculated, self.tolerance will
413 # raise a ValueError. In this case, display '???'.
414 try:
415 vetted_tolerance = f"{self.tolerance:.1e}"
416 if (
417 isinstance(self.expected, Complex)
418 and self.expected.imag
419 and not math.isinf(self.tolerance)
420 ):
421 vetted_tolerance += " ∠ ±180°"
422 except ValueError:
423 vetted_tolerance = "???"
425 return f"{self.expected} ± {vetted_tolerance}"
427 def __eq__(self, actual) -> bool:
428 """Return whether the given value is equal to the expected value
429 within the pre-specified tolerance."""
430 asarray = _as_numpy_array(actual)
431 if asarray is not None:
432 # Call ``__eq__()`` manually to prevent infinite-recursion with
433 # numpy<1.13. See #3748.
434 return all(self.__eq__(a) for a in asarray.flat)
436 # Short-circuit exact equality.
437 if actual == self.expected:
438 return True
440 # If either type is non-numeric, fall back to strict equality.
441 # NB: we need Complex, rather than just Number, to ensure that __abs__,
442 # __sub__, and __float__ are defined.
443 if not (
444 isinstance(self.expected, (Complex, Decimal))
445 and isinstance(actual, (Complex, Decimal))
446 ):
447 return False
449 # Allow the user to control whether NaNs are considered equal to each
450 # other or not. The abs() calls are for compatibility with complex
451 # numbers.
452 if math.isnan(abs(self.expected)): # type: ignore[arg-type]
453 return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type]
455 # Infinity shouldn't be approximately equal to anything but itself, but
456 # if there's a relative tolerance, it will be infinite and infinity
457 # will seem approximately equal to everything. The equal-to-itself
458 # case would have been short circuited above, so here we can just
459 # return false if the expected value is infinite. The abs() call is
460 # for compatibility with complex numbers.
461 if math.isinf(abs(self.expected)): # type: ignore[arg-type]
462 return False
464 # Return true if the two numbers are within the tolerance.
465 result: bool = abs(self.expected - actual) <= self.tolerance
466 return result
468 # Ignore type because of https://github.com/python/mypy/issues/4266.
469 __hash__ = None # type: ignore
471 @property
472 def tolerance(self):
473 """Return the tolerance for the comparison.
475 This could be either an absolute tolerance or a relative tolerance,
476 depending on what the user specified or which would be larger.
477 """
479 def set_default(x, default):
480 return x if x is not None else default
482 # Figure out what the absolute tolerance should be. ``self.abs`` is
483 # either None or a value specified by the user.
484 absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE)
486 if absolute_tolerance < 0:
487 raise ValueError(
488 f"absolute tolerance can't be negative: {absolute_tolerance}"
489 )
490 if math.isnan(absolute_tolerance):
491 raise ValueError("absolute tolerance can't be NaN.")
493 # If the user specified an absolute tolerance but not a relative one,
494 # just return the absolute tolerance.
495 if self.rel is None:
496 if self.abs is not None:
497 return absolute_tolerance
499 # Figure out what the relative tolerance should be. ``self.rel`` is
500 # either None or a value specified by the user. This is done after
501 # we've made sure the user didn't ask for an absolute tolerance only,
502 # because we don't want to raise errors about the relative tolerance if
503 # we aren't even going to use it.
504 relative_tolerance = set_default(
505 self.rel, self.DEFAULT_RELATIVE_TOLERANCE
506 ) * abs(self.expected)
508 if relative_tolerance < 0:
509 raise ValueError(
510 f"relative tolerance can't be negative: {relative_tolerance}"
511 )
512 if math.isnan(relative_tolerance):
513 raise ValueError("relative tolerance can't be NaN.")
515 # Return the larger of the relative and absolute tolerances.
516 return max(relative_tolerance, absolute_tolerance)
519class ApproxDecimal(ApproxScalar):
520 """Perform approximate comparisons where the expected value is a Decimal."""
522 DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
523 DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
526def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
527 """Assert that two numbers (or two ordered sequences of numbers) are equal to each other
528 within some tolerance.
530 Due to the :doc:`python:tutorial/floatingpoint`, numbers that we
531 would intuitively expect to be equal are not always so::
533 >>> 0.1 + 0.2 == 0.3
534 False
536 This problem is commonly encountered when writing tests, e.g. when making
537 sure that floating-point values are what you expect them to be. One way to
538 deal with this problem is to assert that two floating-point numbers are
539 equal to within some appropriate tolerance::
541 >>> abs((0.1 + 0.2) - 0.3) < 1e-6
542 True
544 However, comparisons like this are tedious to write and difficult to
545 understand. Furthermore, absolute comparisons like the one above are
546 usually discouraged because there's no tolerance that works well for all
547 situations. ``1e-6`` is good for numbers around ``1``, but too small for
548 very big numbers and too big for very small ones. It's better to express
549 the tolerance as a fraction of the expected value, but relative comparisons
550 like that are even more difficult to write correctly and concisely.
552 The ``approx`` class performs floating-point comparisons using a syntax
553 that's as intuitive as possible::
555 >>> from pytest import approx
556 >>> 0.1 + 0.2 == approx(0.3)
557 True
559 The same syntax also works for ordered sequences of numbers::
561 >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
562 True
564 ``numpy`` arrays::
566 >>> import numpy as np # doctest: +SKIP
567 >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP
568 True
570 And for a ``numpy`` array against a scalar::
572 >>> import numpy as np # doctest: +SKIP
573 >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
574 True
576 Only ordered sequences are supported, because ``approx`` needs
577 to infer the relative position of the sequences without ambiguity. This means
578 ``sets`` and other unordered sequences are not supported.
580 Finally, dictionary *values* can also be compared::
582 >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
583 True
585 The comparison will be true if both mappings have the same keys and their
586 respective values match the expected tolerances.
588 **Tolerances**
590 By default, ``approx`` considers numbers within a relative tolerance of
591 ``1e-6`` (i.e. one part in a million) of its expected value to be equal.
592 This treatment would lead to surprising results if the expected value was
593 ``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``.
594 To handle this case less surprisingly, ``approx`` also considers numbers
595 within an absolute tolerance of ``1e-12`` of its expected value to be
596 equal. Infinity and NaN are special cases. Infinity is only considered
597 equal to itself, regardless of the relative tolerance. NaN is not
598 considered equal to anything by default, but you can make it be equal to
599 itself by setting the ``nan_ok`` argument to True. (This is meant to
600 facilitate comparing arrays that use NaN to mean "no data".)
602 Both the relative and absolute tolerances can be changed by passing
603 arguments to the ``approx`` constructor::
605 >>> 1.0001 == approx(1)
606 False
607 >>> 1.0001 == approx(1, rel=1e-3)
608 True
609 >>> 1.0001 == approx(1, abs=1e-3)
610 True
612 If you specify ``abs`` but not ``rel``, the comparison will not consider
613 the relative tolerance at all. In other words, two numbers that are within
614 the default relative tolerance of ``1e-6`` will still be considered unequal
615 if they exceed the specified absolute tolerance. If you specify both
616 ``abs`` and ``rel``, the numbers will be considered equal if either
617 tolerance is met::
619 >>> 1 + 1e-8 == approx(1)
620 True
621 >>> 1 + 1e-8 == approx(1, abs=1e-12)
622 False
623 >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
624 True
626 You can also use ``approx`` to compare nonnumeric types, or dicts and
627 sequences containing nonnumeric types, in which case it falls back to
628 strict equality. This can be useful for comparing dicts and sequences that
629 can contain optional values::
631 >>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None})
632 True
633 >>> [None, 1.0000005] == approx([None,1])
634 True
635 >>> ["foo", 1.0000005] == approx([None,1])
636 False
638 If you're thinking about using ``approx``, then you might want to know how
639 it compares to other good ways of comparing floating-point numbers. All of
640 these algorithms are based on relative and absolute tolerances and should
641 agree for the most part, but they do have meaningful differences:
643 - ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative
644 tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute
645 tolerance is met. Because the relative tolerance is calculated w.r.t.
646 both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor
647 ``b`` is a "reference value"). You have to specify an absolute tolerance
648 if you want to compare to ``0.0`` because there is no tolerance by
649 default. More information: :py:func:`math.isclose`.
651 - ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference
652 between ``a`` and ``b`` is less that the sum of the relative tolerance
653 w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance
654 is only calculated w.r.t. ``b``, this test is asymmetric and you can
655 think of ``b`` as the reference value. Support for comparing sequences
656 is provided by :py:func:`numpy.allclose`. More information:
657 :std:doc:`numpy:reference/generated/numpy.isclose`.
659 - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b``
660 are within an absolute tolerance of ``1e-7``. No relative tolerance is
661 considered , so this function is not appropriate for very large or very
662 small numbers. Also, it's only available in subclasses of ``unittest.TestCase``
663 and it's ugly because it doesn't follow PEP8. More information:
664 :py:meth:`unittest.TestCase.assertAlmostEqual`.
666 - ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative
667 tolerance is met w.r.t. ``b`` or if the absolute tolerance is met.
668 Because the relative tolerance is only calculated w.r.t. ``b``, this test
669 is asymmetric and you can think of ``b`` as the reference value. In the
670 special case that you explicitly specify an absolute tolerance but not a
671 relative tolerance, only the absolute tolerance is considered.
673 .. note::
675 ``approx`` can handle numpy arrays, but we recommend the
676 specialised test helpers in :std:doc:`numpy:reference/routines.testing`
677 if you need support for comparisons, NaNs, or ULP-based tolerances.
679 To match strings using regex, you can use
680 `Matches <https://github.com/asottile/re-assert#re_assertmatchespattern-str-args-kwargs>`_
681 from the
682 `re_assert package <https://github.com/asottile/re-assert>`_.
684 .. warning::
686 .. versionchanged:: 3.2
688 In order to avoid inconsistent behavior, :py:exc:`TypeError` is
689 raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons.
690 The example below illustrates the problem::
692 assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10)
693 assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10)
695 In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)``
696 to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to
697 comparison. This is because the call hierarchy of rich comparisons
698 follows a fixed behavior. More information: :py:meth:`object.__ge__`
700 .. versionchanged:: 3.7.1
701 ``approx`` raises ``TypeError`` when it encounters a dict value or
702 sequence element of nonnumeric type.
704 .. versionchanged:: 6.1.0
705 ``approx`` falls back to strict equality for nonnumeric types instead
706 of raising ``TypeError``.
707 """
709 # Delegate the comparison to a class that knows how to deal with the type
710 # of the expected value (e.g. int, float, list, dict, numpy.array, etc).
711 #
712 # The primary responsibility of these classes is to implement ``__eq__()``
713 # and ``__repr__()``. The former is used to actually check if some
714 # "actual" value is equivalent to the given expected value within the
715 # allowed tolerance. The latter is used to show the user the expected
716 # value and tolerance, in the case that a test failed.
717 #
718 # The actual logic for making approximate comparisons can be found in
719 # ApproxScalar, which is used to compare individual numbers. All of the
720 # other Approx classes eventually delegate to this class. The ApproxBase
721 # class provides some convenient methods and overloads, but isn't really
722 # essential.
724 __tracebackhide__ = True
726 if isinstance(expected, Decimal):
727 cls: Type[ApproxBase] = ApproxDecimal
728 elif isinstance(expected, Mapping):
729 cls = ApproxMapping
730 elif _is_numpy_array(expected):
731 expected = _as_numpy_array(expected)
732 cls = ApproxNumpy
733 elif (
734 hasattr(expected, "__getitem__")
735 and isinstance(expected, Sized)
736 # Type ignored because the error is wrong -- not unreachable.
737 and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
738 ):
739 cls = ApproxSequenceLike
740 elif (
741 isinstance(expected, Collection)
742 # Type ignored because the error is wrong -- not unreachable.
743 and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
744 ):
745 msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
746 raise TypeError(msg)
747 else:
748 cls = ApproxScalar
750 return cls(expected, rel, abs, nan_ok)
753def _is_numpy_array(obj: object) -> bool:
754 """
755 Return true if the given object is implicitly convertible to ndarray,
756 and numpy is already imported.
757 """
758 return _as_numpy_array(obj) is not None
761def _as_numpy_array(obj: object) -> Optional["ndarray"]:
762 """
763 Return an ndarray if the given object is implicitly convertible to ndarray,
764 and numpy is already imported, otherwise None.
765 """
766 import sys
768 np: Any = sys.modules.get("numpy")
769 if np is not None:
770 # avoid infinite recursion on numpy scalars, which have __array__
771 if np.isscalar(obj):
772 return None
773 elif isinstance(obj, np.ndarray):
774 return obj
775 elif hasattr(obj, "__array__") or hasattr("obj", "__array_interface__"):
776 return np.asarray(obj)
777 return None
780# builtin pytest.raises helper
782E = TypeVar("E", bound=BaseException)
785@overload
786def raises(
787 expected_exception: Union[Type[E], Tuple[Type[E], ...]],
788 *,
789 match: Optional[Union[str, Pattern[str]]] = ...,
790) -> "RaisesContext[E]":
791 ...
794@overload
795def raises( # noqa: F811
796 expected_exception: Union[Type[E], Tuple[Type[E], ...]],
797 func: Callable[..., Any],
798 *args: Any,
799 **kwargs: Any,
800) -> _pytest._code.ExceptionInfo[E]:
801 ...
804def raises( # noqa: F811
805 expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
806) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
807 r"""Assert that a code block/function call raises an exception.
809 :param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
810 The excpected exception type, or a tuple if one of multiple possible
811 exception types are excepted.
812 :kwparam str | typing.Pattern[str] | None match:
813 If specified, a string containing a regular expression,
814 or a regular expression object, that is tested against the string
815 representation of the exception using :func:`re.search`.
817 To match a literal string that may contain :ref:`special characters
818 <re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
820 (This is only used when :py:func:`pytest.raises` is used as a context manager,
821 and passed through to the function otherwise.
822 When using :py:func:`pytest.raises` as a function, you can use:
823 ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
825 .. currentmodule:: _pytest._code
827 Use ``pytest.raises`` as a context manager, which will capture the exception of the given
828 type::
830 >>> import pytest
831 >>> with pytest.raises(ZeroDivisionError):
832 ... 1/0
834 If the code block does not raise the expected exception (``ZeroDivisionError`` in the example
835 above), or no exception at all, the check will fail instead.
837 You can also use the keyword argument ``match`` to assert that the
838 exception matches a text or regex::
840 >>> with pytest.raises(ValueError, match='must be 0 or None'):
841 ... raise ValueError("value must be 0 or None")
843 >>> with pytest.raises(ValueError, match=r'must be \d+$'):
844 ... raise ValueError("value must be 42")
846 The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
847 details of the captured exception::
849 >>> with pytest.raises(ValueError) as exc_info:
850 ... raise ValueError("value must be 42")
851 >>> assert exc_info.type is ValueError
852 >>> assert exc_info.value.args[0] == "value must be 42"
854 .. note::
856 When using ``pytest.raises`` as a context manager, it's worthwhile to
857 note that normal context manager rules apply and that the exception
858 raised *must* be the final line in the scope of the context manager.
859 Lines of code after that, within the scope of the context manager will
860 not be executed. For example::
862 >>> value = 15
863 >>> with pytest.raises(ValueError) as exc_info:
864 ... if value > 10:
865 ... raise ValueError("value must be <= 10")
866 ... assert exc_info.type is ValueError # this will not execute
868 Instead, the following approach must be taken (note the difference in
869 scope)::
871 >>> with pytest.raises(ValueError) as exc_info:
872 ... if value > 10:
873 ... raise ValueError("value must be <= 10")
874 ...
875 >>> assert exc_info.type is ValueError
877 **Using with** ``pytest.mark.parametrize``
879 When using :ref:`pytest.mark.parametrize ref`
880 it is possible to parametrize tests such that
881 some runs raise an exception and others do not.
883 See :ref:`parametrizing_conditional_raising` for an example.
885 **Legacy form**
887 It is possible to specify a callable by passing a to-be-called lambda::
889 >>> raises(ZeroDivisionError, lambda: 1/0)
890 <ExceptionInfo ...>
892 or you can specify an arbitrary callable with arguments::
894 >>> def f(x): return 1/x
895 ...
896 >>> raises(ZeroDivisionError, f, 0)
897 <ExceptionInfo ...>
898 >>> raises(ZeroDivisionError, f, x=0)
899 <ExceptionInfo ...>
901 The form above is fully supported but discouraged for new code because the
902 context manager form is regarded as more readable and less error-prone.
904 .. note::
905 Similar to caught exception objects in Python, explicitly clearing
906 local references to returned ``ExceptionInfo`` objects can
907 help the Python interpreter speed up its garbage collection.
909 Clearing those references breaks a reference cycle
910 (``ExceptionInfo`` --> caught exception --> frame stack raising
911 the exception --> current frame stack --> local variables -->
912 ``ExceptionInfo``) which makes Python keep all objects referenced
913 from that cycle (including all local variables in the current
914 frame) alive until the next cyclic garbage collection run.
915 More detailed information can be found in the official Python
916 documentation for :ref:`the try statement <python:try>`.
917 """
918 __tracebackhide__ = True
920 if not expected_exception:
921 raise ValueError(
922 f"Expected an exception type or a tuple of exception types, but got `{expected_exception!r}`. "
923 f"Raising exceptions is already understood as failing the test, so you don't need "
924 f"any special code to say 'this should never raise an exception'."
925 )
926 if isinstance(expected_exception, type):
927 excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,)
928 else:
929 excepted_exceptions = expected_exception
930 for exc in excepted_exceptions:
931 if not isinstance(exc, type) or not issubclass(exc, BaseException):
932 msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
933 not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
934 raise TypeError(msg.format(not_a))
936 message = f"DID NOT RAISE {expected_exception}"
938 if not args:
939 match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None)
940 if kwargs:
941 msg = "Unexpected keyword arguments passed to pytest.raises: "
942 msg += ", ".join(sorted(kwargs))
943 msg += "\nUse context-manager form instead?"
944 raise TypeError(msg)
945 return RaisesContext(expected_exception, message, match)
946 else:
947 func = args[0]
948 if not callable(func):
949 raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
950 try:
951 func(*args[1:], **kwargs)
952 except expected_exception as e:
953 # We just caught the exception - there is a traceback.
954 assert e.__traceback__ is not None
955 return _pytest._code.ExceptionInfo.from_exc_info(
956 (type(e), e, e.__traceback__)
957 )
958 fail(message)
961# This doesn't work with mypy for now. Use fail.Exception instead.
962raises.Exception = fail.Exception # type: ignore
965@final
966class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
967 def __init__(
968 self,
969 expected_exception: Union[Type[E], Tuple[Type[E], ...]],
970 message: str,
971 match_expr: Optional[Union[str, Pattern[str]]] = None,
972 ) -> None:
973 self.expected_exception = expected_exception
974 self.message = message
975 self.match_expr = match_expr
976 self.excinfo: Optional[_pytest._code.ExceptionInfo[E]] = None
978 def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
979 self.excinfo = _pytest._code.ExceptionInfo.for_later()
980 return self.excinfo
982 def __exit__(
983 self,
984 exc_type: Optional[Type[BaseException]],
985 exc_val: Optional[BaseException],
986 exc_tb: Optional[TracebackType],
987 ) -> bool:
988 __tracebackhide__ = True
989 if exc_type is None:
990 fail(self.message)
991 assert self.excinfo is not None
992 if not issubclass(exc_type, self.expected_exception):
993 return False
994 # Cast to narrow the exception type now that it's verified.
995 exc_info = cast(Tuple[Type[E], E, TracebackType], (exc_type, exc_val, exc_tb))
996 self.excinfo.fill_unfilled(exc_info)
997 if self.match_expr is not None:
998 self.excinfo.match(self.match_expr)
999 return True