summaryrefslogtreecommitdiff
path: root/sphinx/util
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/util')
-rw-r--r--sphinx/util/__init__.py162
-rw-r--r--sphinx/util/compat.py65
-rw-r--r--sphinx/util/console.py15
-rw-r--r--sphinx/util/docstrings.py60
-rw-r--r--sphinx/util/jsdump.py2
-rw-r--r--sphinx/util/smartypants.py9
-rw-r--r--sphinx/util/stemmer.py15
-rw-r--r--sphinx/util/tags.py90
8 files changed, 396 insertions, 22 deletions
diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py
index 78e9e14a..c03016a4 100644
--- a/sphinx/util/__init__.py
+++ b/sphinx/util/__init__.py
@@ -13,8 +13,11 @@ import os
import re
import sys
import time
+import types
+import shutil
import fnmatch
import tempfile
+import posixpath
import traceback
from os import path
@@ -22,7 +25,7 @@ from os import path
# Generally useful regular expressions.
ws_re = re.compile(r'\s+')
caption_ref_re = re.compile(r'^([^<]+?)\s*<(.+)>$')
-
+url_re = re.compile(r'(?P<schema>.+)://.*')
# SEP separates path elements in the canonical file names
#
@@ -50,6 +53,11 @@ def relative_uri(base, to):
return ('..' + SEP) * (len(b2)-1) + SEP.join(t2)
+def docname_join(basedocname, docname):
+ return posixpath.normpath(
+ posixpath.join('/' + basedocname, '..', docname))[1:]
+
+
def ensuredir(path):
"""Ensure that a path exists."""
try:
@@ -276,9 +284,11 @@ def nested_parse_with_titles(state, content, node):
surrounding_section_level = state.memo.section_level
state.memo.title_styles = []
state.memo.section_level = 0
- state.nested_parse(content, 0, node, match_titles=1)
- state.memo.title_styles = surrounding_title_styles
- state.memo.section_level = surrounding_section_level
+ try:
+ return state.nested_parse(content, 0, node, match_titles=1)
+ finally:
+ state.memo.title_styles = surrounding_title_styles
+ state.memo.section_level = surrounding_section_level
def ustrftime(format, *args):
@@ -286,6 +296,93 @@ def ustrftime(format, *args):
return time.strftime(unicode(format).encode('utf-8'), *args).decode('utf-8')
+class Tee(object):
+ """
+ File-like object writing to two streams.
+ """
+ def __init__(self, stream1, stream2):
+ self.stream1 = stream1
+ self.stream2 = stream2
+
+ def write(self, text):
+ self.stream1.write(text)
+ self.stream2.write(text)
+
+
+class FilenameUniqDict(dict):
+ """
+ A dictionary that automatically generates unique names for its keys,
+ interpreted as filenames, and keeps track of a set of docnames they
+ appear in. Used for images and downloadable files in the environment.
+ """
+ def __init__(self):
+ self._existing = set()
+
+ def add_file(self, docname, newfile):
+ if newfile in self:
+ self[newfile][0].add(docname)
+ return self[newfile][1]
+ uniquename = path.basename(newfile)
+ base, ext = path.splitext(uniquename)
+ i = 0
+ while uniquename in self._existing:
+ i += 1
+ uniquename = '%s%s%s' % (base, i, ext)
+ self[newfile] = (set([docname]), uniquename)
+ self._existing.add(uniquename)
+ return uniquename
+
+ def purge_doc(self, docname):
+ for filename, (docs, _) in self.items():
+ docs.discard(docname)
+ #if not docs:
+ # del self[filename]
+ # self._existing.discard(filename)
+
+ def __getstate__(self):
+ return self._existing
+
+ def __setstate__(self, state):
+ self._existing = state
+
+
+def parselinenos(spec, total):
+ """
+ Parse a line number spec (such as "1,2,4-6") and return a list of
+ wanted line numbers.
+ """
+ items = list()
+ parts = spec.split(',')
+ for part in parts:
+ try:
+ begend = part.strip().split('-')
+ if len(begend) > 2:
+ raise ValueError
+ if len(begend) == 1:
+ items.append(int(begend[0])-1)
+ else:
+ start = (begend[0] == '') and 0 or int(begend[0])-1
+ end = (begend[1] == '') and total or int(begend[1])
+ items.extend(xrange(start, end))
+ except Exception, err:
+ raise ValueError('invalid line number spec: %r' % spec)
+ return items
+
+
+def force_decode(string, encoding):
+ if isinstance(string, str):
+ if encoding:
+ string = string.decode(encoding)
+ else:
+ try:
+ # try decoding with utf-8, should only work for real UTF-8
+ string = string.decode('utf-8')
+ except UnicodeError:
+ # last resort -- can't fail
+ string = string.decode('latin1')
+ return string
+
+
def movefile(source, dest):
# move a file, removing the destination if it exists
if os.path.exists(dest):
@@ -294,3 +391,60 @@ def movefile(source, dest):
except OSError:
pass
os.rename(source, dest)
+
+
+def copy_static_entry(source, target, builder, context={}):
+ if path.isfile(source):
+ if source.lower().endswith('_t'):
+ # templated!
+ fsrc = open(source, 'rb')
+ fdst = open(target[:-2], 'wb')
+ fdst.write(builder.templates.render_string(fsrc.read(), context))
+ fsrc.close()
+ fdst.close()
+ else:
+ shutil.copyfile(source, target)
+ elif path.isdir(source):
+ if filename in builder.config.exclude_dirnames:
+ return
+ if path.exists(target):
+ shutil.rmtree(target)
+ shutil.copytree(source, target)
+
+
+# monkey-patch Node.traverse to get more speed
+# traverse() is called so many times during a build that it saves
+# on average 20-25% overall build time!
+
+def _all_traverse(self):
+ """Version of Node.traverse() that doesn't need a condition."""
+ result = []
+ result.append(self)
+ for child in self.children:
+ result.extend(child._all_traverse())
+ return result
+
+def _fast_traverse(self, cls):
+ """Version of Node.traverse() that only supports instance checks."""
+ result = []
+ if isinstance(self, cls):
+ result.append(self)
+ for child in self.children:
+ result.extend(child._fast_traverse(cls))
+ return result
+
+def _new_traverse(self, condition=None,
+ include_self=1, descend=1, siblings=0, ascend=0):
+ if include_self and descend and not siblings and not ascend:
+ if condition is None:
+ return self._all_traverse()
+ elif isinstance(condition, (types.ClassType, type)):
+ return self._fast_traverse(condition)
+ return self._old_traverse(condition, include_self,
+ descend, siblings, ascend)
+
+import docutils.nodes
+docutils.nodes.Node._old_traverse = docutils.nodes.Node.traverse
+docutils.nodes.Node._all_traverse = _all_traverse
+docutils.nodes.Node._fast_traverse = _fast_traverse
+docutils.nodes.Node.traverse = _new_traverse
diff --git a/sphinx/util/compat.py b/sphinx/util/compat.py
index 4d5e1996..c00f9d4e 100644
--- a/sphinx/util/compat.py
+++ b/sphinx/util/compat.py
@@ -11,7 +11,6 @@
from docutils import nodes
-
# function missing in 0.5 SVN
def make_admonition(node_class, name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
@@ -27,7 +26,7 @@ def make_admonition(node_class, name, arguments, options, content, lineno,
textnodes, messages = state.inline_text(title_text, lineno)
admonition_node += nodes.title(title_text, '', *textnodes)
admonition_node += messages
- if options.has_key('class'):
+ if 'class' in options:
classes = options['class']
else:
classes = ['admonition-' + nodes.make_id(title_text)]
@@ -35,3 +34,65 @@ def make_admonition(node_class, name, arguments, options, content, lineno,
state.nested_parse(content, content_offset, admonition_node)
return [admonition_node]
+
+# support the class-style Directive interface even when using docutils 0.4
+
+try:
+ from docutils.parsers.rst import Directive
+
+except ImportError:
+ class Directive(object):
+ """
+ Fake Directive class to allow Sphinx directives to be written in
+ class style.
+ """
+ required_arguments = 0
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = None
+ has_content = False
+
+ def __init__(self, name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ self.name = name
+ self.arguments = arguments
+ self.options = options
+ self.content = content
+ self.lineno = lineno
+ self.content_offset = content_offset
+ self.block_text = block_text
+ self.state = state
+ self.state_machine = state_machine
+
+ def run(self):
+ raise NotImplementedError('Must override run() is subclass.')
+
+ def directive_dwim(obj):
+ """
+ Return something usable with register_directive(), regardless if
+ class or function. For that, we need to convert classes to a
+ function for docutils 0.4.
+ """
+ if isinstance(obj, type) and issubclass(obj, Directive):
+ def _class_directive(name, arguments, options, content,
+ lineno, content_offset, block_text,
+ state, state_machine):
+ return obj(name, arguments, options, content,
+ lineno, content_offset, block_text,
+ state, state_machine).run()
+ _class_directive.options = obj.option_spec
+ _class_directive.content = obj.has_content
+ _class_directive.arguments = (obj.required_arguments,
+ obj.optional_arguments,
+ obj.final_argument_whitespace)
+ return _class_directive
+ return obj
+
+else:
+ def directive_dwim(obj):
+ """
+ Return something usable with register_directive(), regardless if
+ class or function. Nothing to do here, because docutils 0.5 takes
+ care of converting functions itself.
+ """
+ return obj
diff --git a/sphinx/util/console.py b/sphinx/util/console.py
index 724dee10..083fc6f4 100644
--- a/sphinx/util/console.py
+++ b/sphinx/util/console.py
@@ -16,25 +16,26 @@ codes = {}
def get_terminal_width():
"""Borrowed from the py lib."""
try:
- import os, termios, fcntl, struct
- call = fcntl.ioctl(0, termios.TIOCGWINSZ, "\000"*8)
- height, width = struct.unpack("hhhh", call)[:2]
+ import termios, fcntl, struct
+ call = fcntl.ioctl(0, termios.TIOCGWINSZ,
+ struct.pack('hhhh', 0, 0, 0, 0))
+ height, width = struct.unpack('hhhh', call)[:2]
terminal_width = width
except (SystemExit, KeyboardInterrupt):
raise
except:
# FALLBACK
- terminal_width = int(os.environ.get('COLUMNS', 80))-1
+ terminal_width = int(os.environ.get('COLUMNS', 80)) - 1
return terminal_width
_tw = get_terminal_width()
-def print_and_backspace(text, func):
+def term_width_line(text):
if not codes:
# if no coloring, don't output fancy backspaces
- func(text)
+ return text + '\n'
else:
- func(text.ljust(_tw) + _tw * "\b")
+ return text.ljust(_tw) + '\r'
def color_terminal():
if 'COLORTERM' in os.environ:
diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py
new file mode 100644
index 00000000..ea03340a
--- /dev/null
+++ b/sphinx/util/docstrings.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.util.docstrings
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ Utilities for docstring processing.
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import sys
+
+
+def prepare_docstring(s):
+ """
+ Convert a docstring into lines of parseable reST. Return it as a list of
+ lines usable for inserting into a docutils ViewList (used as argument
+ of nested_parse().) An empty line is added to act as a separator between
+ this docstring and following content.
+ """
+ lines = s.expandtabs().splitlines()
+ # Find minimum indentation of any non-blank lines after first line.
+ margin = sys.maxint
+ for line in lines[1:]:
+ content = len(line.lstrip())
+ if content:
+ indent = len(line) - content
+ margin = min(margin, indent)
+ # Remove indentation.
+ if lines:
+ lines[0] = lines[0].lstrip()
+ if margin < sys.maxint:
+ for i in range(1, len(lines)): lines[i] = lines[i][margin:]
+ # Remove any leading blank lines.
+ while lines and not lines[0]:
+ lines.pop(0)
+ # make sure there is an empty line at the end
+ if lines and lines[-1]:
+ lines.append('')
+ return lines
+
+
+def prepare_commentdoc(s):
+ """
+ Extract documentation comment lines (starting with #:) and return them as a
+ list of lines. Returns an empty list if there is no documentation.
+ """
+ result = []
+ lines = [line.strip() for line in s.expandtabs().splitlines()]
+ for line in lines:
+ if line.startswith('#:'):
+ line = line[2:]
+ # the first space after the comment is ignored
+ if line and line[0] == ' ':
+ line = line[1:]
+ result.append(line)
+ if result and result[-1]:
+ result.append('')
+ return result
diff --git a/sphinx/util/jsdump.py b/sphinx/util/jsdump.py
index 2eb4619e..8c760b68 100644
--- a/sphinx/util/jsdump.py
+++ b/sphinx/util/jsdump.py
@@ -6,7 +6,7 @@
This module implements a simple JavaScript serializer.
Uses the basestring encode function from simplejson by Bob Ippolito.
- :copyright: Copyright 2008 by the Sphinx team, see AUTHORS.
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
diff --git a/sphinx/util/smartypants.py b/sphinx/util/smartypants.py
index 42f20f36..75888ea4 100644
--- a/sphinx/util/smartypants.py
+++ b/sphinx/util/smartypants.py
@@ -162,7 +162,8 @@ def educateQuotes(s):
"""
# Special case if the very first character is a quote
- # followed by punctuation at a non-word-break. Close the quotes by brute force:
+ # followed by punctuation at a non-word-break. Close the quotes
+ # by brute force:
s = single_quote_start_re.sub("&#8217;", s)
s = double_quote_start_re.sub("&#8221;", s)
@@ -200,7 +201,8 @@ def educateQuotesLatex(s, dquotes=("``", "''")):
"""
# Special case if the very first character is a quote
- # followed by punctuation at a non-word-break. Close the quotes by brute force:
+ # followed by punctuation at a non-word-break. Close the quotes
+ # by brute force:
s = single_quote_start_re.sub("\x04", s)
s = double_quote_start_re.sub("\x02", s)
@@ -300,4 +302,5 @@ __author__ = "Chad Miller <smartypantspy@chad.org>"
__version__ = "1.5_1.5: Sat, 13 Aug 2005 15:50:24 -0400"
__url__ = "http://wiki.chad.org/SmartyPantsPy"
__description__ = \
- "Smart-quotes, smart-ellipses, and smart-dashes for weblog entries in pyblosxom"
+ "Smart-quotes, smart-ellipses, and smart-dashes for weblog entries" \
+ " in pyblosxom"
diff --git a/sphinx/util/stemmer.py b/sphinx/util/stemmer.py
index 7eeb77b2..10ce9065 100644
--- a/sphinx/util/stemmer.py
+++ b/sphinx/util/stemmer.py
@@ -111,14 +111,16 @@ class PorterStemmer(object):
return self.cons(j)
def cvc(self, i):
- """cvc(i) is TRUE <=> i-2,i-1,i has the form consonant - vowel - consonant
+ """cvc(i) is TRUE <=> i-2,i-1,i has the form
+ consonant - vowel - consonant
and also if the second c is not w,x or y. this is used when trying to
restore an e at the end of a short e.g.
cav(e), lov(e), hop(e), crim(e), but
snow, box, tray.
"""
- if i < (self.k0 + 2) or not self.cons(i) or self.cons(i-1) or not self.cons(i-2):
+ if i < (self.k0 + 2) or not self.cons(i) or self.cons(i-1) \
+ or not self.cons(i-2):
return 0
ch = self.b[i]
if ch == 'w' or ch == 'x' or ch == 'y':
@@ -138,7 +140,8 @@ class PorterStemmer(object):
return 1
def setto(self, s):
- """setto(s) sets (j+1),...k to the characters in the string s, readjusting k."""
+ """setto(s) sets (j+1),...k to the characters in the string s,
+ readjusting k."""
length = len(s)
self.b = self.b[:self.j+1] + s + self.b[self.j+length+1:]
self.k = self.j + length
@@ -193,7 +196,8 @@ class PorterStemmer(object):
self.setto("e")
def step1c(self):
- """step1c() turns terminal y to i when there is another vowel in the stem."""
+ """step1c() turns terminal y to i when there is another vowel in
+ the stem."""
if (self.ends("y") and self.vowelinstem()):
self.b = self.b[:self.k] + 'i' + self.b[self.k+1:]
@@ -236,7 +240,8 @@ class PorterStemmer(object):
# To match the published algorithm, delete this phrase
def step3(self):
- """step3() dels with -ic-, -full, -ness etc. similar strategy to step2."""
+ """step3() dels with -ic-, -full, -ness etc. similar strategy
+ to step2."""
if self.b[self.k] == 'e':
if self.ends("icate"): self.r("ic")
elif self.ends("ative"): self.r("")
diff --git a/sphinx/util/tags.py b/sphinx/util/tags.py
new file mode 100644
index 00000000..c08e5e5a
--- /dev/null
+++ b/sphinx/util/tags.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.util.tags
+ ~~~~~~~~~~~~~~~~
+
+ :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import warnings
+# jinja2.sandbox imports the sets module on purpose
+warnings.filterwarnings('ignore', 'the sets module', DeprecationWarning,
+ module='jinja2.sandbox')
+
+# (ab)use the Jinja parser for parsing our boolean expressions
+from jinja2 import nodes
+from jinja2.parser import Parser
+from jinja2.environment import Environment
+
+env = Environment()
+
+
+class BooleanParser(Parser):
+ """
+ Only allow condition exprs and/or/not operations.
+ """
+
+ def parse_compare(self):
+ token = self.stream.current
+ if token.type == 'name':
+ if token.value in ('true', 'false', 'True', 'False'):
+ node = nodes.Const(token.value in ('true', 'True'),
+ lineno=token.lineno)
+ elif token.value in ('none', 'None'):
+ node = nodes.Const(None, lineno=token.lineno)
+ else:
+ node = nodes.Name(token.value, 'load', lineno=token.lineno)
+ self.stream.next()
+ elif token.type == 'lparen':
+ self.stream.next()
+ node = self.parse_expression()
+ self.stream.expect('rparen')
+ else:
+ self.fail("unexpected token '%s'" % (token,), token.lineno)
+ return node
+
+
+class Tags(object):
+ def __init__(self, tags=None):
+ self.tags = dict.fromkeys(tags or [], True)
+
+ def has(self, tag):
+ return tag in self.tags
+
+ __contains__ = has
+
+ def __iter__(self):
+ return iter(self.tags)
+
+ def add(self, tag):
+ self.tags[tag] = True
+
+ def remove(self, tag):
+ self.tags.pop(tag, None)
+
+ def eval_condition(self, condition):
+ # exceptions are handled by the caller
+ parser = BooleanParser(env, condition, state='variable')
+ expr = parser.parse_expression()
+ if not parser.stream.eos:
+ raise ValueError('chunk after expression')
+
+ def eval_node(node):
+ if isinstance(node, nodes.CondExpr):
+ if eval_node(node.test):
+ return eval_node(node.expr1)
+ else:
+ return eval_node(node.expr2)
+ elif isinstance(node, nodes.And):
+ return eval_node(node.left) and eval_node(node.right)
+ elif isinstance(node, nodes.Or):
+ return eval_node(node.left) or eval_node(node.right)
+ elif isinstance(node, nodes.Not):
+ return not eval_node(node.node)
+ elif isinstance(node, nodes.Name):
+ return self.tags.get(node.name, False)
+ else:
+ raise ValueError('invalid node, check parsing')
+
+ return eval_node(expr)