]> granicus.if.org Git - python/commitdiff
- Added "testfile" function, a simple function for running & verifying
authorEdward Loper <edloper@gradient.cis.upenn.edu>
Sun, 19 Sep 2004 17:19:33 +0000 (17:19 +0000)
committerEdward Loper <edloper@gradient.cis.upenn.edu>
Sun, 19 Sep 2004 17:19:33 +0000 (17:19 +0000)
  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
Lib/test/test_doctest.py

index 3414f4abce41783dd1968c14ca9be877e22ad8b3..a34b0a1928ab05e774682e375437cebfb0fdec7b 100644 (file)
@@ -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))
index 8c96b2122305f01a3960493c6dd16d3e00e751af..219540a39ad6b3af5e0074ec5febf1b79b1ed12e 100644 (file)
@@ -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())
          <unittest.TestResult run=1 errors=0 failures=1>
 
+       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
+        <BLANKLINE>
+        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.