summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
Diffstat (limited to 'Lib')
-rw-r--r--Lib/doctest.py80
-rw-r--r--Lib/test/test_doctest.py101
2 files changed, 180 insertions, 1 deletions
diff --git a/Lib/doctest.py b/Lib/doctest.py
index 26a8914..dfd8bb9 100644
--- a/Lib/doctest.py
+++ b/Lib/doctest.py
@@ -55,6 +55,7 @@ __all__ = [
'NORMALIZE_WHITESPACE',
'ELLIPSIS',
'IGNORE_EXCEPTION_DETAIL',
+ 'NORMALIZE_NUMBERS',
'COMPARISON_FLAGS',
'REPORT_UDIFF',
'REPORT_CDIFF',
@@ -139,12 +140,14 @@ DONT_ACCEPT_BLANKLINE = register_optionflag('DONT_ACCEPT_BLANKLINE')
NORMALIZE_WHITESPACE = register_optionflag('NORMALIZE_WHITESPACE')
ELLIPSIS = register_optionflag('ELLIPSIS')
IGNORE_EXCEPTION_DETAIL = register_optionflag('IGNORE_EXCEPTION_DETAIL')
+NORMALIZE_NUMBERS = register_optionflag('NORMALIZE_NUMBERS')
COMPARISON_FLAGS = (DONT_ACCEPT_TRUE_FOR_1 |
DONT_ACCEPT_BLANKLINE |
NORMALIZE_WHITESPACE |
ELLIPSIS |
- IGNORE_EXCEPTION_DETAIL)
+ IGNORE_EXCEPTION_DETAIL |
+ NORMALIZE_NUMBERS)
REPORT_UDIFF = register_optionflag('REPORT_UDIFF')
REPORT_CDIFF = register_optionflag('REPORT_CDIFF')
@@ -277,6 +280,72 @@ class _SpoofOut(StringIO):
if hasattr(self, "softspace"):
del self.softspace
+# The number of digits of precision that must be equal for
+# NORMALIZE_NUMBERS to consider two numbers equal.
+_NORMALIZE_NUMBERS_PRECISION_THRESHOLD = 10
+
+# A regular expression that matches Python number literals. This is
+# used by _normalize_numbers to look for numbers that should be
+# normalized.
+_NUMBER_LITERAL = re.compile(r'''
+ (\d+[.]\d*(?:[eE][-+]?\d+)?[jJ]? | # float (w/ digits left of ".")
+ [.]\d+(?:[eE][-+]?\d+)?[jJ]? | # float (no digits left of ".")
+ \d+ (?:[eE][-+]?\d+) [jJ]? | # float (no ".", exponent only)
+ \d [jJ] | # float (no ".", imaginary only)
+ 0[xX]\d+[lL]? | # hexint
+ 0[0-7]*[lL]? | # octint or zero
+ \d+[lL]? ) # decint
+ ''', re.VERBOSE)
+
+def _normalize_numbers(want, got):
+ """
+ If all the numbers in `want` and `got` match (one-for-one), then
+ return a new version of `got` with the exact number strings from
+ `want` spliced in. Two numbers match if `str` of their float
+ values are equal. (I.e., `x` matches `y` if
+ `str(float(x))==str(float(y))`).
+ """
+ want_pieces = _NUMBER_LITERAL.split(want)
+ got_pieces = _NUMBER_LITERAL.split(got)
+
+ # If they don't have the same number of numbers, fail immediately.
+ if len(want_pieces) != len(got_pieces):
+ return got
+
+ # If any individual numbers don't match, then fail.
+ for i in range(1, len(got_pieces), 2):
+ w, g = eval(want_pieces[i]), eval(got_pieces[i])
+ if not _numbers_match(w, g):
+ return got
+
+ # Success; replace numbers in got w/ numbers from want.
+ for i in range(1, len(got_pieces), 2):
+ got_pieces[i] = want_pieces[i]
+ return ''.join(got_pieces)
+
+def _numbers_match(x, y):
+ """
+ A helper function for _normalize_numbers, that returns true if the
+ numbers `x` and `y` are close enough to match for NORMALIZE_NUMBERS.
+ """
+ # Equal numbers match.
+ if x == y:
+ return True
+ # Split up complex numbers into real & imag.
+ if isinstance(x, complex):
+ return (isinstance(y, complex) and
+ _numbers_match(x.real, y.real) and
+ _numbers_match(x.imag, y.imag))
+ # If the signs are different, they don't match.
+ if x*y < 0:
+ return False
+ # If one is zero and the other isn't, they don't match.
+ if x==0 or y==0:
+ return False
+ # They're not exactly equal, but are they close enough?
+ threshold = 10**-_NORMALIZE_NUMBERS_PRECISION_THRESHOLD
+ return (abs(x-y) / min(abs(x), abs(y))) < threshold
+
# Worst-case linear-time ellipsis matching.
def _ellipsis_match(want, got):
"""
@@ -1503,6 +1572,13 @@ class OutputChecker:
if got == want:
return True
+ # This flag causes doctest to treat numbers that are within a
+ # small threshold as if they are equal.
+ if optionflags & NORMALIZE_NUMBERS:
+ got = _normalize_numbers(want, got)
+ if got == want:
+ return True
+
# The ELLIPSIS flag says to let the sequence "..." in `want`
# match any substring in `got`.
if optionflags & ELLIPSIS:
@@ -1783,6 +1859,7 @@ def testmod(m=None, name=None, globs=None, verbose=None, isprivate=None,
NORMALIZE_WHITESPACE
ELLIPSIS
IGNORE_EXCEPTION_DETAIL
+ NORMALIZE_NUMBERS
REPORT_UDIFF
REPORT_CDIFF
REPORT_NDIFF
@@ -1905,6 +1982,7 @@ def testfile(filename, module_relative=True, name=None, package=None,
NORMALIZE_WHITESPACE
ELLIPSIS
IGNORE_EXCEPTION_DETAIL
+ NORMALIZE_NUMBERS
REPORT_UDIFF
REPORT_CDIFF
REPORT_NDIFF
diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py
index d17ca1a..eb0b10c 100644
--- a/Lib/test/test_doctest.py
+++ b/Lib/test/test_doctest.py
@@ -1032,6 +1032,107 @@ treated as equal:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
+The NORMALIZE_NUMBERS flag causes numbers that are equal (to
+approximately 10 decimal places) but formatted differently to match.
+
+ >>> def f(x): '''
+ ... Numbers will match if they are exactly equal:
+ ...
+ ... >>> print 1.1, 'intervening text', 1L # should match
+ ... 1.1 intervening text 1L
+ ... >>> print 1.0j, 22, 22.0, 1, 1e1 # should match
+ ... 1j 22.0 22 1 10.0
+ ...
+ ... Numbers will match if they are equal to 14 digits of
+ ... precision:
+ ...
+ ... >>> 2.00000000001 # should match
+ ... 1.99999999999
+ ... >>> 2.000000001 # should not match
+ ... 1.999999999
+ ... >>> 2.00000000001e10 # should match
+ ... 1.99999999999e10
+ ... >>> 2.000000001e10 # should not match
+ ... 1.999999999e10
+ ... '''
+
+ >>> # Without the flag:
+ >>> test = doctest.DocTestFinder().find(f)[0]
+ >>> doctest.DocTestRunner(verbose=False).run(test)
+ ... # doctest: +ELLIPSIS
+ **********************************************************************
+ File ..., line 4, in f
+ Failed example:
+ print 1.1, 'intervening text', 1L # should match
+ Expected:
+ 1.1 intervening text 1L
+ Got:
+ 1.1 intervening text 1
+ **********************************************************************
+ File ..., line 6, in f
+ Failed example:
+ print 1.0j, 22, 22.0, 1, 1e1 # should match
+ Expected:
+ 1j 22.0 22 1 10.0
+ Got:
+ 1j 22 22.0 1 10.0
+ **********************************************************************
+ File ..., line 12, in f
+ Failed example:
+ 2.00000000001 # should match
+ Expected:
+ 1.99999999999
+ Got:
+ 2.00000000001
+ **********************************************************************
+ File ..., line 14, in f
+ Failed example:
+ 2.000000001 # should not match
+ Expected:
+ 1.999999999
+ Got:
+ 2.0000000010000001
+ **********************************************************************
+ File ..., line 16, in f
+ Failed example:
+ 2.00000000001e10 # should match
+ Expected:
+ 1.99999999999e10
+ Got:
+ 20000000000.099998
+ **********************************************************************
+ File ..., line 18, in f
+ Failed example:
+ 2.000000001e10 # should not match
+ Expected:
+ 1.999999999e10
+ Got:
+ 20000000010.0
+ (6, 6)
+
+ >>> # With the flag:
+ >>> test = doctest.DocTestFinder().find(f)[0]
+ >>> flags = doctest.NORMALIZE_NUMBERS
+ >>> doctest.DocTestRunner(verbose=False, optionflags=flags).run(test)
+ ... # doctest: +ELLIPSIS
+ **********************************************************************
+ File ..., line 14, in f
+ Failed example:
+ 2.000000001 # should not match
+ Expected:
+ 1.999999999
+ Got:
+ 2.0000000010000001
+ **********************************************************************
+ File ..., line 18, in f
+ Failed example:
+ 2.000000001e10 # should not match
+ Expected:
+ 1.999999999e10
+ Got:
+ 20000000010.0
+ (2, 6)
+
The ELLIPSIS flag causes ellipsis marker ("...") in the expected
output to match any substring in the actual output: