[Zope3-checkins] SVN: Zope3/trunk/src/zope/testing/doctest.py Integrated doctest from Python CVS head

Jim Fulton jim at zope.com
Mon Oct 25 17:48:26 EDT 2004


Log message for revision 28245:
  Integrated doctest from Python CVS head
  

Changed:
  U   Zope3/trunk/src/zope/testing/doctest.py

-=-
Modified: Zope3/trunk/src/zope/testing/doctest.py
===================================================================
--- Zope3/trunk/src/zope/testing/doctest.py	2004-10-25 21:24:15 UTC (rev 28244)
+++ Zope3/trunk/src/zope/testing/doctest.py	2004-10-25 21:48:26 UTC (rev 28245)
@@ -8,13 +8,11 @@
 
 r"""Module doctest -- a framework for running examples in docstrings.
 
-NORMAL USAGE
-
 In simplest use, end each module M to be tested with:
 
 def _test():
     import doctest
-    return doctest.testmod()
+    doctest.testmod()
 
 if __name__ == "__main__":
     _test()
@@ -40,133 +38,13 @@
 it by passing "verbose=False".  In either of those cases, sys.argv is not
 examined by testmod.
 
-In any case, testmod returns a 2-tuple of ints (f, t), where f is the
-number of docstring examples that failed and t is the total number of
-docstring examples attempted.
-
 There are a variety of other ways to run doctests, including integration
 with the unittest framework, and support for running non-Python text
 files containing doctests.  There are also many ways to override parts
 of doctest's default behaviors.  See the Library Reference Manual for
 details.
+"""
 
