mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Add a test suite skeleton, a first test for sphinx.config, and fix a bug in config.
This commit is contained in:
parent
d4185e6f80
commit
f4d5c0a380
3
Makefile
3
Makefile
@ -25,3 +25,6 @@ pylint:
|
|||||||
|
|
||||||
reindent:
|
reindent:
|
||||||
@$(PYTHON) utils/reindent.py -r -B .
|
@$(PYTHON) utils/reindent.py -r -B .
|
||||||
|
|
||||||
|
test:
|
||||||
|
@cd tests; $(PYTHON) run.py
|
||||||
|
@ -53,6 +53,8 @@ events = {
|
|||||||
'html-page-context': 'pagename, context, doctree or None',
|
'html-page-context': 'pagename, context, doctree or None',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CONFIG_FILENAME = 'conf.py'
|
||||||
|
|
||||||
class Sphinx(object):
|
class Sphinx(object):
|
||||||
|
|
||||||
def __init__(self, srcdir, confdir, outdir, doctreedir, buildername,
|
def __init__(self, srcdir, confdir, outdir, doctreedir, buildername,
|
||||||
@ -74,7 +76,7 @@ class Sphinx(object):
|
|||||||
self._events = events.copy()
|
self._events = events.copy()
|
||||||
|
|
||||||
# read config
|
# read config
|
||||||
self.config = Config(confdir, 'conf.py', confoverrides)
|
self.config = Config(confdir, CONFIG_FILENAME, confoverrides)
|
||||||
|
|
||||||
# load all extension modules
|
# load all extension modules
|
||||||
for extension in self.config.extensions:
|
for extension in self.config.extensions:
|
||||||
@ -176,9 +178,9 @@ class Sphinx(object):
|
|||||||
self.builderclasses[builder.name] = builder
|
self.builderclasses[builder.name] = builder
|
||||||
|
|
||||||
def add_config_value(self, name, default, rebuild_env):
|
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)
|
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):
|
def add_event(self, name):
|
||||||
if name in self._events:
|
if name in self._events:
|
||||||
|
@ -127,7 +127,7 @@ class Builder(object):
|
|||||||
if not self.freshenv:
|
if not self.freshenv:
|
||||||
try:
|
try:
|
||||||
self.info(bold('trying to load pickled env... '), nonl=True)
|
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))
|
path.join(self.doctreedir, ENV_PICKLE_FILENAME))
|
||||||
self.info('done')
|
self.info('done')
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
|
@ -79,6 +79,7 @@ class Config(object):
|
|||||||
|
|
||||||
def __init__(self, dirname, filename, overrides):
|
def __init__(self, dirname, filename, overrides):
|
||||||
self.overrides = overrides
|
self.overrides = overrides
|
||||||
|
self.values = Config.config_values.copy()
|
||||||
config = {'__file__': path.join(dirname, filename)}
|
config = {'__file__': path.join(dirname, filename)}
|
||||||
olddir = os.getcwd()
|
olddir = os.getcwd()
|
||||||
try:
|
try:
|
||||||
@ -95,17 +96,17 @@ class Config(object):
|
|||||||
def init_values(self):
|
def init_values(self):
|
||||||
config = self._raw_config
|
config = self._raw_config
|
||||||
config.update(self.overrides)
|
config.update(self.overrides)
|
||||||
for name in self._raw_config:
|
for name in config:
|
||||||
if name in self.config_values:
|
if name in self.values:
|
||||||
self.__dict__[name] = config[name]
|
self.__dict__[name] = config[name]
|
||||||
del self._raw_config
|
del self._raw_config
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if name.startswith('_'):
|
if name.startswith('_'):
|
||||||
raise AttributeError(name)
|
raise AttributeError(name)
|
||||||
if name not in self.config_values:
|
if name not in self.values:
|
||||||
raise AttributeError('No such config value: %s' % name)
|
raise AttributeError('No such config value: %s' % name)
|
||||||
default = self.config_values[name][0]
|
default = self.values[name][0]
|
||||||
if callable(default):
|
if callable(default):
|
||||||
return default(self)
|
return default(self)
|
||||||
return default
|
return default
|
||||||
@ -120,4 +121,4 @@ class Config(object):
|
|||||||
delattr(self, name)
|
delattr(self, name)
|
||||||
|
|
||||||
def __contains__(self, name):
|
def __contains__(self, name):
|
||||||
return name in self.config_values
|
return name in self.values
|
||||||
|
@ -163,12 +163,13 @@ class BuildEnvironment:
|
|||||||
# --------- ENVIRONMENT PERSISTENCE ----------------------------------------
|
# --------- ENVIRONMENT PERSISTENCE ----------------------------------------
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def frompickle(filename):
|
def frompickle(config, filename):
|
||||||
picklefile = open(filename, 'rb')
|
picklefile = open(filename, 'rb')
|
||||||
try:
|
try:
|
||||||
env = pickle.load(picklefile)
|
env = pickle.load(picklefile)
|
||||||
finally:
|
finally:
|
||||||
picklefile.close()
|
picklefile.close()
|
||||||
|
env.config.values = config.values
|
||||||
if env.version != ENV_VERSION:
|
if env.version != ENV_VERSION:
|
||||||
raise IOError('env version not current')
|
raise IOError('env version not current')
|
||||||
return env
|
return env
|
||||||
@ -177,6 +178,8 @@ class BuildEnvironment:
|
|||||||
# remove unpicklable attributes
|
# remove unpicklable attributes
|
||||||
warnfunc = self._warnfunc
|
warnfunc = self._warnfunc
|
||||||
self.set_warnfunc(None)
|
self.set_warnfunc(None)
|
||||||
|
values = self.config.values
|
||||||
|
del self.config.values
|
||||||
picklefile = open(filename, 'wb')
|
picklefile = open(filename, 'wb')
|
||||||
# remove potentially pickling-problematic values from config
|
# remove potentially pickling-problematic values from config
|
||||||
for key, val in vars(self.config).items():
|
for key, val in vars(self.config).items():
|
||||||
@ -189,7 +192,8 @@ class BuildEnvironment:
|
|||||||
pickle.dump(self, picklefile, pickle.HIGHEST_PROTOCOL)
|
pickle.dump(self, picklefile, pickle.HIGHEST_PROTOCOL)
|
||||||
finally:
|
finally:
|
||||||
picklefile.close()
|
picklefile.close()
|
||||||
# reset stream
|
# reset attributes
|
||||||
|
self.config.values = values
|
||||||
self.set_warnfunc(warnfunc)
|
self.set_warnfunc(warnfunc)
|
||||||
|
|
||||||
# --------- ENVIRONMENT INITIALIZATION -------------------------------------
|
# --------- ENVIRONMENT INITIALIZATION -------------------------------------
|
||||||
|
970
tests/path.py
Normal file
970
tests/path.py
Normal file
@ -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)
|
||||||
|
|
70
tests/root/Makefile
Normal file
70
tests/root/Makefile
Normal file
@ -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."
|
173
tests/root/conf.py
Normal file
173
tests/root/conf.py
Normal file
@ -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)
|
19
tests/root/contents.txt
Normal file
19
tests/root/contents.txt
Normal file
@ -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`
|
||||||
|
|
4
tests/root/ext.py
Normal file
4
tests/root/ext.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Test extension module
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.add_config_value('value_from_ext', [], False)
|
26
tests/run.py
Executable file
26
tests/run.py
Executable file
@ -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()
|
76
tests/test_config.py
Normal file
76
tests/test_config.py
Normal file
@ -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)
|
117
tests/util.py
Normal file
117
tests/util.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user