diff options
-rw-r--r-- | Doc/lib/libdoctest.tex | 20 | ||||
-rw-r--r-- | Lib/doctest.py | 80 | ||||
-rw-r--r-- | Lib/test/test_doctest.py | 101 |
3 files changed, 199 insertions, 2 deletions
diff --git a/Doc/lib/libdoctest.tex b/Doc/lib/libdoctest.tex index 2635486..a30a432 100644 --- a/Doc/lib/libdoctest.tex +++ b/Doc/lib/libdoctest.tex @@ -581,6 +581,17 @@ TypeError: object doesn't support item assignment \end{datadesc} +\begin{datadesc}{NORMALIZE_NUMBERS} + When specified, number literals in the expected output will match + corresponding number literals in the actual output if their values + are equal (to ten digits of precision). For example, \code{1.1} + will match \code{1.1000000000000001}; and \code{1L} will match + \code{1} and \code{1.0}. Currently, \constant{NORMALIZE_NUMBERS} + can fail to normalize numbers when used in conjunction with + ellipsis. In particular, if an ellipsis marker matches one or + more numbers, then number normalization is not supported. +\end{datadesc} + \begin{datadesc}{COMPARISON_FLAGS} A bitmask or'ing together all the comparison flags above. \end{datadesc} @@ -702,7 +713,7 @@ can be useful. \versionchanged[Constants \constant{DONT_ACCEPT_BLANKLINE}, \constant{NORMALIZE_WHITESPACE}, \constant{ELLIPSIS}, - \constant{IGNORE_EXCEPTION_DETAIL}, + \constant{IGNORE_EXCEPTION_DETAIL}, \constant{NORMALIZE_NUMBERS}, \constant{REPORT_UDIFF}, \constant{REPORT_CDIFF}, \constant{REPORT_NDIFF}, \constant{REPORT_ONLY_FIRST_FAILURE}, \constant{COMPARISON_FLAGS} and \constant{REPORTING_FLAGS} @@ -740,6 +751,7 @@ in any particular order, so a test like % Hey! What happened to Monty Python examples? % Tim: ask Guido -- it's his example! +% doctest: ignore \begin{verbatim} >>> foo() {"Hermione": "hippogryph", "Harry": "broomstick"} @@ -747,6 +759,7 @@ in any particular order, so a test like is vulnerable! One workaround is to do +% doctest: ignore \begin{verbatim} >>> foo() == {"Hermione": "hippogryph", "Harry": "broomstick"} True @@ -754,6 +767,7 @@ True instead. Another is to do +% doctest: ignore \begin{verbatim} >>> d = foo().items() >>> d.sort() @@ -765,6 +779,7 @@ There are others, but you get the idea. Another bad idea is to print things that embed an object address, like +% doctest: ignore \begin{verbatim} >>> id(1.0) # certain to fail some of the time 7948648 @@ -776,6 +791,7 @@ Another bad idea is to print things that embed an object address, like The \constant{ELLIPSIS} directive gives a nice approach for the last example: +% doctest: ignore \begin{verbatim} >>> C() #doctest: +ELLIPSIS <__main__.C instance at 0x...> @@ -785,6 +801,7 @@ Floating-point numbers are also subject to small output variations across platforms, because Python defers to the platform C library for float formatting, and C libraries vary widely in quality here. +% doctest: ignore \begin{verbatim} >>> 1./7 # risky 0.14285714285714285 @@ -1618,6 +1635,7 @@ Doctest provides several mechanisms for debugging doctest examples: Then an interactive Python session may look like this: +% doctest: ignore \begin{verbatim} >>> import a, doctest >>> doctest.testmod(a) 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: |