-
-WHICH DOCSTRINGS ARE EXAMINED?
-
-+ M.__doc__.
-
-+ f.__doc__ for all functions f in M.__dict__.values(), except those
-  defined in other modules.
-
-+ C.__doc__ for all classes C in M.__dict__.values(), except those
-  defined in other modules.
-
-+ If M.__test__ exists and "is true", it must be a dict, and
-  each entry maps a (string) name to a function object, class object, or
-  string.  Function and class object docstrings found from M.__test__
-  are searched, and strings are searched directly as if they were docstrings.
-  In output, a key K in M.__test__ appears with name
-      <name of M>.__test__.K
-
-Any classes found are recursively searched similarly, to test docstrings in
-their contained methods and nested classes.
-
-
-WHAT'S THE EXECUTION CONTEXT?
-
-By default, each time testmod finds a docstring to test, it uses a *copy*
-of M's globals (so that running tests on a module doesn't change the
-module's real globals, and so that one test in M can't leave behind crumbs
-that accidentally allow another test to work).  This means examples can
-freely use any names defined at top-level in M.  It also means that sloppy
-imports (see above) can cause examples in external docstrings to use
-globals inappropriate for them.
-
-You can force use of your own dict as the execution context by passing
-"globs=your_dict" to testmod instead.  Presumably this would be a copy of
-M.__dict__ merged with the globals from other imported modules.
-
-
-WHAT ABOUT EXCEPTIONS?
-
-No problem, as long as the only output generated by the example is the
-traceback itself.  For example:
-
-    >>> [1, 2, 3].remove(42)
-    Traceback (most recent call last):
-      File "<stdin>", line 1, in ?
-    ValueError: list.remove(x): x not in list
-    >>>
-
-Note that only the exception type and value are compared.
-
-
-SO WHAT DOES A DOCTEST EXAMPLE LOOK LIKE ALREADY!?
-
-Oh ya.  It's easy!  In most cases a copy-and-paste of an interactive
-console session works fine -- just make sure the leading whitespace is
-rigidly consistent (you can mix tabs and spaces if you're too lazy to do it
-right, but doctest is not in the business of guessing what you think a tab
-means).
-
-    >>> # comments are ignored
-    >>> x = 12
-    >>> x
-    12
-    >>> if x == 13:
-    ...     print "yes"
-    ... else:
-    ...     print "no"
-    ...     print "NO"
-    ...     print "NO!!!"
-    ...
-    no
-    NO
-    NO!!!
-    >>>
-
-Any expected output must immediately follow the final ">>>" or "..." line
-containing the code, and the expected output (if any) extends to the next
-">>>" or all-whitespace line.  That's it.
-
-Bummers:
-
-+ Output to stdout is captured, but not output to stderr (exception
-  tracebacks are captured via a different means).
-
-+ If you continue a line via backslashing in an interactive session,
-  or for any other reason use a backslash, you should use a raw
-  docstring, which will preserve your backslahses exactly as you type
-  them:
-
-      >>> def f(x):
-      ...     r'''Backslashes in a raw docstring: m\n'''
-      >>> print f.__doc__
-      Backslashes in a raw docstring: m\n
-
-  Otherwise, the backslash will be interpreted as part of the string.
-  E.g., the "\n" above would be interpreted as a newline character.
-  Alternatively, you can double each backslash in the doctest version
-  (and not use a raw string):
-
-      >>> def f(x):
-      ...     '''Backslashes in a raw docstring: m\\n'''
-      >>> print f.__doc__
-      Backslashes in a raw docstring: m\n
-
-The starting column doesn't matter:
-
->>> assert "Easy!"
-     >>> import math
-            >>> math.floor(1.9)
-            1.0
-
-and as many leading whitespace characters are stripped from the expected
-output as appeared in the initial ">>>" line that triggered it.
-
-If you execute this very file, the examples above will be found and
-executed.
-"""
 __docformat__ = 'reStructuredText en'
 
 __all__ = [
@@ -176,10 +54,13 @@
     'DONT_ACCEPT_BLANKLINE',
     'NORMALIZE_WHITESPACE',
     'ELLIPSIS',
+    'IGNORE_EXCEPTION_DETAIL',
+    'COMPARISON_FLAGS',
     'REPORT_UDIFF',
     'REPORT_CDIFF',
     'REPORT_NDIFF',
     'REPORT_ONLY_FIRST_FAILURE',
+    'REPORTING_FLAGS',
     # 1. Utility Functions
     'is_private',
     # 2. Example & DocTest
@@ -197,20 +78,18 @@
     'DebugRunner',
     # 6. Test Functions
     'testmod',
+    'testfile',
     'run_docstring_examples',
     # 7. Tester
     'Tester',
     # 8. Unittest Support
-    'DocTestCase',
     'DocTestSuite',
-    'DocFileCase',
-    'DocFileTest',
     'DocFileSuite',
+    'set_unittest_reportflags',
     # 9. Debugging Support
     'script_from_examples',
     'testsource',
     'debug_src',
-    'debug_script',
     'debug',
 ]
 
@@ -248,6 +127,7 @@
 #                            +---------+
 
 # Option constants.
+
 OPTIONFLAGS_BY_NAME = {}
 def register_optionflag(name):
     flag = 1 << len(OPTIONFLAGS_BY_NAME)
@@ -258,11 +138,24 @@
 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')
+
+COMPARISON_FLAGS = (DONT_ACCEPT_TRUE_FOR_1 |
+                    DONT_ACCEPT_BLANKLINE |
+                    NORMALIZE_WHITESPACE |
+                    ELLIPSIS |
+                    IGNORE_EXCEPTION_DETAIL)
+
 REPORT_UDIFF = register_optionflag('REPORT_UDIFF')
 REPORT_CDIFF = register_optionflag('REPORT_CDIFF')
 REPORT_NDIFF = register_optionflag('REPORT_NDIFF')
 REPORT_ONLY_FIRST_FAILURE = register_optionflag('REPORT_ONLY_FIRST_FAILURE')
 
+REPORTING_FLAGS = (REPORT_UDIFF |
+                   REPORT_CDIFF |
+                   REPORT_NDIFF |
+                   REPORT_ONLY_FIRST_FAILURE)
+
 # Special string markers for use in `want` strings:
 BLANKLINE_MARKER = '<BLANKLINE>'
 ELLIPSIS_MARKER = '...'
@@ -461,6 +354,31 @@
         # Restore stdout.
         sys.stdout = save_stdout
 
+# [XX] Normalize with respect to os.path.pardir?
+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
 ######################################################################
@@ -831,7 +749,7 @@
     """
 
     def __init__(self, verbose=False, parser=DocTestParser(),
-                 recurse=True, _namefilter=None):
+                 recurse=True, _namefilter=None, exclude_empty=True):
         """
         Create a new doctest finder.
 
@@ -843,10 +761,14 @@
 
         If the optional argument `recurse` is false, then `find` will
         only examine the given object, and not any contained objects.
+
+        If the optional argument `exclude_empty` is false, then `find`
+        will include tests for objects with empty docstrings.
         """
         self._parser = parser
         self._verbose = verbose
         self._recurse = recurse
+        self._exclude_empty = exclude_empty
         # _namefilter is undocumented, and exists only for temporary backward-
         # compatibility support of testmod's deprecated isprivate mess.
         self._namefilter = _namefilter
@@ -1002,7 +924,7 @@
                                      "must be strings, functions, methods, "
                                      "classes, or modules: %r" %
                                      (type(val),))
