[Python-checkins] r63954 - in doctools/trunk: Makefile sphinx/application.py sphinx/builder.py sphinx/config.py sphinx/environment.py tests tests/path.py tests/root tests/root/Makefile tests/root/_static tests/root/_templates tests/root/conf.py tests/root/contents.txt tests/root/ext.py tests/run.py tests/test_config.py tests/util.py

georg.brandl python-checkins at python.org
Thu Jun 5 10:58:44 CEST 2008


Author: georg.brandl
Date: Thu Jun  5 10:58:43 2008
New Revision: 63954

Log:
Add a test suite skeleton, a first test for sphinx.config, and fix a bug in config.


Added:
   doctools/trunk/tests/
   doctools/trunk/tests/path.py   (contents, props changed)
   doctools/trunk/tests/root/
   doctools/trunk/tests/root/Makefile   (contents, props changed)
   doctools/trunk/tests/root/_static/
   doctools/trunk/tests/root/_templates/
   doctools/trunk/tests/root/conf.py   (contents, props changed)
   doctools/trunk/tests/root/contents.txt   (contents, props changed)
   doctools/trunk/tests/root/ext.py   (contents, props changed)
   doctools/trunk/tests/run.py   (contents, props changed)
   doctools/trunk/tests/test_config.py   (contents, props changed)
   doctools/trunk/tests/util.py   (contents, props changed)
Modified:
   doctools/trunk/Makefile
   doctools/trunk/sphinx/application.py
   doctools/trunk/sphinx/builder.py
   doctools/trunk/sphinx/config.py
   doctools/trunk/sphinx/environment.py

Modified: doctools/trunk/Makefile
==============================================================================
--- doctools/trunk/Makefile	(original)
+++ doctools/trunk/Makefile	Thu Jun  5 10:58:43 2008
@@ -25,3 +25,6 @@
 
 reindent:
 	@$(PYTHON) utils/reindent.py -r -B .
+
+test:
+	@cd tests; $(PYTHON) run.py

Modified: doctools/trunk/sphinx/application.py
==============================================================================
--- doctools/trunk/sphinx/application.py	(original)
+++ doctools/trunk/sphinx/application.py	Thu Jun  5 10:58:43 2008
@@ -53,6 +53,8 @@
     'html-page-context': 'pagename, context, doctree or None',
 }
 
