1 import math

2 import pprint

3 from collections.abc import Collection

4 from collections.abc import Sized

5 from decimal import Decimal

6 from numbers import Complex

7 from types import TracebackType

8 from typing import Any

9 from typing import Callable

10 from typing import cast

11 from typing import ContextManager

12 from typing import List

13 from typing import Mapping

14 from typing import Optional

15 from typing import Pattern

16 from typing import Sequence

17 from typing import Tuple

18 from typing import Type

19 from typing import TYPE_CHECKING

20 from typing import TypeVar

21 from typing import Union

22

23 if TYPE_CHECKING:

24 from numpy import ndarray

25

26

27 import _pytest._code

28 from _pytest.compat import final

29 from _pytest.compat import STRING_TYPES

30 from _pytest.compat import overload

31 from _pytest.outcomes import fail

32

33

34 def _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 )

41

42

43 def _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

67

68

69# builtin pytest.approx helper

70

71

72 class ApproxBase:

73 """Provide shared utilities for making approximate comparisons between

74 numbers or sequences of numbers."""

75

76 # Tell numpy to use our `__eq__` operator instead of its.

77 __array_ufunc__ = None

78 __array_priority__ = 100

79

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()

87

88 def __repr__(self) -> str:

89 raise NotImplementedError

90

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 ]

97

98 def __eq__(self, actual) -> bool:

99 return all(

100 a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual)

101 )

102

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 )

108

109 # Ignore type because of https://github.com/python/mypy/issues/4266.

110 __hash__ = None # type: ignore

111

112 def __ne__(self, actual) -> bool:

113 return not (actual == self)

114

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)

119

120 def _yield_comparisons(self, actual):

121 """Yield all the pairs of numbers to be compared.

122

123 This is used to implement the `__eq__` method.

124 """

125 raise NotImplementedError

126

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.

134

135

136 def _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)

143

144

145 class ApproxNumpy(ApproxBase):

146 """Perform approximate comparisons where the expected value is numpy array."""

147

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})"

153

154 def _repr_compare(self, other_side: "ndarray") -> List[str]:

155 import itertools

156 import math

157

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

169

170 np_array_shape = self.expected.shape

171 approx_side_as_seq = _recursive_sequence_map(

172 self._approx_scalar, self.expected.tolist()

173 )

174

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 ]

180

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)

196

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 )

213

214 def __eq__(self, actual) -> bool:

215 import numpy as np

216

217 # self.expected is supposed to always be an array here.

218

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

224

225 if not np.isscalar(actual) and actual.shape != self.expected.shape:

226 return False

227

228 return super().__eq__(actual)

229

230 def _yield_comparisons(self, actual):

231 import numpy as np

232

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.

236

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()

243

244

245 class ApproxMapping(ApproxBase):

246 """Perform approximate comparisons where the expected value is a mapping

247 with numeric values (the keys can be anything)."""

248

249 def __repr__(self) -> str:

250 return "approx({!r})".format(

251 {k: self._approx_scalar(v) for k, v in self.expected.items()}

252 )

253

254 def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:

255 import math

256

257 approx_side_as_map = {

258 k: self._approx_scalar(v) for k, v in self.expected.items()

259 }

260

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)

283

284 message_data = [

285 (str(key), str(other_side[key]), str(approx_side_as_map[key]))

286 for key in different_ids

287 ]

288

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 )

297

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

304

305 return super().__eq__(actual)

306

307 def _yield_comparisons(self, actual):

308 for k in self.expected.keys():

309 yield actual[k], self.expected[k]

310

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)))

317

318

319 class ApproxSequenceLike(ApproxBase):

320 """Perform approximate comparisons where the expected value is a sequence of numbers."""

321

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 )

329

330 def _repr_compare(self, other_side: Sequence[float]) -> List[str]:

331 import math

332

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 ]

338

339 approx_side_as_map = _recursive_sequence_map(self._approx_scalar, self.expected)

340

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)

356

357 message_data = [

358 (str(i), str(other_side[i]), str(approx_side_as_map[i]))

359 for i in different_ids

360 ]

361

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 )

370

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)

378

379 def _yield_comparisons(self, actual):

380 return zip(actual, self.expected)

381

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)))

388

389

390 class ApproxScalar(ApproxBase):

391 """Perform approximate comparisons where the expected value is a single number."""

392

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

397

398 def __repr__(self) -> str:

399 """Return a string communicating both the expected value and the

400 tolerance for the comparison being made.

401

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)

411

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 = "???"

424

425 return f"{self.expected} ± {vetted_tolerance}"

426

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)

435

436 # Short-circuit exact equality.

437 if actual == self.expected:

438 return True

439

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

448

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]

454

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

463

464 # Return true if the two numbers are within the tolerance.

465 result: bool = abs(self.expected - actual) <= self.tolerance

466 return result

467

468 # Ignore type because of https://github.com/python/mypy/issues/4266.

469 __hash__ = None # type: ignore

470

471 @property

472 def tolerance(self):

473 """Return the tolerance for the comparison.

