From 052d0cd291ca0f447fe7c8bf5ad30cbb1ad8de8f Mon Sep 17 00:00:00 2001 From: Edward Loper Date: Sun, 19 Sep 2004 17:19:33 +0000 Subject: - Added "testfile" function, a simple function for running & verifying all examples in a given text file. (analagous to "testmod") - Minor docstring fixes. - Added module_relative parameter to DocTestFile/DocTestSuite, which controls whether paths are module-relative & os-independent, or os-specific. --- Lib/doctest.py | 220 +++++++++++++++++++++++++++++++++++++++-------- Lib/test/test_doctest.py | 135 ++++++++++++++++++++++++++++- 2 files changed, 315 insertions(+), 40 deletions(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index 3414f4a..a34b0a1 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -200,6 +200,7 @@ __all__ = [ 'DebugRunner', # 6. Test Functions 'testmod', + 'testfile', 'run_docstring_examples', # 7. Tester 'Tester', @@ -478,6 +479,30 @@ class _OutputRedirectingPdb(pdb.Pdb): # Restore stdout. sys.stdout = save_stdout +def _module_relative_path(module, path): + if not inspect.ismodule(module): + raise TypeError, 'Expected a module: %r' % module + if path.startswith('/'): + raise ValueError, 'Module-relative files may not have absolute paths' + + # Find the base directory for the path. + if hasattr(module, '__file__'): + # A normal module/package + basedir = os.path.split(module.__file__)[0] + elif module.__name__ == '__main__': + # An interactive session. + if len(sys.argv)>0 and sys.argv[0] != '': + basedir = os.path.split(sys.argv[0])[0] + else: + basedir = os.curdir + else: + # A module w/o __file__ (this includes builtins) + raise ValueError("Can't resolve paths relative to the module " + + module + " (it has no __file__)") + + # Combine the base directory and the path. + return os.path.join(basedir, *(path.split('/'))) + ###################################################################### ## 2. Example & DocTest ###################################################################### @@ -1881,6 +1906,7 @@ def testmod(m=None, name=None, globs=None, verbose=None, isprivate=None, DONT_ACCEPT_BLANKLINE NORMALIZE_WHITESPACE ELLIPSIS + IGNORE_EXCEPTION_DETAIL REPORT_UDIFF REPORT_CDIFF REPORT_NDIFF @@ -1896,9 +1922,7 @@ def testmod(m=None, name=None, globs=None, verbose=None, isprivate=None, treat all functions as public. Optionally, "isprivate" can be set to doctest.is_private to skip over functions marked as private using the underscore naming convention; see its docs for details. - """ - """ [XX] This is no longer true: Advanced tomfoolery: testmod runs methods of a local instance of class doctest.Tester, then merges the results into (or creates) global Tester instance doctest.master. Methods of doctest.master @@ -1950,6 +1974,121 @@ def testmod(m=None, name=None, globs=None, verbose=None, isprivate=None, return runner.failures, runner.tries +def testfile(filename, module_relative=True, name=None, package=None, + globs=None, verbose=None, report=True, optionflags=0, + extraglobs=None, raise_on_error=False): + """ + Test examples in the given file. Return (#failures, #tests). + + Optional keyword arg "module_relative" specifies how filenames + should be interpreted: + + - If "module_relative" is True (the default), then "filename" + specifies a module-relative path. By default, this path is + relative to the calling module's directory; but if the + "package" argument is specified, then it is relative to that + package. To ensure os-independence, "filename" should use + "/" characters to separate path segments, and should not + be an absolute path (i.e., it may not begin with "/"). + + - If "module_relative" is False, then "filename" specifies an + os-specific path. The path may be absolute or relative (to + the current working directory). + + Optional keyword arg "name" gives the name of the file; by default + use the file's name. + + Optional keyword argument "package" is a Python package or the + name of a Python package whose directory should be used as the + base directory for a module relative filename. If no package is + specified, then the calling module's directory is used as the base + directory for module relative filenames. It is an error to + specify "package" if "module_relative" is False. + + Optional keyword arg "globs" gives a dict to be used as the globals + when executing examples; by default, use {}. A copy of this dict + is actually used for each docstring, so that each docstring's + examples start with a clean slate. + + Optional keyword arg "extraglobs" gives a dictionary that should be + merged into the globals that are used to execute examples. By + default, no extra globals are used. + + Optional keyword arg "verbose" prints lots of stuff if true, prints + only failures if false; by default, it's true iff "-v" is in sys.argv. + + Optional keyword arg "report" prints a summary at the end when true, + else prints nothing at the end. In verbose mode, the summary is + detailed, else very brief (in fact, empty if all tests passed). + + Optional keyword arg "optionflags" or's together module constants, + and defaults to 0. Possible values (see the docs for details): + + DONT_ACCEPT_TRUE_FOR_1 + DONT_ACCEPT_BLANKLINE + NORMALIZE_WHITESPACE + ELLIPSIS + IGNORE_EXCEPTION_DETAIL + REPORT_UDIFF + REPORT_CDIFF + REPORT_NDIFF + REPORT_ONLY_FIRST_FAILURE + + Optional keyword arg "raise_on_error" raises an exception on the + first unexpected exception or failure. This allows failures to be + post-mortem debugged. + + Advanced tomfoolery: testmod runs methods of a local instance of + class doctest.Tester, then merges the results into (or creates) + global Tester instance doctest.master. Methods of doctest.master + can be called directly too, if you want to do something unusual. + Passing report=0 to testmod is especially useful then, to delay + displaying a summary. Invoke doctest.master.summarize(verbose) + when you're done fiddling. + """ + global master + + if package and not module_relative: + raise ValueError("Package may only be specified for module-" + "relative paths.") + + # Relativize the path + if module_relative: + package = _normalize_module(package) + filename = _module_relative_path(package, filename) + + # If no name was given, then use the file's name. + if name is None: + name = os.path.split(filename)[-1] + + # Assemble the globals. + if globs is None: + globs = {} + else: + globs = globs.copy() + if extraglobs is not None: + globs.update(extraglobs) + + if raise_on_error: + runner = DebugRunner(verbose=verbose, optionflags=optionflags) + else: + runner = DocTestRunner(verbose=verbose, optionflags=optionflags) + + # Read the file, convert it to a test, and run it. + s = open(filename).read() + test = DocTestParser().get_doctest(s, globs, name, filename, 0) + runner.run(test) + + if report: + runner.summarize() + + if master is None: + master = runner + else: + master.merge(runner) + + return runner.failures, runner.tries + def run_docstring_examples(f, globs, verbose=False, name="NoName", compileflags=None, optionflags=0): """ @@ -2311,52 +2450,59 @@ class DocFileCase(DocTestCase): % (self._dt_test.name, self._dt_test.filename, err) ) -def DocFileTest(path, package=None, globs=None, **options): - name = path.split('/')[-1] +def DocFileTest(path, module_relative=True, package=None, + globs=None, **options): + if globs is None: + globs = {} - # Interpret relative paths as relative to the given package's - # directory (or the current module, if no package is specified). - if not os.path.isabs(path): + if package and not module_relative: + raise ValueError("Package may only be specified for module-" + "relative paths.") + + # Relativize the path. + if module_relative: package = _normalize_module(package) - if hasattr(package, '__file__'): - # A normal package/module. - dir = os.path.split(package.__file__)[0] - path = os.path.join(dir, *(path.split('/'))) - elif package.__name__ == '__main__': - # An interactive session. - if sys.argv[0] != '': - dir = os.path.split(sys.argv[0])[0] - path = os.path.join(dir, *(path.split('/'))) - else: - # A module w/o __file__ (this includes builtins) - raise ValueError("Can't resolve paths relative to " + - "the module %s (it has" % package + - "no __file__)") + path = _module_relative_path(package, path) - doc = open(path).read() + # Find the file and read it. + name = os.path.split(path)[-1] - if globs is None: - globs = {} + doc = open(path).read() + # Convert it to a test, and wrap it in a DocFileCase. test = DocTestParser().get_doctest(doc, globs, name, path, 0) - return DocFileCase(test, **options) def DocFileSuite(*paths, **kw): - """Creates a suite of doctest files. - - One or more text file paths are given as strings. These should - use "/" characters to separate path segments. Paths are relative - to the directory of the calling module, or relative to the package - passed as a keyword argument. + """A unittest suite for one or more doctest files. + + The path to each doctest file is given as a string; the + interpretation of that string depends on the keyword argument + "module_relative". A number of options may be provided as keyword arguments: + module_relative + If "module_relative" is True, then the given file paths are + interpreted as os-independent module-relative paths. By + default, these paths are relative to the calling module's + directory; but if the "package" argument is specified, then + they are relative to that package. To ensure os-independence, + "filename" should use "/" characters to separate path + segments, and may not be an absolute path (i.e., it may not + begin with "/"). + + If "module_relative" is False, then the given file paths are + interpreted as os-specific paths. These paths may be absolute + or relative (to the current working directory). + package - The name of a Python package. Text-file paths will be - interpreted relative to the directory containing this package. - The package may be supplied as a package object or as a dotted - package name. + A Python package or the name of a Python package whose directory + should be used as the base directory for module relative paths. + If "package" is not specified, then the calling module's + directory is used as the base directory for module relative + filenames. It is an error to specify "package" if + "module_relative" is False. setUp The name of a set-up function. This is called before running the @@ -2375,14 +2521,14 @@ def DocFileSuite(*paths, **kw): optionflags A set of doctest option flags expressed as an integer. - """ suite = unittest.TestSuite() # We do this here so that _normalize_module is called at the right # level. If it were called in DocFileTest, then this function # would be the caller and we might guess the package incorrectly. - kw['package'] = _normalize_module(kw.get('package')) + if kw.get('module_relative', True): + kw['package'] = _normalize_module(kw.get('package')) for path in paths: suite.addTest(DocFileTest(path, **kw)) diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py index 8c96b21..219540a 100644 --- a/Lib/test/test_doctest.py +++ b/Lib/test/test_doctest.py @@ -1829,8 +1829,9 @@ def test_DocFileSuite(): ... package=new.module('__main__')) >>> sys.argv = save_argv - Absolute paths may also be used; they should use the native - path separator (*not* '/'). + By setting `module_relative=False`, os-specific paths may be + used (including absolute paths and paths relative to the + working directory): >>> # Get the absolute path of the test package. >>> test_doctest_path = os.path.abspath(test.test_doctest.__file__) @@ -1839,10 +1840,17 @@ def test_DocFileSuite(): >>> # Use it to find the absolute path of test_doctest.txt. >>> test_file = os.path.join(test_pkg_path, 'test_doctest.txt') - >>> suite = doctest.DocFileSuite(test_file) + >>> suite = doctest.DocFileSuite(test_file, module_relative=False) >>> suite.run(unittest.TestResult()) + It is an error to specify `package` when `module_relative=False`: + + >>> suite = doctest.DocFileSuite(test_file, module_relative=False, + ... package='test') + Traceback (most recent call last): + ValueError: Package may only be specified for module-relative paths. + You can specify initial global variables: >>> suite = doctest.DocFileSuite('test_doctest.txt', @@ -1991,6 +1999,127 @@ def test_unittest_reportflags(): """ +def test_testfile(): r""" +Tests for the `testfile()` function. This function runs all the +doctest examples in a given file. In its simple invokation, it is +called with the name of a file, which is taken to be relative to the +calling module. The return value is (#failures, #tests). + + >>> doctest.testfile('test_doctest.txt') # doctest: +ELLIPSIS + ********************************************************************** + File "...", line 6, in test_doctest.txt + Failed example: + favorite_color + Exception raised: + ... + NameError: name 'favorite_color' is not defined + ********************************************************************** + 1 items had failures: + 1 of 2 in test_doctest.txt + ***Test Failed*** 1 failures. + (1, 2) + >>> doctest.master = None # Reset master. + +(Note: we'll be clearing doctest.master after each call to +`doctest.testfile`, to supress warnings about multiple tests with the +same name.) + +Globals may be specified with the `globs` and `extraglobs` parameters: + + >>> globs = {'favorite_color': 'blue'} + >>> doctest.testfile('test_doctest.txt', globs=globs) + (0, 2) + >>> doctest.master = None # Reset master. + + >>> extraglobs = {'favorite_color': 'red'} + >>> doctest.testfile('test_doctest.txt', globs=globs, + ... extraglobs=extraglobs) # doctest: +ELLIPSIS + ********************************************************************** + File "...", line 6, in test_doctest.txt + Failed example: + favorite_color + Expected: + 'blue' + Got: + 'red' + ********************************************************************** + 1 items had failures: + 1 of 2 in test_doctest.txt + ***Test Failed*** 1 failures. + (1, 2) + >>> doctest.master = None # Reset master. + +The file may be made relative to a given module or package, using the +optional `module_relative` parameter: + + >>> doctest.testfile('test_doctest.txt', globs=globs, + ... module_relative='test') + (0, 2) + >>> doctest.master = None # Reset master. + +Verbosity can be increased with the optional `verbose` paremter: + + >>> doctest.testfile('test_doctest.txt', globs=globs, verbose=True) + Trying: + favorite_color + Expecting: + 'blue' + ok + Trying: + if 1: + print 'a' + print + print 'b' + Expecting: + a + + b + ok + 1 items passed all tests: + 2 tests in test_doctest.txt + 2 tests in 1 items. + 2 passed and 0 failed. + Test passed. + (0, 2) + >>> doctest.master = None # Reset master. + +The name of the test may be specified with the optional `name` +parameter: + + >>> doctest.testfile('test_doctest.txt', name='newname') + ... # doctest: +ELLIPSIS + ********************************************************************** + File "...", line 6, in newname + ... + (1, 2) + >>> doctest.master = None # Reset master. + +The summary report may be supressed with the optional `report` +parameter: + + >>> doctest.testfile('test_doctest.txt', report=False) + ... # doctest: +ELLIPSIS + ********************************************************************** + File "...", line 6, in test_doctest.txt + Failed example: + favorite_color + Exception raised: + ... + NameError: name 'favorite_color' is not defined + (1, 2) + >>> doctest.master = None # Reset master. + +The optional keyword argument `raise_on_error` can be used to raise an +exception on the first error (which may be useful for postmortem +debugging): + + >>> doctest.testfile('test_doctest.txt', raise_on_error=True) + ... # doctest: +ELLIPSIS + Traceback (most recent call last): + UnexpectedException: ... + >>> doctest.master = None # Reset master. +""" + # old_test1, ... used to live in doctest.py, but cluttered it. Note # that these use the deprecated doctest.Tester, so should go away (or # be rewritten) someday. -- cgit v0.12