+CONFIG_FILENAME = 'conf.py'
+
 class Sphinx(object):
 
     def __init__(self, srcdir, confdir, outdir, doctreedir, buildername,
@@ -74,7 +76,7 @@
         self._events = events.copy()
 
         # read config
-        self.config = Config(confdir, 'conf.py', confoverrides)
+        self.config = Config(confdir, CONFIG_FILENAME, confoverrides)
 
         # load all extension modules
         for extension in self.config.extensions:
@@ -176,9 +178,9 @@
         self.builderclasses[builder.name] = builder
 
     def add_config_value(self, name, default, rebuild_env):
-        if name in self.config.config_values:
+        if name in self.config.values:
             raise ExtensionError('Config value %r already present' % name)
-        self.config.config_values[name] = (default, rebuild_env)
+        self.config.values[name] = (default, rebuild_env)
 
     def add_event(self, name):
         if name in self._events:

Modified: doctools/trunk/sphinx/builder.py
==============================================================================
--- doctools/trunk/sphinx/builder.py	(original)
+++ doctools/trunk/sphinx/builder.py	Thu Jun  5 10:58:43 2008
@@ -127,7 +127,7 @@
         if not self.freshenv:
             try:
                 self.info(bold('trying to load pickled env... '), nonl=True)
-                self.env = BuildEnvironment.frompickle(
+                self.env = BuildEnvironment.frompickle(self.config,
                     path.join(self.doctreedir, ENV_PICKLE_FILENAME))
                 self.info('done')
             except Exception, err:

Modified: doctools/trunk/sphinx/config.py
==============================================================================
--- doctools/trunk/sphinx/config.py	(original)
+++ doctools/trunk/sphinx/config.py	Thu Jun  5 10:58:43 2008
@@ -79,6 +79,7 @@
 
     def __init__(self, dirname, filename, overrides):
         self.overrides = overrides
+        self.values = Config.config_values.copy()
         config = {'__file__': path.join(dirname, filename)}
         olddir = os.getcwd()
         try:
@@ -95,17 +96,17 @@
     def init_values(self):
         config = self._raw_config
         config.update(self.overrides)
-        for name in self._raw_config:
-            if name in self.config_values:
+        for name in config:
+            if name in self.values:
                 self.__dict__[name] = config[name]
         del self._raw_config
 
     def __getattr__(self, name):
         if name.startswith('_'):
             raise AttributeError(name)
-        if name not in self.config_values:
+        if name not in self.values:
             raise AttributeError('No such config value: %s' % name)
-        default = self.config_values[name][0]
+        default = self.values[name][0]
         if callable(default):
             return default(self)
         return default
@@ -120,4 +121,4 @@
         delattr(self, name)
 
     def __contains__(self, name):
-        return name in self.config_values
+        return name in self.values

Modified: doctools/trunk/sphinx/environment.py
==============================================================================
--- doctools/trunk/sphinx/environment.py	(original)
+++ doctools/trunk/sphinx/environment.py	Thu Jun  5 10:58:43 2008
@@ -163,12 +163,13 @@
     # --------- ENVIRONMENT PERSISTENCE ----------------------------------------
 
     @staticmethod
-    def frompickle(filename):
+    def frompickle(config, filename):
         picklefile = open(filename, 'rb')
         try:
             env = pickle.load(picklefile)
         finally:
             picklefile.close()
+        env.config.values = config.values
         if env.version != ENV_VERSION:
             raise IOError('env version not current')
         return env
@@ -177,6 +178,8 @@
         # remove unpicklable attributes
         warnfunc = self._warnfunc
         self.set_warnfunc(None)
+        values = self.config.values
+        del self.config.values
         picklefile = open(filename, 'wb')
         # remove potentially pickling-problematic values from config
         for key, val in vars(self.config).items():
@@ -189,7 +192,8 @@
             pickle.dump(self, picklefile, pickle.HIGHEST_PROTOCOL)
         finally:
             picklefile.close()
-        # reset stream
+        # reset attributes
+        self.config.values = values
         self.set_warnfunc(warnfunc)
 
     # --------- ENVIRONMENT INITIALIZATION -------------------------------------

Added: doctools/trunk/tests/path.py
==============================================================================
--- (empty file)
+++ doctools/trunk/tests/path.py	Thu Jun  5 10:58:43 2008
@@ -0,0 +1,970 @@
+""" path.py - An object representing a path to a file or directory.
+
+Example:
+
+from path import path
+d = path('/home/guido/bin')
+for f in d.files('*.py'):
+    f.chmod(0755)
+
+This module requires Python 2.2 or later.
+
+
+URL:     http://www.jorendorff.com/articles/python/path
+Author:  Jason Orendorff <jason.orendorff\x40gmail\x2ecom> (and others - see the url!)
+Date:    9 Mar 2007
+"""
+
+
+# TODO
+#   - Tree-walking functions don't avoid symlink loops.  Matt Harrison
+#     sent me a patch for this.
+#   - Bug in write_text().  It doesn't support Universal newline mode.
+#   - Better error message in listdir() when self isn't a
+#     directory. (On Windows, the error message really sucks.)
+#   - Make sure everything has a good docstring.
+#   - Add methods for regex find and replace.
+#   - guess_content_type() method?
+#   - Perhaps support arguments to touch().
+
+from __future__ import generators
+
+import sys, warnings, os, fnmatch, glob, shutil, codecs, md5
+
+__version__ = '2.2'
+__all__ = ['path']
+
+# Platform-specific support for path.owner
+if os.name == 'nt':
+    try:
+        import win32security
+    except ImportError:
+        win32security = None
+else:
+    try:
+        import pwd
+    except ImportError:
+        pwd = None
+
+# Pre-2.3 support.  Are unicode filenames supported?
+_base = str
+_getcwd = os.getcwd
+try:
+    if os.path.supports_unicode_filenames:
+        _base = unicode
+        _getcwd = os.getcwdu
+except AttributeError:
+    pass
+
+# Pre-2.3 workaround for booleans
+try:
+    True, False
+except NameError:
+    True, False = 1, 0
+
+# Pre-2.3 workaround for basestring.
+try:
+    basestring
+except NameError:
+    basestring = (str, unicode)
+
+# Universal newline support
+_textmode = 'r'
+if hasattr(file, 'newlines'):
+    _textmode = 'U'
+
+
+class TreeWalkWarning(Warning):
+    pass
+
+class path(_base):
+    """ Represents a filesystem path.
+
+    For documentation on individual methods, consult their
+    counterparts in os.path.
+    """
+
+    # --- Special Python methods.
+
+    def __repr__(self):
+        return 'path(%s)' % _base.__repr__(self)
+
+    # Adding a path and a string yields a path.
+    def __add__(self, more):
+        try:
+            resultStr = _base.__add__(self, more)
+        except TypeError:  #Python bug
+            resultStr = NotImplemented
+        if resultStr is NotImplemented:
+            return resultStr
+        return self.__class__(resultStr)
+
+    def __radd__(self, other):
+        if isinstance(other, basestring):
+            return self.__class__(other.__add__(self))
+        else:
+            return NotImplemented
+
+    # The / operator joins paths.
+    def __div__(self, rel):
+        """ fp.__div__(rel) == fp / rel == fp.joinpath(rel)
+
+        Join two path components, adding a separator character if
+        needed.
+        """
+        return self.__class__(os.path.join(self, rel))
+
+    # Make the / operator work even when true division is enabled.
+    __truediv__ = __div__
+
+    def getcwd(cls):
+        """ Return the current working directory as a path object. """
+        return cls(_getcwd())
+    getcwd = classmethod(getcwd)
+
+
+    # --- Operations on path strings.
+
+    isabs = os.path.isabs
+    def abspath(self):       return self.__class__(os.path.abspath(self))
+    def normcase(self):      return self.__class__(os.path.normcase(self))
+    def normpath(self):      return self.__class__(os.path.normpath(self))
+    def realpath(self):      return self.__class__(os.path.realpath(self))
+    def expanduser(self):    return self.__class__(os.path.expanduser(self))
+    def expandvars(self):    return self.__class__(os.path.expandvars(self))
+    def dirname(self):       return self.__class__(os.path.dirname(self))
+    basename = os.path.basename
+
+    def expand(self):
+        """ Clean up a filename by calling expandvars(),
+        expanduser(), and normpath() on it.
+
+        This is commonly everything needed to clean up a filename
+        read from a configuration file, for example.
+        """
+        return self.expandvars().expanduser().normpath()
+
+    def _get_namebase(self):
+        base, ext = os.path.splitext(self.name)
+        return base
+
+    def _get_ext(self):
+        f, ext = os.path.splitext(_base(self))
+        return ext
+
+    def _get_drive(self):
+        drive, r = os.path.splitdrive(self)
+        return self.__class__(drive)
+
+    parent = property(
+        dirname, None, None,
+        """ This path's parent directory, as a new path object.
+
+        For example, path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib')
+        """)
+
+    name = property(
+        basename, None, None,
+        """ The name of this file or directory without the full path.
+
+        For example, path('/usr/local/lib/libpython.so').name == 'libpython.so'
+        """)
+
+    namebase = property(
+        _get_namebase, None, None,
+        """ The same as path.name, but with one file extension stripped off.
+
+        For example, path('/home/guido/python.tar.gz').name     == 'python.tar.gz',
+        but          path('/home/guido/python.tar.gz').namebase == 'python.tar'
+        """)
+
+    ext = property(
+        _get_ext, None, None,
+        """ The file extension, for example '.py'. """)
+
+    drive = property(
+        _get_drive, None, None,
+        """ The drive specifier, for example 'C:'.
+        This is always empty on systems that don't use drive specifiers.
+        """)
+
+    def splitpath(self):
+        """ p.splitpath() -> Return (p.parent, p.name). """
+        parent, child = os.path.split(self)
+        return self.__class__(parent), child
+
+    def splitdrive(self):
+        """ p.splitdrive() -> Return (p.drive, <the rest of p>).
+
+        Split the drive specifier from this path.  If there is
+        no drive specifier, p.drive is empty, so the return value
+        is simply (path(''), p).  This is always the case on Unix.
+        """
+        drive, rel = os.path.splitdrive(self)
+        return self.__class__(drive), rel
+
+    def splitext(self):
+        """ p.splitext() -> Return (p.stripext(), p.ext).
+
+        Split the filename extension from this path and return
+        the two parts.  Either part may be empty.
+
+        The extension is everything from '.' to the end of the
+        last path segment.  This has the property that if
+        (a, b) == p.splitext(), then a + b == p.
+        """
+        filename, ext = os.path.splitext(self)
+        return self.__class__(filename), ext
+
+    def stripext(self):
+        """ p.stripext() -> Remove one file extension from the path.
+
+        For example, path('/home/guido/python.tar.gz').stripext()
+        returns path('/home/guido/python.tar').
+        """
+        return self.splitext()[0]
+
+    if hasattr(os.path, 'splitunc'):
+        def splitunc(self):
+            unc, rest = os.path.splitunc(self)
+            return self.__class__(unc), rest
+
+        def _get_uncshare(self):
+            unc, r = os.path.splitunc(self)
+            return self.__class__(unc)
+
+        uncshare = property(
+            _get_uncshare, None, None,
+            """ The UNC mount point for this path.
+            This is empty for paths on local drives. """)
+
+    def joinpath(self, *args):
+        """ Join two or more path components, adding a separator
+        character (os.sep) if needed.  Returns a new path
+        object.
+        """
+        return self.__class__(os.path.join(self, *args))
+
+    def splitall(self):
+        r""" Return a list of the path components in this path.
+
+        The first item in the list will be a path.  Its value will be
+        either os.curdir, os.pardir, empty, or the root directory of
+        this path (for example, '/' or 'C:\\').  The other items in
+        the list will be strings.
+
+        path.path.joinpath(*result) will yield the original path.
+        """
+        parts = []
+        loc = self
+        while loc != os.curdir and loc != os.pardir:
+            prev = loc
+            loc, child = prev.splitpath()
+            if loc == prev:
+                break
+            parts.append(child)
+        parts.append(loc)
+        parts.reverse()
+        return parts
+
+    def relpath(self):
+        """ Return this path as a relative path,
+        based from the current working directory.
+        """
+        cwd = self.__class__(os.getcwd())
+        return cwd.relpathto(self)
+
+    def relpathto(self, dest):
+        """ Return a relative path from self to dest.
+
+        If there is no relative path from self to dest, for example if
+        they reside on different drives in Windows, then this returns
+        dest.abspath().
+        """
+        origin = self.abspath()
+        dest = self.__class__(dest).abspath()
+
+        orig_list = origin.normcase().splitall()
+        # Don't normcase dest!  We want to preserve the case.
+        dest_list = dest.splitall()
+
+        if orig_list[0] != os.path.normcase(dest_list[0]):
+            # Can't get here from there.
+            return dest
+
+        # Find the location where the two paths start to differ.
+        i = 0
+        for start_seg, dest_seg in zip(orig_list, dest_list):
+            if start_seg != os.path.normcase(dest_seg):
+                break
+            i += 1
+
+        # Now i is the point where the two paths diverge.
+        # Need a certain number of "os.pardir"s to work up
+        # from the origin to the point of divergence.
+        segments = [os.pardir] * (len(orig_list) - i)
+        # Need to add the diverging part of dest_list.
+        segments += dest_list[i:]
+        if len(segments) == 0:
+            # If they happen to be identical, use os.curdir.
+            relpath = os.curdir
+        else:
+            relpath = os.path.join(*segments)
+        return self.__class__(relpath)
+
+    # --- Listing, searching, walking, and matching
+
+    def listdir(self, pattern=None):
+        """ D.listdir() -> List of items in this directory.
+
+        Use D.files() or D.dirs() instead if you want a listing
+        of just files or just subdirectories.
+
+        The elements of the list are path objects.
+
+        With the optional 'pattern' argument, this only lists
+        items whose names match the given pattern.
+        """
+        names = os.listdir(self)
+        if pattern is not None:
+            names = fnmatch.filter(names, pattern)
+        return [self / child for child in names]
+
+    def dirs(self, pattern=None):
+        """ D.dirs() -> List of this directory's subdirectories.
+
+        The elements of the list are path objects.
+        This does not walk recursively into subdirectories
+        (but see path.walkdirs).
+
+        With the optional 'pattern' argument, this only lists
+        directories whose names match the given pattern.  For
+        example, d.dirs('build-*').
+        """
+        return [p for p in self.listdir(pattern) if p.isdir()]
+
+    def files(self, pattern=None):
+        """ D.files() -> List of the files in this directory.
+
+        The elements of the list are path objects.
+        This does not walk into subdirectories (see path.walkfiles).
+
+        With the optional 'pattern' argument, this only lists files
+        whose names match the given pattern.  For example,
+        d.files('*.pyc').
+        """
+        
+        return [p for p in self.listdir(pattern) if p.isfile()]
+
+    def walk(self, pattern=None, errors='strict'):
+        """ D.walk() -> iterator over files and subdirs, recursively.
+
+        The iterator yields path objects naming each child item of
+        this directory and its descendants.  This requires that
+        D.isdir().
+
+        This performs a depth-first traversal of the directory tree.
+        Each directory is returned just before all its children.
+
+        The errors= keyword argument controls behavior when an
+        error occurs.  The default is 'strict', which causes an
+        exception.  The other allowed values are 'warn', which
+        reports the error via warnings.warn(), and 'ignore'.
+        """
+        if errors not in ('strict', 'warn', 'ignore'):
+            raise ValueError("invalid errors parameter")
+
+        try:
+            childList = self.listdir()
+        except Exception:
+            if errors == 'ignore':
+                return
+            elif errors == 'warn':
+                warnings.warn(
+                    "Unable to list directory '%s': %s"
+                    % (self, sys.exc_info()[1]),
+                    TreeWalkWarning)
+                return
+            else:
+                raise
+
+        for child in childList:
+            if pattern is None or child.fnmatch(pattern):
+                yield child
+            try:
+                isdir = child.isdir()
+            except Exception:
+                if errors == 'ignore':
+                    isdir = False
+                elif errors == 'warn':
+                    warnings.warn(
+                        "Unable to access '%s': %s"
+                        % (child, sys.exc_info()[1]),
+                        TreeWalkWarning)
+                    isdir = False
+                else:
+                    raise
+
+            if isdir:
+                for item in child.walk(pattern, errors):
+                    yield item
+
+    def walkdirs(self, pattern=None, errors='strict'):
+        """ D.walkdirs() -> iterator over subdirs, recursively.
+
+        With the optional 'pattern' argument, this yields only
+        directories whose names match the given pattern.  For
+        example, mydir.walkdirs('*test') yields only directories
+        with names ending in 'test'.
+
+        The errors= keyword argument controls behavior when an
+        error occurs.  The default is 'strict', which causes an
+        exception.  The other allowed values are 'warn', which
+        reports the error via warnings.warn(), and 'ignore'.
+        """
+        if errors not in ('strict', 'warn', 'ignore'):
+            raise ValueError("invalid errors parameter")
+
+        try:
+            dirs = self.dirs()
+        except Exception:
+            if errors == 'ignore':
+                return
+            elif errors == 'warn':
+                warnings.warn(
+                    "Unable to list directory '%s': %s"
+                    % (self, sys.exc_info()[1]),
+                    TreeWalkWarning)
+                return
+            else:
+                raise
+
+        for child in dirs:
+            if pattern is None or child.fnmatch(pattern):
+                yield child
+            for subsubdir in child.walkdirs(pattern, errors):
+                yield subsubdir
+
+    def walkfiles(self, pattern=None, errors='strict'):
+        """ D.walkfiles() -> iterator over files in D, recursively.
+
+        The optional argument, pattern, limits the results to files
+        with names that match the pattern.  For example,
+        mydir.walkfiles('*.tmp') yields only files with the .tmp
+        extension.
+        """
+        if errors not in ('strict', 'warn', 'ignore'):
+            raise ValueError("invalid errors parameter")
+
+        try:
+            childList = self.listdir()
+        except Exception:
+            if errors == 'ignore':
+                return
+            elif errors == 'warn':
+                warnings.warn(
+                    "Unable to list directory '%s': %s"
+                    % (self, sys.exc_info()[1]),
+                    TreeWalkWarning)
+                return
+            else:
+                raise
+
+        for child in childList:
+            try:
+                isfile = child.isfile()
+                isdir = not isfile and child.isdir()
+            except:
+                if errors == 'ignore':
+                    continue
+                elif errors == 'warn':
+                    warnings.warn(
+                        "Unable to access '%s': %s"
+                        % (self, sys.exc_info()[1]),
+                        TreeWalkWarning)
+                    continue
+                else:
+                    raise
+
+            if isfile:
+                if pattern is None or child.fnmatch(pattern):
+                    yield child
+            elif isdir:
+                for f in child.walkfiles(pattern, errors):
+                    yield f
+
+    def fnmatch(self, pattern):
+        """ Return True if self.name matches the given pattern.
+
+        pattern - A filename pattern with wildcards,
+            for example '*.py'.
+        """
+        return fnmatch.fnmatch(self.name, pattern)
+
+    def glob(self, pattern):
+        """ Return a list of path objects that match the pattern.
+
+        pattern - a path relative to this directory, with wildcards.
+
+        For example, path('/users').glob('*/bin/*') returns a list
+        of all the files users have in their bin directories.
+        """
+        cls = self.__class__
+        return [cls(s) for s in glob.glob(_base(self / pattern))]
+
+
+    # --- Reading or writing an entire file at once.
+
+    def open(self, mode='r'):
+        """ Open this file.  Return a file object. """
+        return file(self, mode)
+
+    def bytes(self):
+        """ Open this file, read all bytes, return them as a string. """
+        f = self.open('rb')
+        try:
+            return f.read()
+        finally:
+            f.close()
+
+    def write_bytes(self, bytes, append=False):
+        """ Open this file and write the given bytes to it.
+
+        Default behavior is to overwrite any existing file.
+        Call p.write_bytes(bytes, append=True) to append instead.
+        """
+        if append:
+            mode = 'ab'
+        else:
+            mode = 'wb'
+        f = self.open(mode)
+        try:
+            f.write(bytes)
+        finally:
+            f.close()
+
+    def text(self, encoding=None, errors='strict'):
+        r""" Open this file, read it in, return the content as a string.
+
+        This uses 'U' mode in Python 2.3 and later, so '\r\n' and '\r'
+        are automatically translated to '\n'.
+
+        Optional arguments:
+
+        encoding - The Unicode encoding (or character set) of
+            the file.  If present, the content of the file is
+            decoded and returned as a unicode object; otherwise
+            it is returned as an 8-bit str.
+        errors - How to handle Unicode errors; see help(str.decode)
+            for the options.  Default is 'strict'.
+        """
+        if encoding is None:
+            # 8-bit
+            f = self.open(_textmode)
+            try:
+                return f.read()
+            finally:
+                f.close()
+        else:
+            # Unicode
+            f = codecs.open(self, 'r', encoding, errors)
+            # (Note - Can't use 'U' mode here, since codecs.open
+            # doesn't support 'U' mode, even in Python 2.3.)
+            try:
+                t = f.read()
+            finally:
+                f.close()
+            return (t.replace(u'\r\n', u'\n')
+                     .replace(u'\r\x85', u'\n')
+                     .replace(u'\r', u'\n')
+                     .replace(u'\x85', u'\n')
+                     .replace(u'\u2028', u'\n'))
+
+    def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False):
+        r""" Write the given text to this file.
+
+        The default behavior is to overwrite any existing file;
+        to append instead, use the 'append=True' keyword argument.
+
+        There are two differences between path.write_text() and
+        path.write_bytes(): newline handling and Unicode handling.
+        See below.
+
+        Parameters:
+
+          - text - str/unicode - The text to be written.
+
+          - encoding - str - The Unicode encoding that will be used.
+            This is ignored if 'text' isn't a Unicode string.
+
+          - errors - str - How to handle Unicode encoding errors.
+            Default is 'strict'.  See help(unicode.encode) for the
+            options.  This is ignored if 'text' isn't a Unicode
+            string.
+
+          - linesep - keyword argument - str/unicode - The sequence of
+            characters to be used to mark end-of-line.  The default is
+            os.linesep.  You can also specify None; this means to
+            leave all newlines as they are in 'text'.
+
+          - append - keyword argument - bool - Specifies what to do if
+            the file already exists (True: append to the end of it;
+            False: overwrite it.)  The default is False.
+
+
+        --- Newline handling.
+
+        write_text() converts all standard end-of-line sequences
+        ('\n', '\r', and '\r\n') to your platform's default end-of-line
+        sequence (see os.linesep; on Windows, for example, the
+        end-of-line marker is '\r\n').
+
+        If you don't like your platform's default, you can override it
+        using the 'linesep=' keyword argument.  If you specifically want
+        write_text() to preserve the newlines as-is, use 'linesep=None'.
+
+        This applies to Unicode text the same as to 8-bit text, except
+        there are three additional standard Unicode end-of-line sequences:
+        u'\x85', u'\r\x85', and u'\u2028'.
+
+        (This is slightly different from when you open a file for
+        writing with fopen(filename, "w") in C or file(filename, 'w')
+        in Python.)
+
+
+        --- Unicode
+
+        If 'text' isn't Unicode, then apart from newline handling, the
+        bytes are written verbatim to the file.  The 'encoding' and
+        'errors' arguments are not used and must be omitted.
+
+        If 'text' is Unicode, it is first converted to bytes using the
+        specified 'encoding' (or the default encoding if 'encoding'
+        isn't specified).  The 'errors' argument applies only to this
+        conversion.
+
+        """
+        if isinstance(text, unicode):
+            if linesep is not None:
+                # Convert all standard end-of-line sequences to
+                # ordinary newline characters.
+                text = (text.replace(u'\r\n', u'\n')
+                            .replace(u'\r\x85', u'\n')
+                            .replace(u'\r', u'\n')
+                            .replace(u'\x85', u'\n')
+                            .replace(u'\u2028', u'\n'))
+                text = text.replace(u'\n', linesep)
+            if encoding is None:
+                encoding = sys.getdefaultencoding()
+            bytes = text.encode(encoding, errors)
+        else:
+            # It is an error to specify an encoding if 'text' is
+            # an 8-bit string.
+            assert encoding is None
+
+            if linesep is not None:
+                text = (text.replace('\r\n', '\n')
+                            .replace('\r', '\n'))
+                bytes = text.replace('\n', linesep)
+
+        self.write_bytes(bytes, append)
+
+    def lines(self, encoding=None, errors='strict', retain=True):
+        r""" Open this file, read all lines, return them in a list.
+
+        Optional arguments:
+            encoding - The Unicode encoding (or character set) of
+                the file.  The default is None, meaning the content
+                of the file is read as 8-bit characters and returned
+                as a list of (non-Unicode) str objects.
+            errors - How to handle Unicode errors; see help(str.decode)
+                for the options.  Default is 'strict'
+            retain - If true, retain newline characters; but all newline
+                character combinations ('\r', '\n', '\r\n') are
+                translated to '\n'.  If false, newline characters are
+                stripped off.  Default is True.
+
+        This uses 'U' mode in Python 2.3 and later.
+        """
+        if encoding is None and retain:
+            f = self.open(_textmode)
+            try:
+                return f.readlines()
+            finally:
+                f.close()
+        else:
+            return self.text(encoding, errors).splitlines(retain)
+
+    def write_lines(self, lines, encoding=None, errors='strict',
+                    linesep=os.linesep, append=False):
+        r""" Write the given lines of text to this file.
+
+        By default this overwrites any existing file at this path.
+
+        This puts a platform-specific newline sequence on every line.
+        See 'linesep' below.
+
+        lines - A list of strings.
+
+        encoding - A Unicode encoding to use.  This applies only if
+            'lines' contains any Unicode strings.
+
+        errors - How to handle errors in Unicode encoding.  This
+            also applies only to Unicode strings.
+
+        linesep - The desired line-ending.  This line-ending is
+            applied to every line.  If a line already has any
+            standard line ending ('\r', '\n', '\r\n', u'\x85',
+            u'\r\x85', u'\u2028'), that will be stripped off and
+            this will be used instead.  The default is os.linesep,
+            which is platform-dependent ('\r\n' on Windows, '\n' on
+            Unix, etc.)  Specify None to write the lines as-is,
+            like file.writelines().
+
+        Use the keyword argument append=True to append lines to the
+        file.  The default is to overwrite the file.  Warning:
+        When you use this with Unicode data, if the encoding of the
+        existing data in the file is different from the encoding
+        you specify with the encoding= parameter, the result is
+        mixed-encoding data, which can really confuse someone trying
+        to read the file later.
+        """
+        if append:
+            mode = 'ab'
+        else:
+            mode = 'wb'
+        f = self.open(mode)
+        try:
+            for line in lines:
+                isUnicode = isinstance(line, unicode)
+                if linesep is not None:
+                    # Strip off any existing line-end and add the
+                    # specified linesep string.
+                    if isUnicode:
+                        if line[-2:] in (u'\r\n', u'\x0d\x85'):
+                            line = line[:-2]
+                        elif line[-1:] in (u'\r', u'\n',
+                                           u'\x85', u'\u2028'):
+                            line = line[:-1]
+                    else:
+                        if line[-2:] == '\r\n':
+                            line = line[:-2]
+                        elif line[-1:] in ('\r', '\n'):
+                            line = line[:-1]
+                    line += linesep
+                if isUnicode:
+                    if encoding is None:
+                        encoding = sys.getdefaultencoding()
+                    line = line.encode(encoding, errors)
+                f.write(line)
+        finally:
+            f.close()
+
+    def read_md5(self):
+        """ Calculate the md5 hash for this file.
+
+        This reads through the entire file.
+        """
+        f = self.open('rb')
+        try:
+            m = md5.new()
+            while True:
+                d = f.read(8192)
+                if not d:
+                    break
+                m.update(d)
+        finally:
+            f.close()
+        return m.digest()
+
+    # --- Methods for querying the filesystem.
+
+    exists = os.path.exists
+    isdir = os.path.isdir
+    isfile = os.path.isfile
+    islink = os.path.islink
+    ismount = os.path.ismount
+
+    if hasattr(os.path, 'samefile'):
+        samefile = os.path.samefile
+
+    getatime = os.path.getatime
+    atime = property(
+        getatime, None, None,
+        """ Last access time of the file. """)
+
+    getmtime = os.path.getmtime
+    mtime = property(
+        getmtime, None, None,
+        """ Last-modified time of the file. """)
+
+    if hasattr(os.path, 'getctime'):
+        getctime = os.path.getctime
+        ctime = property(
+            getctime, None, None,
+            """ Creation time of the file. """)
+
+    getsize = os.path.getsize
+    size = property(
+        getsize, None, None,
+        """ Size of the file, in bytes. """)
+
+    if hasattr(os, 'access'):
+        def access(self, mode):
+            """ Return true if current user has access to this path.
+
+            mode - One of the constants os.F_OK, os.R_OK, os.W_OK, os.X_OK
+            """
+            return os.access(self, mode)
+
+    def stat(self):
+        """ Perform a stat() system call on this path. """
+        return os.stat(self)
+
+    def lstat(self):
+        """ Like path.stat(), but do not follow symbolic links. """
+        return os.lstat(self)
+
+    def get_owner(self):
+        r""" Return the name of the owner of this file or directory.
+
+        This follows symbolic links.
+
+        On Windows, this returns a name of the form ur'DOMAIN\User Name'.
+        On Windows, a group can own a file or directory.
+        """
+        if os.name == 'nt':
+            if win32security is None:
+                raise Exception("path.owner requires win32all to be installed")
+            desc = win32security.GetFileSecurity(
+                self, win32security.OWNER_SECURITY_INFORMATION)
+            sid = desc.GetSecurityDescriptorOwner()
+            account, domain, typecode = win32security.LookupAccountSid(None, sid)
+            return domain + u'\\' + account
+        else:
+            if pwd is None:
+                raise NotImplementedError("path.owner is not implemented on this platform.")
+            st = self.stat()
+            return pwd.getpwuid(st.st_uid).pw_name
+
+    owner = property(
+        get_owner, None, None,
+        """ Name of the owner of this file or directory. """)
+
+    if hasattr(os, 'statvfs'):
+        def statvfs(self):
+            """ Perform a statvfs() system call on this path. """
+            return os.statvfs(self)
+
+    if hasattr(os, 'pathconf'):
+        def pathconf(self, name):
+            return os.pathconf(self, name)
+
+
+    # --- Modifying operations on files and directories
+
+    def utime(self, times):
+        """ Set the access and modified times of this file. """
+        os.utime(self, times)
+
+    def chmod(self, mode):
+        os.chmod(self, mode)
+
+    if hasattr(os, 'chown'):
+        def chown(self, uid, gid):
+            os.chown(self, uid, gid)
+
+    def rename(self, new):
+        os.rename(self, new)
+
+    def renames(self, new):
+        os.renames(self, new)
+
+
+    # --- Create/delete operations on directories
+
+    def mkdir(self, mode=0777):
+        os.mkdir(self, mode)
+
+    def makedirs(self, mode=0777):
+        os.makedirs(self, mode)
+
+    def rmdir(self):
+        os.rmdir(self)
+
+    def removedirs(self):
+        os.removedirs(self)
+
+
+    # --- Modifying operations on files
+
+    def touch(self):
+        """ Set the access/modified times of this file to the current time.
+        Create the file if it does not exist.
+        """
+        fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0666)
+        os.close(fd)
+        os.utime(self, None)
+
+    def remove(self):
+        os.remove(self)
+
+    def unlink(self):
+        os.unlink(self)
+
+
+    # --- Links
+
+    if hasattr(os, 'link'):
+        def link(self, newpath):
+            """ Create a hard link at 'newpath', pointing to this file. """
+            os.link(self, newpath)
+
+    if hasattr(os, 'symlink'):
+        def symlink(self, newlink):
+            """ Create a symbolic link at 'newlink', pointing here. """
+            os.symlink(self, newlink)
+
+    if hasattr(os, 'readlink'):
+        def readlink(self):
+            """ Return the path to which this symbolic link points.
+
+            The result may be an absolute or a relative path.
+            """
+            return self.__class__(os.readlink(self))
+
+        def readlinkabs(self):
+            """ Return the path to which this symbolic link points.
+
+            The result is always an absolute path.
+            """
+            p = self.readlink()
+            if p.isabs():
+                return p
+            else:
+                return (self.parent / p).abspath()
+
+
+    # --- High-level functions from shutil
+
+    copyfile = shutil.copyfile
+    copymode = shutil.copymode
+    copystat = shutil.copystat
+    copy = shutil.copy
+    copy2 = shutil.copy2
+    copytree = shutil.copytree
+    if hasattr(shutil, 'move'):
+        move = shutil.move
+    rmtree = shutil.rmtree
+
+
+    # --- Special stuff from os
+
+    if hasattr(os, 'chroot'):
+        def chroot(self):
+            os.chroot(self)
+
+    if hasattr(os, 'startfile'):
+        def startfile(self):
+            os.startfile(self)
+