-                valname = '%s.%s' % (name, valname)
+                valname = '%s.__test__.%s' % (name, valname)
                 self._find(tests, val, valname, module, source_lines,
                            globs, seen)
 
@@ -1038,18 +960,21 @@
         else:
             try:
                 if obj.__doc__ is None:
-                    return None
-                docstring = str(obj.__doc__)
+                    docstring = ''
+                else:
+                    docstring = obj.__doc__
+                    if not isinstance(docstring, basestring):
+                        docstring = str(docstring)
             except (TypeError, AttributeError):
-                return None
+                docstring = ''
 
+        # Find the docstring's location in the file.
+        lineno = self._find_lineno(obj, source_lines)
+
         # Don't bother if the docstring is empty.
-        if not docstring:
+        if self._exclude_empty and not docstring:
             return None
 
-        # Find the docstring's location in the file.
-        lineno = self._find_lineno(obj, source_lines)
-
         # Return a DocTest for this object.
         if module is None:
             filename = None
@@ -1279,6 +1204,10 @@
         # to modify them).
         original_optionflags = self.optionflags
 
+        SUCCESS, FAILURE, BOOM = range(3) # `outcome` state
+
+        check = self._checker.check_output
+
         # Process each example.
         for examplenum, example in enumerate(test.examples):
 
@@ -1323,46 +1252,54 @@
 
             got = self._fakeout.getvalue()  # the actual output
             self._fakeout.truncate(0)
+            outcome = FAILURE   # guilty until proved innocent or insane
 
             # If the example executed without raising any exceptions,
-            # then verify its output and report its outcome.
+            # verify its output.
             if exception is None:
-                if self._checker.check_output(example.want, got,
-                                              self.optionflags):
-                    if not quiet:
-                        self.report_success(out, test, example, got)
-                else:
-                    if not quiet:
-                        self.report_failure(out, test, example, got)
-                    failures += 1
+                if check(example.want, got, self.optionflags):
+                    outcome = SUCCESS
 
-            # If the example raised an exception, then check if it was
-            # expected.
+            # The example raised an exception:  check if it was expected.
             else:
                 exc_info = sys.exc_info()
                 exc_msg = traceback.format_exception_only(*exc_info[:2])[-1]
+                if not quiet:
+                    got += _exception_traceback(exc_info)
 
-                # If `example.exc_msg` is None, then we weren't
-                # expecting an exception.
+                # If `example.exc_msg` is None, then we weren't expecting
+                # an exception.
                 if example.exc_msg is None:
-                    if not quiet:
-                        self.report_unexpected_exception(out, test, example,
-                                                         exc_info)
-                    failures += 1
-                # If `example.exc_msg` matches the actual exception
-                # message (`exc_msg`), then the example succeeds.
-                elif (self._checker.check_output(example.exc_msg, exc_msg,
-                                                 self.optionflags)):
-                    if not quiet:
-                        got += _exception_traceback(exc_info)
-                        self.report_success(out, test, example, got)
-                # Otherwise, the example fails.
-                else:
-                    if not quiet:
-                        got += _exception_traceback(exc_info)
-                        self.report_failure(out, test, example, got)
-                    failures += 1
+                    outcome = BOOM
 
+                # We expected an exception:  see whether it matches.
+                elif check(example.exc_msg, exc_msg, self.optionflags):
+                    outcome = SUCCESS
+
+                # Another chance if they didn't care about the detail.
+                elif self.optionflags & IGNORE_EXCEPTION_DETAIL:
+                    m1 = re.match(r'[^:]*:', example.exc_msg)
+                    m2 = re.match(r'[^:]*:', exc_msg)
+                    if m1 and m2 and check(m1.group(0), m2.group(0),
+                                           self.optionflags):
+                        outcome = SUCCESS
+
+            # Report the outcome.
+            if outcome is SUCCESS:
+                if not quiet:
+                    self.report_success(out, test, example, got)
+            elif outcome is FAILURE:
+                if not quiet:
+                    self.report_failure(out, test, example, got)
+                failures += 1
+            elif outcome is BOOM:
+                if not quiet:
+                    self.report_unexpected_exception(out, test, example,
+                                                     exc_info)
+                failures += 1
+            else:
+                assert False, ("unknown outcome", outcome)
+
         # Restore the option flags (in case they were modified)
         self.optionflags = original_optionflags
 
@@ -1502,6 +1439,20 @@
             print "Test passed."
         return totalf, totalt
 
