1313from _pytest .compat import safe_getattr
1414from _pytest .fixtures import FixtureRequest
1515from _pytest .outcomes import Skipped
16+ from _pytest .python_api import approx
1617from _pytest .warning_types import PytestWarning
1718
1819DOCTEST_REPORT_CHOICE_NONE = "none"
@@ -286,6 +287,7 @@ def _get_flag_lookup():
286287 COMPARISON_FLAGS = doctest .COMPARISON_FLAGS ,
287288 ALLOW_UNICODE = _get_allow_unicode_flag (),
288289 ALLOW_BYTES = _get_allow_bytes_flag (),
290+ NUMBER = _get_number_flag (),
289291 )
290292
291293
@@ -453,10 +455,15 @@ def func():
453455
454456def _get_checker ():
455457 """
456- Returns a doctest.OutputChecker subclass that takes in account the
457- ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES
458- to strip b'' prefixes.
459- Useful when the same doctest should run in Python 2 and Python 3.
458+ Returns a doctest.OutputChecker subclass that supports some
459+ additional options:
460+
461+ * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
462+ prefixes (respectively) in string literals. Useful when the same
463+ doctest should run in Python 2 and Python 3.
464+
465+ * NUMBER to ignore floating-point differences smaller than the
466+ precision of the literal number in the doctest.
460467
461468 An inner class is used to avoid importing "doctest" at the module
462469 level.
@@ -469,38 +476,89 @@ def _get_checker():
469476
470477 class LiteralsOutputChecker (doctest .OutputChecker ):
471478 """
472- Copied from doctest_nose_plugin.py from the nltk project:
473- https://github.com/nltk/nltk
474-
475- Further extended to also support byte literals.
479+ Based on doctest_nose_plugin.py from the nltk project
480+ (https://github.com/nltk/nltk) and on the "numtest" doctest extension
481+ by Sebastien Boisgerault (https://github.com/boisgera/numtest).
476482 """
477483
478484 _unicode_literal_re = re .compile (r"(\W|^)[uU]([rR]?[\'\"])" , re .UNICODE )
479485 _bytes_literal_re = re .compile (r"(\W|^)[bB]([rR]?[\'\"])" , re .UNICODE )
486+ _number_re = re .compile (
487+ r"""
488+ (?P<number>
489+ (?P<mantissa>
490+ (?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
491+ |
492+ (?P<integer2> [+-]?\d+)\.
493+ )
494+ (?:
495+ [Ee]
496+ (?P<exponent1> [+-]?\d+)
497+ )?
498+ |
499+ (?P<integer3> [+-]?\d+)
500+ (?:
501+ [Ee]
502+ (?P<exponent2> [+-]?\d+)
503+ )
504+ )
505+ """ ,
506+ re .VERBOSE ,
507+ )
480508
481509 def check_output (self , want , got , optionflags ):
482- res = doctest .OutputChecker .check_output (self , want , got , optionflags )
483- if res :
510+ if doctest .OutputChecker .check_output (self , want , got , optionflags ):
484511 return True
485512
486513 allow_unicode = optionflags & _get_allow_unicode_flag ()
487514 allow_bytes = optionflags & _get_allow_bytes_flag ()
488- if not allow_unicode and not allow_bytes :
489- return False
515+ allow_number = optionflags & _get_number_flag ()
490516
491- else : # pragma: no cover
492-
493- def remove_prefixes (regex , txt ):
494- return re .sub (regex , r"\1\2" , txt )
517+ if not allow_unicode and not allow_bytes and not allow_number :
518+ return False
495519
496- if allow_unicode :
497- want = remove_prefixes (self ._unicode_literal_re , want )
498- got = remove_prefixes (self ._unicode_literal_re , got )
499- if allow_bytes :
500- want = remove_prefixes (self ._bytes_literal_re , want )
501- got = remove_prefixes (self ._bytes_literal_re , got )
502- res = doctest .OutputChecker .check_output (self , want , got , optionflags )
503- return res
520+ def remove_prefixes (regex , txt ):
521+ return re .sub (regex , r"\1\2" , txt )
522+
523+ if allow_unicode :
524+ want = remove_prefixes (self ._unicode_literal_re , want )
525+ got = remove_prefixes (self ._unicode_literal_re , got )
526+
527+ if allow_bytes :
528+ want = remove_prefixes (self ._bytes_literal_re , want )
529+ got = remove_prefixes (self ._bytes_literal_re , got )
530+
531+ if allow_number :
532+ got = self ._remove_unwanted_precision (want , got )
533+
534+ return doctest .OutputChecker .check_output (self , want , got , optionflags )
535+
536+ def _remove_unwanted_precision (self , want , got ):
537+ wants = list (self ._number_re .finditer (want ))
538+ gots = list (self ._number_re .finditer (got ))
539+ if len (wants ) != len (gots ):
540+ return got
541+ offset = 0
542+ for w , g in zip (wants , gots ):
543+ fraction = w .group ("fraction" )
544+ exponent = w .group ("exponent1" )
545+ if exponent is None :
546+ exponent = w .group ("exponent2" )
547+ if fraction is None :
548+ precision = 0
549+ else :
550+ precision = len (fraction )
551+ if exponent is not None :
552+ precision -= int (exponent )
553+ if float (w .group ()) == approx (float (g .group ()), abs = 10 ** - precision ):
554+ # They're close enough. Replace the text we actually
555+ # got with the text we want, so that it will match when we
556+ # check the string literally.
557+ got = (
558+ got [: g .start () + offset ] + w .group () + got [g .end () + offset :]
559+ )
560+ offset += w .end () - w .start () - (g .end () - g .start ())
561+ return got
504562
505563 _get_checker .LiteralsOutputChecker = LiteralsOutputChecker
506564 return _get_checker .LiteralsOutputChecker ()
@@ -524,6 +582,15 @@ def _get_allow_bytes_flag():
524582 return doctest .register_optionflag ("ALLOW_BYTES" )
525583
526584
585+ def _get_number_flag ():
586+ """
587+ Registers and returns the NUMBER flag.
588+ """
589+ import doctest
590+
591+ return doctest .register_optionflag ("NUMBER" )
592+
593+
527594def _get_report_choice (key ):
528595 """
529596 This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
0 commit comments