Added: doctools/trunk/tests/root/Makefile
==============================================================================
--- (empty file)
+++ doctools/trunk/tests/root/Makefile	Thu Jun  5 10:58:43 2008
@@ -0,0 +1,70 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  pickle    to make pickle files (usable by e.g. sphinx-web)"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview over all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+
+clean:
+	-rm -rf _build/*
+
+html:
+	mkdir -p _build/html _build/doctrees
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html
+	@echo
+	@echo "Build finished. The HTML pages are in _build/html."
+
+pickle:
+	mkdir -p _build/pickle _build/doctrees
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files or run"
+	@echo "  sphinx-web _build/pickle"
+	@echo "to start the sphinx-web server."
+
+web: pickle
+
+htmlhelp:
+	mkdir -p _build/htmlhelp _build/doctrees
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in _build/htmlhelp."
+
+latex:
+	mkdir -p _build/latex _build/doctrees
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in _build/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	mkdir -p _build/changes _build/doctrees
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes
+	@echo
+	@echo "The overview file is in _build/changes."
+
+linkcheck:
+	mkdir -p _build/linkcheck _build/doctrees
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in _build/linkcheck/output.txt."

Added: doctools/trunk/tests/root/conf.py
==============================================================================
--- (empty file)
+++ doctools/trunk/tests/root/conf.py	Thu Jun  5 10:58:43 2008
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+#
+# Sphinx Tests documentation build configuration file, created by
+# sphinx-quickstart on Wed Jun  4 23:49:58 2008.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# The contents of this file are pickled, so don't put values in the namespace
+# that aren't pickleable (module imports are okay, they're removed automatically).
+#
+# All configuration values have a default value; values that are commented out
+# serve to show the default value.
+
+import sys, os
+
+# If your extensions are in another directory, add it here. If the directory
+# is relative to the documentation root, use os.path.abspath to make it
+# absolute, like shown here.
+sys.path.append(os.path.abspath('.'))
+
+# General configuration
+# ---------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['ext']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.txt'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General substitutions.
+project = 'Sphinx Tests'
+copyright = '2008, Georg Brandl'
+
+# The default replacements for |version| and |release|, also used in various
+# other places throughout the built documents.
+#
+# The short X.Y version.
+version = '0.4'
+# The full version, including alpha/beta/rc tags.
+release = '0.4alpha1'
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directories, that shouldn't be searched
+# for source files.
+#exclude_dirs = []
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+
+# Options for HTML output
+# -----------------------
+
+# The style sheet to use for HTML and HTML Help pages. A file of that name
+# must exist either in Sphinx' static/ path, or in one of the custom paths
+# given in html_static_path.
+html_style = 'default.css'
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (within the static path) to place at the top of
+# the sidebar.
+#html_logo = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the reST sources are included in the HTML build as _sources/<name>.
+#html_copy_source = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'SphinxTestsdoc'
+
+
+# Options for LaTeX output
+# ------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, document class [howto/manual]).
+latex_documents = [
+  ('contents', 'SphinxTests.tex', 'Sphinx Tests Documentation', 'Georg Brandl', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+value_from_conf_py = 84
+
+
+def setup(app):
+    app.add_config_value('value_from_conf_py', 42, False)

Added: doctools/trunk/tests/root/contents.txt
==============================================================================
--- (empty file)
+++ doctools/trunk/tests/root/contents.txt	Thu Jun  5 10:58:43 2008
@@ -0,0 +1,19 @@
+.. Sphinx Tests documentation master file, created by sphinx-quickstart on Wed Jun  4 23:49:58 2008.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to Sphinx Tests's documentation!
+========================================
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+

Added: doctools/trunk/tests/root/ext.py
==============================================================================
--- (empty file)
+++ doctools/trunk/tests/root/ext.py	Thu Jun  5 10:58:43 2008
@@ -0,0 +1,4 @@
+# Test extension module
+
+def setup(app):
+    app.add_config_value('value_from_ext', [], False)

Added: doctools/trunk/tests/run.py
==============================================================================
--- (empty file)
+++ doctools/trunk/tests/run.py	Thu Jun  5 10:58:43 2008
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+    Sphinx unit test driver
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    This script runs the Sphinx unit test suite.
+
+    :copyright: 2008 by Georg Brandl.
+    :license: BSD.
+"""
+
+import sys
+from os import path
+
+# always test the sphinx package from this directory
+sys.path.insert(0, path.join(path.dirname(__file__), path.pardir))
+
+try:
+    import nose
+except ImportError:
+    print "The nose package is needed to run the Sphinx test suite."
+    sys.exit(1)
+
+print "Running Sphinx test suite..."
+nose.main()

Added: doctools/trunk/tests/test_config.py
==============================================================================
--- (empty file)
+++ doctools/trunk/tests/test_config.py	Thu Jun  5 10:58:43 2008
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+"""
+    test_config
+    ~~~~~~~~~~~
+
+    Test the sphinx.config.Config class and its handling in the
+    Application class.
+
+    :copyright: 2008 by Georg Brandl.
+    :license: BSD.
+"""
+
+from util import *
+
+from sphinx.application import ExtensionError
+
+
+def test_core_config():
+    overrides = {'master_doc': 'master', 'nonexisting_value': 'True'}
+    cfg = TestApp(confoverrides=overrides).config
+
+    # simple values
+    assert 'project' in cfg.__dict__
+    assert cfg.project == 'Sphinx Tests'
+    assert cfg.templates_path == ['_templates']
+
+    # overrides
+    assert cfg.master_doc == 'master'
+
+    # simple default values
+    assert 'exclude_dirs' not in cfg.__dict__
+    assert cfg.exclude_dirs == []
+    assert cfg.show_authors == False
+
+    # complex default values
+    assert 'html_title' not in cfg.__dict__
+    assert cfg.html_title == 'Sphinx Tests v0.4alpha1 documentation'
+
+    # complex default values mustn't raise
+    for valuename in cfg.config_values:
+        getattr(cfg, valuename)
+
+    # "contains" gives True both for set and unset values
+    assert 'project' in cfg
+    assert 'html_title' in cfg
+
+    # invalid values
+    raises(AttributeError, getattr, cfg, '_value')
+    raises(AttributeError, getattr, cfg, 'nonexisting_value')
+
+    # non-value attributes are deleted from the namespace
+    raises(AttributeError, getattr, cfg, 'sys')
+
+    # setting attributes
+    cfg.project = 'Foo'
+    assert cfg.project == 'Foo'
+
+    # alternative access via item interface
+    cfg['project'] = 'Sphinx Tests'
+    assert cfg['project'] == cfg.project == 'Sphinx Tests'
+
+
+def test_extension_values():
+    app = TestApp()
+    cfg = app.config
+
+    # default value
+    assert cfg.value_from_ext == []
+    # non-default value
+    assert cfg.value_from_conf_py == 84
+
+    # no duplicate values allowed
+    raises_msg(ExtensionError, 'already present', app.add_config_value,
+               'html_title', 'x', True)
+    raises_msg(ExtensionError, 'already present', app.add_config_value,
+               'value_from_ext', 'x', True)

Added: doctools/trunk/tests/util.py
==============================================================================
--- (empty file)
+++ doctools/trunk/tests/util.py	Thu Jun  5 10:58:43 2008
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+"""
+    Sphinx test suite utilities
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :copyright: 2008 by Georg Brandl.
+    :license: BSD.
+"""
+
+import sys
+import StringIO
+import tempfile
+
+from sphinx import application, builder
+
+from path import path
+
+
+__all__ = [
+    'raises', 'raises_msg',
+    'ErrorOutput', 'TestApp',
+    'with_tempdir', 'write_file',
+]
+
+
+def _excstr(exc):
+    if type(exc) is tuple:
+        return str(tuple(map(_excstr, exc)))
+    return exc.__name__
+
+def raises(exc, func, *args, **kwds):
+    """
+    Raise :exc:`AssertionError` if ``func(*args, **kwds)`` does not
+    raise *exc*.
+    """
+    try:
+        func(*args, **kwds)
+    except exc:
+        pass
+    else:
+        raise AssertionError('%s did not raise %s' %
+                             (func.__name__, _excstr(exc)))
+
+def raises_msg(exc, msg, func, *args, **kwds):
+    """
+    Raise :exc:`AssertionError` if ``func(*args, **kwds)`` does not
+    raise *exc*, and check if the message contains *msg*.
+    """
+    try:
+        func(*args, **kwds)
+    except exc, err:
+        assert msg in str(err)
+    else:
+        raise AssertionError('%s did not raise %s' %
+                             (func.__name__, _excstr(exc)))
+
+
+class ErrorOutput(object):
+    """
+    File-like object that raises :exc:`AssertionError` on ``write()``.
+    """
+    def __init__(self, name):
+        self.name = name
+
+    def write(self, text):
+        assert False, 'tried to write %r to %s' % (text, self.name)
+
+
+class TestApp(application.Sphinx):
+    """
+    A subclass of :class:`Sphinx` that runs on the test root, with some
+    better default values for the initialization parameters.
+    """
+
+    def __init__(self, srcdir=None, confdir=None, outdir=None, doctreedir=None,
+                 buildername='html', confoverrides=None, status=None, warning=None,
+                 freshenv=None, confname='conf.py'):
+
+        application.CONFIG_FILENAME = confname
+
+        if srcdir is None:
+            srcdir = path(__file__).parent.joinpath('root').abspath()
+        else:
+            srcdir = path(srcdir)
+        if confdir is None:
+            confdir = srcdir
+        if outdir is None:
+            outdir = srcdir.joinpath('_build', buildername)
+        if doctreedir is None:
+            doctreedir = srcdir.joinpath(srcdir, '_build', 'doctrees')
+        if confoverrides is None:
+            confoverrides = {}
+        if status is None:
+            status = StringIO.StringIO()
+        if warning is None:
+            warning = ErrorOutput('stderr')
+        if freshenv is None:
+            freshenv = True
+
+        application.Sphinx.__init__(self, srcdir, confdir, outdir, doctreedir,
+                                    buildername, confoverrides, status, warning,
+                                    freshenv)
+
+
+def with_tempdir(func):
+    def new_func():
+        tempdir = path(tempfile.mkdtemp())
+        func(tempdir)
+        tempdir.rmtree()
+    new_func.__name__ = func.__name__
+    return new_func
+
+
+def write_file(name, contents):
+    f = open(str(name), 'wb')
+    f.write(contents)
+    f.close()


More information about the Python-checkins mailing list