+    #/////////////////////////////////////////////////////////////////
+    # Backward compatibility cruft to maintain doctest.master.
+    #/////////////////////////////////////////////////////////////////
+    def merge(self, other):
+        d = self._name2ft
+        for name, (f, t) in other._name2ft.items():
+            if name in d:
+                print "*** DocTestRunner.merge: '" + name + "' in both" \
+                    " testers; summing outcomes."
+                f2, t2 = d[name]
+                f = f + f2
+                t = t + t2
+            d[name] = f, t
+
 class OutputChecker:
     """
     A class used to check the whether the actual output from a doctest
@@ -1781,11 +1732,16 @@
 ######################################################################
 # These should be backwards compatible.
 
+# For backward compatibility, a global instance of a DocTestRunner
+# class, updated by testmod.
+master = None
+
 def testmod(m=None, name=None, globs=None, verbose=None, isprivate=None,
             report=True, optionflags=0, extraglobs=None,
-            raise_on_error=False):
+            raise_on_error=False, exclude_empty=False):
     """m=None, name=None, globs=None, verbose=None, isprivate=None,
-       report=True, optionflags=0, extraglobs=None
+       report=True, optionflags=0, extraglobs=None, raise_on_error=False,
+       exclude_empty=False
 
     Test examples in docstrings in functions and classes reachable
     from module m (or the current module if m is not supplied), starting
@@ -1828,6 +1784,7 @@
         DONT_ACCEPT_BLANKLINE
         NORMALIZE_WHITESPACE
         ELLIPSIS
+        IGNORE_EXCEPTION_DETAIL
         REPORT_UDIFF
         REPORT_CDIFF
         REPORT_NDIFF
@@ -1843,9 +1800,7 @@
     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
@@ -1854,6 +1809,8 @@
     displaying a summary.  Invoke doctest.master.summarize(verbose)
     when you're done fiddling.
     """
+    global master
+
     if isprivate is not None:
         warnings.warn("the isprivate argument is deprecated; "
                       "examine DocTestFinder.find() lists instead",
@@ -1875,7 +1832,7 @@
         name = m.__name__
 
     # Find, parse, and run all tests in the given module.
-    finder = DocTestFinder(_namefilter=isprivate)
+    finder = DocTestFinder(_namefilter=isprivate, exclude_empty=exclude_empty)
 
     if raise_on_error:
         runner = DebugRunner(verbose=verbose, optionflags=optionflags)
@@ -1888,8 +1845,131 @@
     if report:
         runner.summarize()
 
+    if master is None:
+        master = runner
+    else:
+        master.merge(runner)
+
     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, parser=DocTestParser()):
+    """
+    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 test; by default
+    use the file's basename.
+
+    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.
+
+    Optional keyword arg "parser" specifies a DocTestParser (or
+    subclass) that should be used to extract tests from the files.
+
+    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.basename(filename)
+
+    # 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 = parser.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):
     """
@@ -1928,8 +2008,7 @@
                       DeprecationWarning, stacklevel=2)
         if mod is None and globs is None:
             raise TypeError("Tester.__init__: must specify mod or globs")
-        from inspect import ismodule as _ismodule
-        if mod is not None and not _ismodule(mod):
+        if mod is not None and not inspect.ismodule(mod):
             raise TypeError("Tester.__init__: mod must be a module; %r" %
                             (mod,))
         if globs is None:
@@ -1973,35 +2052,22 @@
         import new
         m = new.module(name)
         m.__test__ = d
-        return self.rundoc(m, name, module)
+        return self.rundoc(m, name)
 
     def summarize(self, verbose=None):
         return self.testrunner.summarize(verbose)
 
     def merge(self, other):
-        d = self.testrunner._name2ft
-        for name, (f, t) in other.testrunner._name2ft.items():
-            if name in d:
-                print "*** Tester.merge: '" + name + "' in both" \
-                    " testers; summing outcomes."
-                f2, t2 = d[name]
-                f = f + f2
-                t = t + t2
-            d[name] = f, t
+        self.testrunner.merge(other.testrunner)
 
 ######################################################################
 ## 8. Unittest Support
 ######################################################################
 
 _unittest_reportflags = 0
-valid_unittest_reportflags = (
-    REPORT_CDIFF |
-    REPORT_UDIFF |
-    REPORT_NDIFF |
-    REPORT_ONLY_FIRST_FAILURE
-    )
+
 def set_unittest_reportflags(flags):
-    """Sets the unit test option flags
+    """Sets the unittest option flags.
 
     The old flag is returned so that a runner could restore the old
     value if it wished to:
@@ -2015,42 +2081,26 @@
       >>> doctest._unittest_reportflags == (REPORT_NDIFF |
       ...                                   REPORT_ONLY_FIRST_FAILURE)
       True
-      
+
     Only reporting flags can be set:
 
       >>> set_unittest_reportflags(ELLIPSIS)
       Traceback (most recent call last):
       ...
-      ValueError: ('Invalid flags passed', 8)
+      ValueError: ('Only reporting flags allowed', 8)
 
       >>> set_unittest_reportflags(old) == (REPORT_NDIFF |
       ...                                   REPORT_ONLY_FIRST_FAILURE)
       True
-
     """
