diff options
Diffstat (limited to 'sphinx/util')
| -rw-r--r-- | sphinx/util/__init__.py | 162 | ||||
| -rw-r--r-- | sphinx/util/compat.py | 65 | ||||
| -rw-r--r-- | sphinx/util/console.py | 15 | ||||
| -rw-r--r-- | sphinx/util/docstrings.py | 60 | ||||
| -rw-r--r-- | sphinx/util/jsdump.py | 2 | ||||
| -rw-r--r-- | sphinx/util/smartypants.py | 9 | ||||
| -rw-r--r-- | sphinx/util/stemmer.py | 15 | ||||
| -rw-r--r-- | sphinx/util/tags.py | 90 |
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("’", s) s = double_quote_start_re.sub("”", 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) |