474

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 """

478

479 def set_default(x, default):

480 return x if x is not None else default

481

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)

485

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.")

492

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

498

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)

507

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.")

514

515 # Return the larger of the relative and absolute tolerances.

516 return max(relative_tolerance, absolute_tolerance)

517

518

519 class ApproxDecimal(ApproxScalar):

520 """Perform approximate comparisons where the expected value is a Decimal."""

521

522 DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")

523 DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")

524

525

526 def 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.

529

530 Due to the :doc:`python:tutorial/floatingpoint`, numbers that we

531 would intuitively expect to be equal are not always so::

532

533 >>> 0.1 + 0.2 == 0.3

534 False

535

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::

540

541 >>> abs((0.1 + 0.2) - 0.3) < 1e-6

542 True

543

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.

551

552 The ``approx`` class performs floating-point comparisons using a syntax

553 that's as intuitive as possible::

554

555 >>> from pytest import approx

556 >>> 0.1 + 0.2 == approx(0.3)

557 True

558

559 The same syntax also works for ordered sequences of numbers::

560

561 >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))

562 True

563

564 ``numpy`` arrays::

565

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

569

570 And for a ``numpy`` array against a scalar::

571

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

575

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.

579

580 Finally, dictionary *values* can also be compared::

581

582 >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})

583 True

584

585 The comparison will be true if both mappings have the same keys and their

586 respective values match the expected tolerances.

587

588 **Tolerances**

589

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".)

601

602 Both the relative and absolute tolerances can be changed by passing

603 arguments to the ``approx`` constructor::

604

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

611

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::

618

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

625

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::

630

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

637

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:

642

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`.

650

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`.

658

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`.

665

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.

672

673 .. note::

674

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.

678

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>`_.

683

684 .. warning::

685

686 .. versionchanged:: 3.2

687

688 In order to avoid inconsistent behavior, :py:exc:`TypeError` is

689 raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons.

690 The example below illustrates the problem::

691

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)

694

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__`

699

700 .. versionchanged:: 3.7.1

701 ``approx`` raises ``TypeError`` when it encounters a dict value or

702 sequence element of nonnumeric type.

703

704 .. versionchanged:: 6.1.0

705 ``approx`` falls back to strict equality for nonnumeric types instead

706 of raising ``TypeError``.

707 """

708

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.

723

724 __tracebackhide__ = True

725

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

749

750 return cls(expected, rel, abs, nan_ok)

751

752

753 def _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

759

760

761 def _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

767

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

778

779

780# builtin pytest.raises helper

781

782 E = TypeVar("E", bound=BaseException)

783

784

785@overload

786 def raises(

787 expected_exception: Union[Type[E], Tuple[Type[E], ...]],

788 *,

789 match: Optional[Union[str, Pattern[str]]] = ...,

790) -> "RaisesContext[E]":

791 ...

792

793

794@overload

795 def 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 ...

802

803

804 def 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.

808

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`.

816

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`.

819

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")``.)

824

825 .. currentmodule:: _pytest._code

826

827 Use ``pytest.raises`` as a context manager, which will capture the exception of the given

828 type::

829

830 >>> import pytest

831 >>> with pytest.raises(ZeroDivisionError):

832 ... 1/0

833

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.

836

837 You can also use the keyword argument ``match`` to assert that the

838 exception matches a text or regex::

839

840 >>> with pytest.raises(ValueError, match='must be 0 or None'):

841 ... raise ValueError("value must be 0 or None")

842

843 >>> with pytest.raises(ValueError, match=r'must be \d+$'):

844 ... raise ValueError("value must be 42")

845

846 The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the

847 details of the captured exception::

848

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"

853

854 .. note::

855

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::

861

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

867

868 Instead, the following approach must be taken (note the difference in

869 scope)::

870

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

876

877 **Using with** ``pytest.mark.parametrize``

878

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.

882

883 See :ref:`parametrizing_conditional_raising` for an example.

884

885 **Legacy form**

886

887 It is possible to specify a callable by passing a to-be-called lambda::

888

889 >>> raises(ZeroDivisionError, lambda: 1/0)

890 <ExceptionInfo ...>

891

892 or you can specify an arbitrary callable with arguments::

893

894 >>> def f(x): return 1/x

895 ...

896 >>> raises(ZeroDivisionError, f, 0)

897 <ExceptionInfo ...>

898 >>> raises(ZeroDivisionError, f, x=0)

899 <ExceptionInfo ...>

900

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.

903

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.

908

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

919

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))

935

936 message = f"DID NOT RAISE {expected_exception}"

937

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)

959

960

961# This doesn't work with mypy for now. Use fail.Exception instead.

962 raises.Exception = fail.Exception # type: ignore

963

964

965@final

966 class 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

977

978 def __enter__(self) -> _pytest._code.ExceptionInfo[E]:

979 self.excinfo = _pytest._code.ExceptionInfo.for_later()

980 return self.excinfo

981

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