+    global _unittest_reportflags
 
-    # extract the valid reporting flags:
-    rflags = flags & valid_unittest_reportflags
-
-    # Now remove these flags from the given flags
-    nrflags = flags ^ rflags
-
-    if nrflags:
-        raise ValueError("Invalid flags passed", flags)
-    
-    global _unittest_reportflags
+    if (flags & REPORTING_FLAGS) != flags:
+        raise ValueError("Only reporting flags allowed", flags)
     old = _unittest_reportflags
     _unittest_reportflags = flags
     return old
-    
 
-class FakeModule:
-    """Fake module created by tests
-    """
-    
-    def __init__(self, dict, name):
-        self.__dict__ = dict
-        self.__name__ = name
 
 class DocTestCase(unittest.TestCase):
 
@@ -2066,7 +2116,7 @@
 
     def setUp(self):
         test = self._dt_test
-            
+
         if self._dt_setUp is not None:
             self._dt_setUp(test)
 
@@ -2083,12 +2133,12 @@
         old = sys.stdout
         new = StringIO()
         optionflags = self._dt_optionflags
-        
-        if not (optionflags & valid_unittest_reportflags):
+
+        if not (optionflags & REPORTING_FLAGS):
             # The option flags don't include any reporting flags,
             # so add the default reporting flags
             optionflags |= _unittest_reportflags
-        
+
         runner = DocTestRunner(optionflags=optionflags,
                                checker=self._dt_checker, verbose=False)
 
@@ -2216,20 +2266,14 @@
 
     A number of options may be provided as keyword arguments:
 
-    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.
-
     setUp
-      The name of a set-up function.  This is called before running the
+      A set-up function.  This is called before running the
       tests in each file. The setUp function will be passed a DocTest
       object.  The setUp function can access the test globals as the
       globs attribute of the test passed.
 
     tearDown
-      The name of a tear-down function.  This is called after running the
+      A tear-down function.  This is called after running the
       tests in each file.  The tearDown function will be passed a DocTest
       object.  The tearDown function can access the test globals as the
       globs attribute of the test passed.
@@ -2281,44 +2325,67 @@
                 % (self._dt_test.name, self._dt_test.filename, err)
                 )
 
-def DocFileTest(path, package=None, globs=None, **options):
-    package = _normalize_module(package)
-    name = path.split('/')[-1]
-    dir = os.path.split(package.__file__)[0]
-    path = os.path.join(dir, *(path.split('/')))
-    doc = open(path).read()
-
+def DocFileTest(path, module_relative=True, package=None,
+                globs=None, parser=DocTestParser(), **options):
     if globs is None:
         globs = {}
 
-    test = DocTestParser().get_doctest(doc, globs, name, path, 0)
+    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)
+        path = _module_relative_path(package, path)
+
+    # Find the file and read it.
+    name = os.path.basename(path)
+    doc = open(path).read()
+
+    # Convert it to a test, and wrap it in a DocFileCase.
+    test = parser.get_doctest(doc, globs, name, path, 0)
     return DocFileCase(test, **options)
 
 def DocFileSuite(*paths, **kw):
-    """Creates a suite of doctest files.
+    """A unittest suite for one or more 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.
+    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
+      A set-up function.  This is called before running the
       tests in each file. The setUp function will be passed a DocTest
       object.  The setUp function can access the test globals as the
       globs attribute of the test passed.
 
     tearDown
-      The name of a tear-down function.  This is called after running the
+      A tear-down function.  This is called after running the
       tests in each file.  The tearDown function will be passed a DocTest
       object.  The tearDown function can access the test globals as the
       globs attribute of the test passed.
@@ -2327,15 +2394,19 @@
       A dictionary containing initial global variables for the tests.
 
     optionflags
-       A set of doctest option flags expressed as an integer.
-      
+      A set of doctest option flags expressed as an integer.
+
+    parser
+      A DocTestParser (or subclass) that should be used to extract
+      tests from the files.
     """
     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))



More information about the Zope3-Checkins mailing list