diff options
| author | Georg Brandl <georg@python.org> | 2009-03-16 23:56:42 +0100 |
|---|---|---|
| committer | Georg Brandl <georg@python.org> | 2009-03-16 23:56:42 +0100 |
| commit | f6e56aa1a6476048d93897a2e5a20aa376fcf1b4 (patch) | |
| tree | 3ccec41501f25e8386d626a52d8d7da09040ecdc /tests | |
| parent | 73d7e7c709394974494fb6000c6274f07e79cce5 (diff) | |
| parent | a1691a3424ae6b91743db7f37cf4fe86f7c93760 (diff) | |
| download | sphinx-0.6b1.tar.gz | |
merge with 0.50.6b1
Diffstat (limited to 'tests')
31 files changed, 2098 insertions, 381 deletions
diff --git a/tests/coverage.py b/tests/coverage.py new file mode 100755 index 00000000..148e573c --- /dev/null +++ b/tests/coverage.py @@ -0,0 +1,1163 @@ +#!/usr/bin/python +# +# Perforce Defect Tracking Integration Project +# <http://www.ravenbrook.com/project/p4dti/> +# +# COVERAGE.PY -- COVERAGE TESTING +# +# Gareth Rees, Ravenbrook Limited, 2001-12-04 +# Ned Batchelder, 2004-12-12 +# http://nedbatchelder.com/code/modules/coverage.html +# +# +# 1. INTRODUCTION +# +# This module provides coverage testing for Python code. +# +# The intended readership is all Python developers. +# +# This document is not confidential. +# +# See [GDR 2001-12-04a] for the command-line interface, programmatic +# interface and limitations. See [GDR 2001-12-04b] for requirements and +# design. + +r"""Usage: + +coverage.py -x [-p] MODULE.py [ARG1 ARG2 ...] + Execute module, passing the given command-line arguments, collecting + coverage data. With the -p option, write to a temporary file containing + the machine name and process ID. + +coverage.py -e + Erase collected coverage data. + +coverage.py -c + Collect data from multiple coverage files (as created by -p option above) + and store it into a single file representing the union of the coverage. + +coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ... + Report on the statement coverage for the given files. With the -m + option, show line numbers of the statements that weren't executed. + +coverage.py -a [-d dir] [-o dir1,dir2,...] FILE1 FILE2 ... + Make annotated copies of the given files, marking statements that + are executed with > and statements that are missed with !. With + the -d option, make the copies in that directory. Without the -d + option, make each copy in the same directory as the original. + +-o dir,dir2,... + Omit reporting or annotating files when their filename path starts with + a directory listed in the omit list. + e.g. python coverage.py -i -r -o c:\python23,lib\enthought\traits + +Coverage data is saved in the file .coverage by default. Set the +COVERAGE_FILE environment variable to save it somewhere else.""" + +__version__ = "2.85.20080914" # see detailed history at the end of this file. + +import compiler +import compiler.visitor +import glob +import os +import re +import string +import symbol +import sys +import threading +import token +import types +import zipimport +from socket import gethostname + +# Python version compatibility +try: + strclass = basestring # new to 2.3 +except: + strclass = str + +# 2. IMPLEMENTATION +# +# This uses the "singleton" pattern. +# +# The word "morf" means a module object (from which the source file can +# be deduced by suitable manipulation of the __file__ attribute) or a +# filename. +# +# When we generate a coverage report we have to canonicalize every +# filename in the coverage dictionary just in case it refers to the +# module we are reporting on. It seems a shame to throw away this +# information so the data in the coverage dictionary is transferred to +# the 'cexecuted' dictionary under the canonical filenames. +# +# The coverage dictionary is called "c" and the trace function "t". The +# reason for these short names is that Python looks up variables by name +# at runtime and so execution time depends on the length of variables! +# In the bottleneck of this application it's appropriate to abbreviate +# names to increase speed. + +class StatementFindingAstVisitor(compiler.visitor.ASTVisitor): + """ A visitor for a parsed Abstract Syntax Tree which finds executable + statements. + """ + def __init__(self, statements, excluded, suite_spots): + compiler.visitor.ASTVisitor.__init__(self) + self.statements = statements + self.excluded = excluded + self.suite_spots = suite_spots + self.excluding_suite = 0 + + def doRecursive(self, node): + for n in node.getChildNodes(): + self.dispatch(n) + + visitStmt = visitModule = doRecursive + + def doCode(self, node): + if hasattr(node, 'decorators') and node.decorators: + self.dispatch(node.decorators) + self.recordAndDispatch(node.code) + else: + self.doSuite(node, node.code) + + visitFunction = visitClass = doCode + + def getFirstLine(self, node): + # Find the first line in the tree node. + lineno = node.lineno + for n in node.getChildNodes(): + f = self.getFirstLine(n) + if lineno and f: + lineno = min(lineno, f) + else: + lineno = lineno or f + return lineno + + def getLastLine(self, node): + # Find the first line in the tree node. + lineno = node.lineno + for n in node.getChildNodes(): + lineno = max(lineno, self.getLastLine(n)) + return lineno + + def doStatement(self, node): + self.recordLine(self.getFirstLine(node)) + + visitAssert = visitAssign = visitAssTuple = visitPrint = \ + visitPrintnl = visitRaise = visitSubscript = visitDecorators = \ + doStatement + + def visitPass(self, node): + # Pass statements have weird interactions with docstrings. If this + # pass statement is part of one of those pairs, claim that the statement + # is on the later of the two lines. + l = node.lineno + if l: + lines = self.suite_spots.get(l, [l,l]) + self.statements[lines[1]] = 1 + + def visitDiscard(self, node): + # Discard nodes are statements that execute an expression, but then + # discard the results. This includes function calls, so we can't + # ignore them all. But if the expression is a constant, the statement + # won't be "executed", so don't count it now. + if node.expr.__class__.__name__ != 'Const': + self.doStatement(node) + + def recordNodeLine(self, node): + # Stmt nodes often have None, but shouldn't claim the first line of + # their children (because the first child might be an ignorable line + # like "global a"). + if node.__class__.__name__ != 'Stmt': + return self.recordLine(self.getFirstLine(node)) + else: + return 0 + + def recordLine(self, lineno): + # Returns a bool, whether the line is included or excluded. + if lineno: + # Multi-line tests introducing suites have to get charged to their + # keyword. + if lineno in self.suite_spots: + lineno = self.suite_spots[lineno][0] + # If we're inside an excluded suite, record that this line was + # excluded. + if self.excluding_suite: + self.excluded[lineno] = 1 + return 0 + # If this line is excluded, or suite_spots maps this line to + # another line that is exlcuded, then we're excluded. + elif self.excluded.has_key(lineno) or \ + self.suite_spots.has_key(lineno) and \ + self.excluded.has_key(self.suite_spots[lineno][1]): + return 0 + # Otherwise, this is an executable line. + else: + self.statements[lineno] = 1 + return 1 + return 0 + + default = recordNodeLine + + def recordAndDispatch(self, node): + self.recordNodeLine(node) + self.dispatch(node) + + def doSuite(self, intro, body, exclude=0): + exsuite = self.excluding_suite + if exclude or (intro and not self.recordNodeLine(intro)): + self.excluding_suite = 1 + self.recordAndDispatch(body) + self.excluding_suite = exsuite + + def doPlainWordSuite(self, prevsuite, suite): + # Finding the exclude lines for else's is tricky, because they aren't + # present in the compiler parse tree. Look at the previous suite, + # and find its last line. If any line between there and the else's + # first line are excluded, then we exclude the else. + lastprev = self.getLastLine(prevsuite) + firstelse = self.getFirstLine(suite) + for l in range(lastprev+1, firstelse): + if self.suite_spots.has_key(l): + self.doSuite(None, suite, exclude=self.excluded.has_key(l)) + break + else: + self.doSuite(None, suite) + + def doElse(self, prevsuite, node): + if node.else_: + self.doPlainWordSuite(prevsuite, node.else_) + + def visitFor(self, node): + self.doSuite(node, node.body) + self.doElse(node.body, node) + + visitWhile = visitFor + + def visitIf(self, node): + # The first test has to be handled separately from the rest. + # The first test is credited to the line with the "if", but the others + # are credited to the line with the test for the elif. + self.doSuite(node, node.tests[0][1]) + for t, n in node.tests[1:]: + self.doSuite(t, n) + self.doElse(node.tests[-1][1], node) + + def visitTryExcept(self, node): + self.doSuite(node, node.body) + for i in range(len(node.handlers)): + a, b, h = node.handlers[i] + if not a: + # It's a plain "except:". Find the previous suite. + if i > 0: + prev = node.handlers[i-1][2] + else: + prev = node.body + self.doPlainWordSuite(prev, h) + else: + self.doSuite(a, h) + self.doElse(node.handlers[-1][2], node) + + def visitTryFinally(self, node): + self.doSuite(node, node.body) + self.doPlainWordSuite(node.body, node.final) + + def visitWith(self, node): + self.doSuite(node, node.body) + + def visitGlobal(self, node): + # "global" statements don't execute like others (they don't call the + # trace function), so don't record their line numbers. + pass + +the_coverage = None + +class CoverageException(Exception): + pass + +class coverage: + # Name of the cache file (unless environment variable is set). + cache_default = ".coverage" + + # Environment variable naming the cache file. + cache_env = "COVERAGE_FILE" + + # A dictionary with an entry for (Python source file name, line number + # in that file) if that line has been executed. + c = {} + + # A map from canonical Python source file name to a dictionary in + # which there's an entry for each line number that has been + # executed. + cexecuted = {} + + # Cache of results of calling the analysis2() method, so that you can + # specify both -r and -a without doing double work. + analysis_cache = {} + + # Cache of results of calling the canonical_filename() method, to + # avoid duplicating work. + canonical_filename_cache = {} + + def __init__(self): + global the_coverage + if the_coverage: + raise CoverageException("Only one coverage object allowed.") + self.usecache = 1 + self.cache = None + self.parallel_mode = False + self.exclude_re = '' + self.nesting = 0 + self.cstack = [] + self.xstack = [] + self.relative_dir = self.abs_file(os.curdir)+os.sep + self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]') + + # t(f, x, y). This method is passed to sys.settrace as a trace function. + # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and + # the arguments and return value of the trace function. + # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code + # objects. + + def t(self, f, w, unused): #pragma: no cover + if w == 'line': + self.c[(f.f_code.co_filename, f.f_lineno)] = 1 + #-for c in self.cstack: + #- c[(f.f_code.co_filename, f.f_lineno)] = 1 + return self.t + + def help(self, error=None): #pragma: no cover + if error: + print error + print + print __doc__ + sys.exit(1) + + def command_line(self, argv, help_fn=None): + import getopt + help_fn = help_fn or self.help + settings = {} + optmap = { + '-a': 'annotate', + '-c': 'collect', + '-d:': 'directory=', + '-e': 'erase', + '-h': 'help', + '-i': 'ignore-errors', + '-m': 'show-missing', + '-p': 'parallel-mode', + '-r': 'report', + '-x': 'execute', + '-o:': 'omit=', + } + short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '') + long_opts = optmap.values() + options, args = getopt.getopt(argv, short_opts, long_opts) + for o, a in options: + if optmap.has_key(o): + settings[optmap[o]] = 1 + elif optmap.has_key(o + ':'): + settings[optmap[o + ':']] = a + elif o[2:] in long_opts: + settings[o[2:]] = 1 + elif o[2:] + '=' in long_opts: + settings[o[2:]+'='] = a + else: #pragma: no cover + pass # Can't get here, because getopt won't return anything unknown. + + if settings.get('help'): + help_fn() + + for i in ['erase', 'execute']: + for j in ['annotate', 'report', 'collect']: + if settings.get(i) and settings.get(j): + help_fn("You can't specify the '%s' and '%s' " + "options at the same time." % (i, j)) + + args_needed = (settings.get('execute') + or settings.get('annotate') + or settings.get('report')) + action = (settings.get('erase') + or settings.get('collect') + or args_needed) + if not action: + help_fn("You must specify at least one of -e, -x, -c, -r, or -a.") + if not args_needed and args: + help_fn("Unexpected arguments: %s" % " ".join(args)) + + self.parallel_mode = settings.get('parallel-mode') + self.get_ready() + + if settings.get('erase'): + self.erase() + if settings.get('execute'): + if not args: + help_fn("Nothing to do.") + sys.argv = args + self.start() + import __main__ + sys.path[0] = os.path.dirname(sys.argv[0]) + execfile(sys.argv[0], __main__.__dict__) + if settings.get('collect'): + self.collect() + if not args: + args = self.cexecuted.keys() + + ignore_errors = settings.get('ignore-errors') + show_missing = settings.get('show-missing') + directory = settings.get('directory=') + + omit = settings.get('omit=') + if omit is not None: + omit = [self.abs_file(p) for p in omit.split(',')] + else: + omit = [] + + if settings.get('report'): + self.report(args, show_missing, ignore_errors, omit_prefixes=omit) + if settings.get('annotate'): + self.annotate(args, directory, ignore_errors, omit_prefixes=omit) + + def use_cache(self, usecache, cache_file=None): + self.usecache = usecache + if cache_file and not self.cache: + self.cache_default = cache_file + + def get_ready(self, parallel_mode=False): + if self.usecache and not self.cache: + self.cache = os.environ.get(self.cache_env, self.cache_default) + if self.parallel_mode: + self.cache += "." + gethostname() + "." + str(os.getpid()) + self.restore() + self.analysis_cache = {} + + def start(self, parallel_mode=False): + self.get_ready() + if self.nesting == 0: #pragma: no cover + sys.settrace(self.t) + if hasattr(threading, 'settrace'): + threading.settrace(self.t) + self.nesting += 1 + + def stop(self): + self.nesting -= 1 + if self.nesting == 0: #pragma: no cover + sys.settrace(None) + if hasattr(threading, 'settrace'): + threading.settrace(None) + + def erase(self): + self.get_ready() + self.c = {} + self.analysis_cache = {} + self.cexecuted = {} + if self.cache and os.path.exists(self.cache): + os.remove(self.cache) + + def exclude(self, re): + if self.exclude_re: + self.exclude_re += "|" + self.exclude_re += "(" + re + ")" + + def begin_recursive(self): + self.cstack.append(self.c) + self.xstack.append(self.exclude_re) + + def end_recursive(self): + self.c = self.cstack.pop() + self.exclude_re = self.xstack.pop() + + # save(). Save coverage data to the coverage cache. + + def save(self): + if self.usecache and self.cache: + self.canonicalize_filenames() + cache = open(self.cache, 'wb') + import marshal + marshal.dump(self.cexecuted, cache) + cache.close() + + # restore(). Restore coverage data from the coverage cache (if it exists). + + def restore(self): + self.c = {} + self.cexecuted = {} + assert self.usecache + if os.path.exists(self.cache): + self.cexecuted = self.restore_file(self.cache) + + def restore_file(self, file_name): + try: + cache = open(file_name, 'rb') + import marshal + cexecuted = marshal.load(cache) + cache.close() + if isinstance(cexecuted, types.DictType): + return cexecuted + else: + return {} + except: + return {} + + # collect(). Collect data in multiple files produced by parallel mode + + def collect(self): + cache_dir, local = os.path.split(self.cache) + for f in os.listdir(cache_dir or '.'): + if not f.startswith(local): + continue + + full_path = os.path.join(cache_dir, f) + cexecuted = self.restore_file(full_path) + self.merge_data(cexecuted) + + def merge_data(self, new_data): + for file_name, file_data in new_data.items(): + if self.cexecuted.has_key(file_name): + self.merge_file_data(self.cexecuted[file_name], file_data) + else: + self.cexecuted[file_name] = file_data + + def merge_file_data(self, cache_data, new_data): + for line_number in new_data.keys(): + if not cache_data.has_key(line_number): + cache_data[line_number] = new_data[line_number] + + def abs_file(self, filename): + """ Helper function to turn a filename into an absolute normalized + filename. + """ + return os.path.normcase(os.path.abspath(os.path.realpath(filename))) + + def get_zip_data(self, filename): + """ Get data from `filename` if it is a zip file path, or return None + if it is not. + """ + markers = ['.zip'+os.sep, '.egg'+os.sep] + for marker in markers: + if marker in filename: + parts = filename.split(marker) + try: + zi = zipimport.zipimporter(parts[0]+marker[:-1]) + except zipimport.ZipImportError: + continue + try: + data = zi.get_data(parts[1]) + except IOError: + continue + return data + return None + + # canonical_filename(filename). Return a canonical filename for the + # file (that is, an absolute path with no redundant components and + # normalized case). See [GDR 2001-12-04b, 3.3]. + + def canonical_filename(self, filename): + if not self.canonical_filename_cache.has_key(filename): + f = filename + if os.path.isabs(f) and not os.path.exists(f): + if not self.get_zip_data(f): + f = os.path.basename(f) + if not os.path.isabs(f): + for path in [os.curdir] + sys.path: + g = os.path.join(path, f) + if os.path.exists(g): + f = g + break + cf = self.abs_file(f) + self.canonical_filename_cache[filename] = cf + return self.canonical_filename_cache[filename] + + # canonicalize_filenames(). Copy results from "c" to "cexecuted", + # canonicalizing filenames on the way. Clear the "c" map. + + def canonicalize_filenames(self): + for filename, lineno in self.c.keys(): + if filename == '<string>': + # Can't do anything useful with exec'd strings, so skip them. + continue + f = self.canonical_filename(filename) + if not self.cexecuted.has_key(f): + self.cexecuted[f] = {} + self.cexecuted[f][lineno] = 1 + self.c = {} + + # morf_filename(morf). Return the filename for a module or file. + + def morf_filename(self, morf): + if hasattr(morf, '__file__'): + f = morf.__file__ + else: + f = morf + return self.canonical_filename(f) + + # analyze_morf(morf). Analyze the module or filename passed as + # the argument. If the source code can't be found, raise an error. + # Otherwise, return a tuple of (1) the canonical filename of the + # source code for the module, (2) a list of lines of statements + # in the source code, (3) a list of lines of excluded statements, + # and (4), a map of line numbers to multi-line line number ranges, for + # statements that cross lines. + + def analyze_morf(self, morf): + if self.analysis_cache.has_key(morf): + return self.analysis_cache[morf] + filename = self.morf_filename(morf) + ext = os.path.splitext(filename)[1] + source, sourcef = None, None + if ext == '.pyc': + if not os.path.exists(filename[:-1]): + source = self.get_zip_data(filename[:-1]) + if not source: + raise CoverageException( + "No source for compiled code '%s'." % filename + ) + filename = filename[:-1] + if not source: + sourcef = open(filename, 'rU') + source = sourcef.read() + try: + lines, excluded_lines, line_map = self.find_executable_statements( + source, exclude=self.exclude_re + ) + except SyntaxError, synerr: + raise CoverageException( + "Couldn't parse '%s' as Python source: '%s' at line %d" % + (filename, synerr.msg, synerr.lineno) + ) + if sourcef: + sourcef.close() + result = filename, lines, excluded_lines, line_map + self.analysis_cache[morf] = result + return result + + def first_line_of_tree(self, tree): + while True: + if len(tree) == 3 and type(tree[2]) == type(1): + return tree[2] + tree = tree[1] + + def last_line_of_tree(self, tree): + while True: + if len(tree) == 3 and type(tree[2]) == type(1): + return tree[2] + tree = tree[-1] + + def find_docstring_pass_pair(self, tree, spots): + for i in range(1, len(tree)): + if self.is_string_constant(tree[i]) and self.is_pass_stmt(tree[i+1]): + first_line = self.first_line_of_tree(tree[i]) + last_line = self.last_line_of_tree(tree[i+1]) + self.record_multiline(spots, first_line, last_line) + + def is_string_constant(self, tree): + try: + return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.expr_stmt + except: + return False + + def is_pass_stmt(self, tree): + try: + return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.pass_stmt + except: + return False + + def record_multiline(self, spots, i, j): + for l in range(i, j+1): + spots[l] = (i, j) + + def get_suite_spots(self, tree, spots): + """ Analyze a parse tree to find suite introducers which span a number + of lines. + """ + for i in range(1, len(tree)): + if type(tree[i]) == type(()): + if tree[i][0] == symbol.suite: + # Found a suite, look back for the colon and keyword. + lineno_colon = lineno_word = None + for j in range(i-1, 0, -1): + if tree[j][0] == token.COLON: + # Colons are never executed themselves: we want the + # line number of the last token before the colon. + lineno_colon = self.last_line_of_tree(tree[j-1]) + elif tree[j][0] == token.NAME: + if tree[j][1] == 'elif': + # Find the line number of the first non-terminal + # after the keyword. + t = tree[j+1] + while t and token.ISNONTERMINAL(t[0]): + t = t[1] + if t: + lineno_word = t[2] + else: + lineno_word = tree[j][2] + break + elif tree[j][0] == symbol.except_clause: + # "except" clauses look like: + # ('except_clause', ('NAME', 'except', lineno), ...) + if tree[j][1][0] == token.NAME: + lineno_word = tree[j][1][2] + break + if lineno_colon and lineno_word: + # Found colon and keyword, mark all the lines + # between the two with the two line numbers. + self.record_multiline(spots, lineno_word, lineno_colon) + + # "pass" statements are tricky: different versions of Python + # treat them differently, especially in the common case of a + # function with a doc string and a single pass statement. + self.find_docstring_pass_pair(tree[i], spots) + + elif tree[i][0] == symbol.simple_stmt: + first_line = self.first_line_of_tree(tree[i]) + last_line = self.last_line_of_tree(tree[i]) + if first_line != last_line: + self.record_multiline(spots, first_line, last_line) + self.get_suite_spots(tree[i], spots) + + def find_executable_statements(self, text, exclude=None): + # Find lines which match an exclusion pattern. + excluded = {} + suite_spots = {} + if exclude: + reExclude = re.compile(exclude) + lines = text.split('\n') + for i in range(len(lines)): + if reExclude.search(lines[i]): + excluded[i+1] = 1 + + # Parse the code and analyze the parse tree to find out which statements + # are multiline, and where suites begin and end. + import parser + tree = parser.suite(text+'\n\n').totuple(1) + self.get_suite_spots(tree, suite_spots) + #print "Suite spots:", suite_spots + + # Use the compiler module to parse the text and find the executable + # statements. We add newlines to be impervious to final partial lines. + statements = {} + ast = compiler.parse(text+'\n\n') + visitor = StatementFindingAstVisitor(statements, excluded, suite_spots) + compiler.walk(ast, visitor, walker=visitor) + + lines = statements.keys() + lines.sort() + excluded_lines = excluded.keys() + excluded_lines.sort() + return lines, excluded_lines, suite_spots + + # format_lines(statements, lines). Format a list of line numbers + # for printing by coalescing groups of lines as long as the lines + # represent consecutive statements. This will coalesce even if + # there are gaps between statements, so if statements = + # [1,2,3,4,5,10,11,12,13,14] and lines = [1,2,5,10,11,13,14] then + # format_lines will return "1-2, 5-11, 13-14". + + def format_lines(self, statements, lines): + pairs = [] + i = 0 + j = 0 + start = None + pairs = [] + while i < len(statements) and j < len(lines): + if statements[i] == lines[j]: + if start == None: + start = lines[j] + end = lines[j] + j = j + 1 + elif start: + pairs.append((start, end)) + start = None + i = i + 1 + if start: + pairs.append((start, end)) + def stringify(pair): + start, end = pair + if start == end: + return "%d" % start + else: + return "%d-%d" % (start, end) + ret = string.join(map(stringify, pairs), ", ") + return ret + + # Backward compatibility with version 1. + def analysis(self, morf): + f, s, _, m, mf = self.analysis2(morf) + return f, s, m, mf + + def analysis2(self, morf): + filename, statements, excluded, line_map = self.analyze_morf(morf) + self.canonicalize_filenames() + if not self.cexecuted.has_key(filename): + self.cexecuted[filename] = {} + missing = [] + for line in statements: + lines = line_map.get(line, [line, line]) + for l in range(lines[0], lines[1]+1): + if self.cexecuted[filename].has_key(l): + break + else: + missing.append(line) + return (filename, statements, excluded, missing, + self.format_lines(statements, missing)) + + def relative_filename(self, filename): + """ Convert filename to relative filename from self.relative_dir. + """ + return filename.replace(self.relative_dir, "") + + def morf_name(self, morf): + """ Return the name of morf as used in report. + """ + if hasattr(morf, '__name__'): + return morf.__name__ + else: + return self.relative_filename(os.path.splitext(morf)[0]) + + def filter_by_prefix(self, morfs, omit_prefixes): + """ Return list of morfs where the morf name does not begin + with any one of the omit_prefixes. + """ + filtered_morfs = [] + for morf in morfs: + for prefix in omit_prefixes: + if self.morf_name(morf).startswith(prefix): + break + else: + filtered_morfs.append(morf) + + return filtered_morfs + + def morf_name_compare(self, x, y): + return cmp(self.morf_name(x), self.morf_name(y)) + + def report(self, morfs, show_missing=1, ignore_errors=0, file=None, omit_prefixes=[]): + if not isinstance(morfs, types.ListType): + morfs = [morfs] + # On windows, the shell doesn't expand wildcards. Do it here. + globbed = [] + for morf in morfs: + if isinstance(morf, strclass): + globbed.extend(glob.glob(morf)) + else: + globbed.append(morf) + morfs = globbed + + morfs = self.filter_by_prefix(morfs, omit_prefixes) + morfs.sort(self.morf_name_compare) + + max_name = max([5,] + map(len, map(self.morf_name, morfs))) + fmt_name = "%%- %ds " % max_name + fmt_err = fmt_name + "%s: %s" + header = fmt_name % "Name" + " Stmts Exec Cover" + fmt_coverage = fmt_name + "% 6d % 6d % 5d%%" + if show_missing: + header = header + " Missing" + fmt_coverage = fmt_coverage + " %s" + if not file: + file = sys.stdout + print >>file, header + print >>file, "-" * len(header) + total_statements = 0 + total_executed = 0 + for morf in morfs: + name = self.morf_name(morf) + try: + _, statements, _, missing, readable = self.analysis2(morf) + n = len(statements) + m = n - len(missing) + if n > 0: + pc = 100.0 * m / n + else: + pc = 100.0 + args = (name, n, m, pc) + if show_missing: + args = args + (readable,) + print >>file, fmt_coverage % args + total_statements = total_statements + n + total_executed = total_executed + m + except KeyboardInterrupt: #pragma: no cover + raise + except: + if not ignore_errors: + typ, msg = sys.exc_info()[:2] + print >>file, fmt_err % (name, typ, msg) + if len(morfs) > 1: + print >>file, "-" * len(header) + if total_statements > 0: + pc = 100.0 * total_executed / total_statements + else: + pc = 100.0 + args = ("TOTAL", total_statements, total_executed, pc) + if show_missing: + args = args + ("",) + print >>file, fmt_coverage % args + + # annotate(morfs, ignore_errors). + + blank_re = re.compile(r"\s*(#|$)") + else_re = re.compile(r"\s*else\s*:\s*(#|$)") + + def annotate(self, morfs, directory=None, ignore_errors=0, omit_prefixes=[]): + morfs = self.filter_by_prefix(morfs, omit_prefixes) + for morf in morfs: + try: + filename, statements, excluded, missing, _ = self.analysis2(morf) + self.annotate_file(filename, statements, excluded, missing, directory) + except KeyboardInterrupt: + raise + except: + if not ignore_errors: + raise + + def annotate_file(self, filename, statements, excluded, missing, directory=None): + source = open(filename, 'r') + if directory: + dest_file = os.path.join(directory, + os.path.basename(filename) + + ',cover') + else: + dest_file = filename + ',cover' + dest = open(dest_file, 'w') + lineno = 0 + i = 0 + j = 0 + covered = 1 + while 1: + line = source.readline() + if line == '': + break + lineno = lineno + 1 + while i < len(statements) and statements[i] < lineno: + i = i + 1 + while j < len(missing) and missing[j] < lineno: + j = j + 1 + if i < len(statements) and statements[i] == lineno: + covered = j >= len(missing) or missing[j] > lineno + if self.blank_re.match(line): + dest.write(' ') + elif self.else_re.match(line): + # Special logic for lines containing only 'else:'. + # See [GDR 2001-12-04b, 3.2]. + if i >= len(statements) and j >= len(missing): + dest.write('! ') + elif i >= len(statements) or j >= len(missing): + dest.write('> ') + elif statements[i] == missing[j]: + dest.write('! ') + else: + dest.write('> ') + elif lineno in excluded: + dest.write('- ') + elif covered: + dest.write('> ') + else: + dest.write('! ') + dest.write(line) + source.close() + dest.close() + +# Singleton object. +the_coverage = coverage() + +# Module functions call methods in the singleton object. +def use_cache(*args, **kw): + return the_coverage.use_cache(*args, **kw) + +def start(*args, **kw): + return the_coverage.start(*args, **kw) + +def stop(*args, **kw): + return the_coverage.stop(*args, **kw) + +def erase(*args, **kw): + return the_coverage.erase(*args, **kw) + +def begin_recursive(*args, **kw): + return the_coverage.begin_recursive(*args, **kw) + +def end_recursive(*args, **kw): + return the_coverage.end_recursive(*args, **kw) + +def exclude(*args, **kw): + return the_coverage.exclude(*args, **kw) + +def analysis(*args, **kw): + return the_coverage.analysis(*args, **kw) + +def analysis2(*args, **kw): + return the_coverage.analysis2(*args, **kw) + +def report(*args, **kw): + return the_coverage.report(*args, **kw) + +def annotate(*args, **kw): + return the_coverage.annotate(*args, **kw) + +def annotate_file(*args, **kw): + return the_coverage.annotate_file(*args, **kw) + +# Save coverage data when Python exits. (The atexit module wasn't +# introduced until Python 2.0, so use sys.exitfunc when it's not +# available.) +try: + import atexit + atexit.register(the_coverage.save) +except ImportError: + sys.exitfunc = the_coverage.save + +def main(): + the_coverage.command_line(sys.argv[1:]) + +# Command-line interface. +if __name__ == '__main__': + main() + + +# A. REFERENCES +# +# [GDR 2001-12-04a] "Statement coverage for Python"; Gareth Rees; +# Ravenbrook Limited; 2001-12-04; +# <http://www.nedbatchelder.com/code/modules/rees-coverage.html>. +# +# [GDR 2001-12-04b] "Statement coverage for Python: design and +# analysis"; Gareth Rees; Ravenbrook Limited; 2001-12-04; +# <http://www.nedbatchelder.com/code/modules/rees-design.html>. +# +# [van Rossum 2001-07-20a] "Python Reference Manual (releae 2.1.1)"; +# Guide van Rossum; 2001-07-20; +# <http://www.python.org/doc/2.1.1/ref/ref.html>. +# +# [van Rossum 2001-07-20b] "Python Library Reference"; Guido van Rossum; +# 2001-07-20; <http://www.python.org/doc/2.1.1/lib/lib.html>. +# +# +# B. DOCUMENT HISTORY +# +# 2001-12-04 GDR Created. +# +# 2001-12-06 GDR Added command-line interface and source code +# annotation. +# +# 2001-12-09 GDR Moved design and interface to separate documents. +# +# 2001-12-10 GDR Open cache file as binary on Windows. Allow +# simultaneous -e and -x, or -a and -r. +# +# 2001-12-12 GDR Added command-line help. Cache analysis so that it +# only needs to be done once when you specify -a and -r. +# +# 2001-12-13 GDR Improved speed while recording. Portable between +# Python 1.5.2 and 2.1.1. +# +# 2002-01-03 GDR Module-level functions work correctly. +# +# 2002-01-07 GDR Update sys.path when running a file with the -x option, +# so that it matches the value the program would get if it were run on +# its own. +# +# 2004-12-12 NMB Significant code changes. +# - Finding executable statements has been rewritten so that docstrings and +# other quirks of Python execution aren't mistakenly identified as missing +# lines. +# - Lines can be excluded from consideration, even entire suites of lines. +# - The filesystem cache of covered lines can be disabled programmatically. +# - Modernized the code. +# +# 2004-12-14 NMB Minor tweaks. Return 'analysis' to its original behavior +# and add 'analysis2'. Add a global for 'annotate', and factor it, adding +# 'annotate_file'. +# +# 2004-12-31 NMB Allow for keyword arguments in the module global functions. +# Thanks, Allen. +# +# 2005-12-02 NMB Call threading.settrace so that all threads are measured. +# Thanks Martin Fuzzey. Add a file argument to report so that reports can be +# captured to a different destination. +# +# 2005-12-03 NMB coverage.py can now measure itself. +# +# 2005-12-04 NMB Adapted Greg Rogers' patch for using relative filenames, +# and sorting and omitting files to report on. +# +# 2006-07-23 NMB Applied Joseph Tate's patch for function decorators. +# +# 2006-08-21 NMB Applied Sigve Tjora and Mark van der Wal's fixes for argument +# handling. +# +# 2006-08-22 NMB Applied Geoff Bache's parallel mode patch. +# +# 2006-08-23 NMB Refactorings to improve testability. Fixes to command-line +# logic for parallel mode and collect. +# +# 2006-08-25 NMB "#pragma: nocover" is excluded by default. +# +# 2006-09-10 NMB Properly ignore docstrings and other constant expressions that +# appear in the middle of a function, a problem reported by Tim Leslie. +# Minor changes to avoid lint warnings. +# +# 2006-09-17 NMB coverage.erase() shouldn't clobber the exclude regex. +# Change how parallel mode is invoked, and fix erase() so that it erases the +# cache when called programmatically. +# +# 2007-07-21 NMB In reports, ignore code executed from strings, since we can't +# do anything useful with it anyway. +# Better file handling on Linux, thanks Guillaume Chazarain. +# Better shell support on Windows, thanks Noel O'Boyle. +# Python 2.2 support maintained, thanks Catherine Proulx. +# +# 2007-07-22 NMB Python 2.5 now fully supported. The method of dealing with +# multi-line statements is now less sensitive to the exact line that Python +# reports during execution. Pass statements are handled specially so that their +# disappearance during execution won't throw off the measurement. +# +# 2007-07-23 NMB Now Python 2.5 is *really* fully supported: the body of the +# new with statement is counted as executable. +# +# 2007-07-29 NMB Better packaging. +# +# 2007-09-30 NMB Don't try to predict whether a file is Python source based on +# the extension. Extensionless files are often Pythons scripts. Instead, simply +# parse the file and catch the syntax errors. Hat tip to Ben Finney. +# +# 2008-05-25 NMB Open files in rU mode to avoid line ending craziness. +# Thanks, Edward Loper. +# +# 2008-09-14 NMB Add support for finding source files in eggs. +# Don't check for morf's being instances of ModuleType, instead use duck typing +# so that pseudo-modules can participate. Thanks, Imri Goldberg. +# Use os.realpath as part of the fixing of filenames so that symlinks won't +# confuse things. Thanks, Patrick Mezard. +# +# +# C. COPYRIGHT AND LICENCE +# +# Copyright 2001 Gareth Rees. All rights reserved. +# Copyright 2004-2008 Ned Batchelder. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. +# +# $Id: coverage.py 100 2008-10-12 12:08:22Z nedbat $ diff --git a/tests/root/_templates/layout.html b/tests/root/_templates/layout.html index 1f4688e6..e8920025 100644 --- a/tests/root/_templates/layout.html +++ b/tests/root/_templates/layout.html @@ -1,4 +1,5 @@ {% extends "!layout.html" %} {% block extrahead %} <meta name="hc" content="{{ hckey }}" /> +{{ super() }} {% endblock %} diff --git a/tests/root/autodoc.txt b/tests/root/autodoc.txt index 2b57e0e9..c718feb4 100644 --- a/tests/root/autodoc.txt +++ b/tests/root/autodoc.txt @@ -1,7 +1,28 @@ Autodoc tests ============= +Just testing a few autodoc possibilities... + +.. automodule:: util + .. automodule:: test_autodoc :members: .. autofunction:: function + +.. autoclass:: Class + :inherited-members: + + Additional content. + +.. autoclass:: Outer + :members: Inner + +.. autoattribute:: Class.docattr + +.. autoexception:: CustomEx + :members: f + +.. autoclass:: CustomDict + :show-inheritance: + :members: diff --git a/tests/root/autosummary.txt b/tests/root/autosummary.txt new file mode 100644 index 00000000..edf75e32 --- /dev/null +++ b/tests/root/autosummary.txt @@ -0,0 +1,7 @@ +Autosummary test +================ + +.. autosummary:: + :toctree: generated + + sphinx.application.TemplateBridge diff --git a/tests/root/conf.py b/tests/root/conf.py index 12951d03..fd82be7d 100644 --- a/tests/root/conf.py +++ b/tests/root/conf.py @@ -1,183 +1,71 @@ # -*- 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', 'sphinx.ext.autodoc', 'sphinx.ext.jsmath', - 'sphinx.ext.coverage', 'sphinx.ext.todo'] + 'sphinx.ext.coverage', 'sphinx.ext.todo', + 'sphinx.ext.autosummary'] + jsmath_path = 'dummy.js' -# 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 = 'contents' +source_suffix = '.txt' -# General substitutions. project = 'Sphinx <Tests>' copyright = '2008, Georg Brandl & Team' - -# 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. +# If this is changed, remember to update the versionchanges! +version = '0.6' +release = '0.6alpha1' 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_trees = ['_build'] - keep_warnings = True - -# 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' +rst_epilog = '.. |subst| replace:: global substitution' -# Options for HTML output -# ----------------------- +html_theme = 'testtheme' +html_theme_path = ['.'] +html_theme_options = {'testopt': 'testoverride'} -# 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 = '' - html_context = {'hckey': 'hcval'} -# 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 +latex_additional_files = ['svgimg.svg'] value_from_conf_py = 84 coverage_c_path = ['special/*.h'] coverage_c_regexes = {'cfunction': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} +autosummary_generate = ['autosummary'] + +# modify tags from conf.py +tags.add('confpytag') + +from sphinx import addnodes + +def userdesc_parse(env, sig, signode): + x, y = sig.split(':') + signode += addnodes.desc_name(x, x) + signode += addnodes.desc_parameterlist() + signode[-1] += addnodes.desc_parameter(y, y) + return x def setup(app): app.add_config_value('value_from_conf_py', 42, False) + app.add_description_unit('userdesc', 'userdescrole', '%s (userdesc)', + userdesc_parse) diff --git a/tests/root/contents.txt b/tests/root/contents.txt index 61646f62..24d790a5 100644 --- a/tests/root/contents.txt +++ b/tests/root/contents.txt @@ -9,14 +9,19 @@ Contents: .. toctree:: :maxdepth: 2 + :numbered: images subdir/images + subdir/includes includes markup desc math autodoc + autosummary + + Python <http://python.org/> Indices and tables ================== diff --git a/tests/root/desc.txt b/tests/root/desc.txt index beb470b6..d6915dc2 100644 --- a/tests/root/desc.txt +++ b/tests/root/desc.txt @@ -58,3 +58,14 @@ Testing references ================== Referencing :class:`mod.Cls` or :Class:`mod.Cls` should be the same. + + +User markup +=========== + +.. userdesc:: myobj:parameter + + Description of userdesc. + + +Referencing :userdescrole:`myobj`. diff --git a/tests/root/images.txt b/tests/root/images.txt index be868dfe..bd64d573 100644 --- a/tests/root/images.txt +++ b/tests/root/images.txt @@ -23,3 +23,6 @@ Sphinx image handling .. an image with subdir and unspecified extension .. image:: subdir/simg.* + +.. an SVG image (for HTML at least) +.. image:: svgimg.* diff --git a/tests/root/includes.txt b/tests/root/includes.txt index ad507fc6..44e33af0 100644 --- a/tests/root/includes.txt +++ b/tests/root/includes.txt @@ -14,3 +14,33 @@ Test file and literal inclusion :encoding: latin-1 .. include:: wrongenc.inc :encoding: latin-1 + +Literalinclude options +====================== + +.. highlight:: text + +.. cssclass:: inc-pyobj1 +.. literalinclude:: literal.inc + :pyobject: Foo + +.. cssclass:: inc-pyobj2 +.. literalinclude:: literal.inc + :pyobject: Bar.baz + +.. cssclass:: inc-lines +.. literalinclude:: literal.inc + :lines: 6-7,9 + +.. cssclass:: inc-startend +.. literalinclude:: literal.inc + :start-after: coding: utf-8 + :end-before: class Foo + + +Testing downloadable files +========================== + +Download :download:`img.png` here. +Download :download:`this <subdir/img.png>` there. +Don't download :download:`this <nonexisting.png>`. diff --git a/tests/root/literal.inc b/tests/root/literal.inc index a4ce93d2..d5b9890c 100644 --- a/tests/root/literal.inc +++ b/tests/root/literal.inc @@ -2,3 +2,12 @@ # -*- coding: utf-8 -*- foo = u"Including Unicode characters: üöä" + +class Foo: + pass + +class Bar: + def baz(): + pass + +def bar(): pass diff --git a/tests/root/markup.txt b/tests/root/markup.txt index 454762e3..52d407e2 100644 --- a/tests/root/markup.txt +++ b/tests/root/markup.txt @@ -11,6 +11,8 @@ Testing various markup :author: Me :keywords: docs, sphinx +A |subst|. + .. _label: :: @@ -82,13 +84,13 @@ Tables Version markup -------------- -.. versionadded:: 0.5 +.. versionadded:: 0.6 Some funny **stuff**. -.. versionchanged:: 0.5 +.. versionchanged:: 0.6 Even more funny stuff. -.. deprecated:: 0.4 +.. deprecated:: 0.6 Boring stuff. @@ -104,6 +106,16 @@ Reference lookup: [Ref1]_ (defined in another file). `Google <http://www.google.com>`_ For everything. +.. hlist:: + :columns: 4 + + * This + * is + * a horizontal + * list + * with several + * items + .. rubric:: Side note This is a side note. @@ -140,6 +152,7 @@ Option list: try2_stmt: "try" ":" `suite` : "finally" ":" `suite` +Test :abbr:`abbr (abbreviation)` and another :abbr:`abbr (abbreviation)`. Index markup ------------ @@ -164,6 +177,26 @@ Invalid index markup... Testing öäü... +Only directive +-------------- + +.. only:: html + + In HTML. + +.. only:: latex + + In LaTeX. + +.. only:: html or latex + + In both. + +.. only:: confpytag and (testtag or nonexisting_tag) + + Always present, because set through conf.py/command line. + + .. rubric:: Footnotes .. [#] Like footnotes. diff --git a/tests/root/rimg.png b/tests/root/rimg.png Binary files differnew file mode 100644 index 00000000..1081dc14 --- /dev/null +++ b/tests/root/rimg.png diff --git a/tests/root/special/code.py b/tests/root/special/code.py new file mode 100644 index 00000000..70c48d2e --- /dev/null +++ b/tests/root/special/code.py @@ -0,0 +1,2 @@ +print "line 1" +print "line 2" diff --git a/tests/root/subdir/images.txt b/tests/root/subdir/images.txt index 33adf5b5..f2adf88d 100644 --- a/tests/root/subdir/images.txt +++ b/tests/root/subdir/images.txt @@ -2,3 +2,5 @@ Image including source in subdir ================================ .. image:: img.* + +.. image:: /rimg.png diff --git a/tests/root/subdir/includes.txt b/tests/root/subdir/includes.txt new file mode 100644 index 00000000..3e1ae0d1 --- /dev/null +++ b/tests/root/subdir/includes.txt @@ -0,0 +1,12 @@ +Including in subdir +=================== + +.. absolute filename +.. literalinclude:: /special/code.py + :lines: 1 + +.. relative filename +.. literalinclude:: ../special/code.py + :lines: 2 + +Absolute :download:`/img.png` download. diff --git a/tests/root/svgimg.pdf b/tests/root/svgimg.pdf Binary files differnew file mode 100644 index 00000000..cacbd855 --- /dev/null +++ b/tests/root/svgimg.pdf diff --git a/tests/root/svgimg.svg b/tests/root/svgimg.svg new file mode 100644 index 00000000..10e035b6 --- /dev/null +++ b/tests/root/svgimg.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + height="60" + width="60" + _SVGFile__filename="oldscale/apps/warning.svg" + version="1.0" + y="0" + x="0" + id="svg1" + sodipodi:version="0.32" + inkscape:version="0.41" + sodipodi:docname="exclamation.svg" + sodipodi:docbase="/home/danny/work/icons/primary/scalable/actions"> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0000000" + inkscape:pageshadow="2" + inkscape:zoom="7.5136000" + inkscape:cx="42.825186" + inkscape:cy="24.316071" + inkscape:window-width="1020" + inkscape:window-height="691" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:current-layer="svg1" /> + <defs + id="defs3"> + <linearGradient + id="linearGradient1160"> + <stop + style="stop-color: #000000;stop-opacity: 1.0;" + id="stop1161" + offset="0" /> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + id="stop1162" + offset="1" /> + </linearGradient> + <linearGradient + xlink:href="#linearGradient1160" + id="linearGradient1163" /> + </defs> + <metadata + id="metadata12"> + <RDF + id="RDF13"> + <Work + about="" + id="Work14"> + <title + id="title15">Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004)</title> + <description + id="description17" /> + <subject + id="subject18"> + <Bag + id="Bag19"> + <li + id="li20" /> + </Bag> + </subject> + <publisher + id="publisher21"> + <Agent + about="" + id="Agent22"> + <title + id="title23" /> + </Agent> + </publisher> + <creator + id="creator24"> + <Agent + about="" + id="Agent25"> + <title + id="title26">Danny Allen</title> + </Agent> + </creator> + <rights + id="rights28"> + <Agent + about="" + id="Agent29"> + <title + id="title30">Danny Allen</title> + </Agent> + </rights> + <date + id="date32" /> + <format + id="format33">image/svg+xml</format> + <type + id="type35" + resource="http://purl.org/dc/dcmitype/StillImage" /> + <license + id="license36" + resource="http://creativecommons.org/licenses/LGPL/2.1/"> + <date + id="date37" /> + </license> + <language + id="language38">en</language> + </Work> + </RDF> + <rdf:RDF + id="RDF40"> + <cc:Work + rdf:about="" + id="Work41"> + <dc:format + id="format42">image/svg+xml</dc:format> + <dc:type + id="type44" + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + id="g2099"> + <path + style="color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:8.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none" + d="M 55.311891,51.920745 L 4.6880989,51.920744 L 29.999995,8.0792542 L 55.311891,51.920745 z " + id="path1724" /> + <path + style="color:#000000;fill:#ffe940;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#000000;stroke-width:3.1250010;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none" + d="M 55.311891,51.920745 L 4.6880989,51.920744 L 29.999995,8.0792542 L 55.311891,51.920745 z " + id="path1722" /> + <path + style="font-size:12.000000;font-weight:900;fill:none;fill-opacity:1.0000000;stroke:#ffffff;stroke-width:8.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000" + d="M 34.944960,10.779626 L 34.944960,33.186510 C 34.944960,34.752415 34.501979,36.081368 33.616007,37.173380 C 32.750636,38.265402 31.545298,38.811408 29.999995,38.811408 C 28.475302,38.811408 27.269965,38.265402 26.383993,37.173380 C 25.498020,36.060767 25.055030,34.731804 25.055030,33.186510 L 25.055030,10.779626 C 25.055030,9.1931155 25.498020,7.8641562 26.383993,6.7927462 C 27.269965,5.7007332 28.475302,5.1547262 29.999995,5.1547262 C 31.009593,5.1547262 31.885265,5.4019740 32.627010,5.8964706 C 33.389356,6.3909681 33.966274,7.0709005 34.357752,7.9362696 C 34.749221,8.7810349 34.944960,9.7288200 34.944960,10.779626 z " + id="path1099" /> + <path + style="font-size:12.000000;font-weight:900;fill:#e71c02;fill-opacity:1.0000000;stroke:none;stroke-width:3.1249981;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0000000" + d="M 29.999995,3.5986440 C 28.102272,3.5986440 26.318514,4.3848272 25.156245,5.8173940 C 24.028906,7.1806889 23.499995,8.9087770 23.499995,10.786144 L 23.499995,33.192394 C 23.499995,35.036302 24.050685,36.772771 25.156245,38.161144 C 26.318514,39.593721 28.102273,40.379893 29.999995,40.379894 C 31.913354,40.379894 33.697195,39.576736 34.843745,38.129894 C 35.959941,36.754118 36.499995,35.052976 36.499995,33.192394 L 36.499995,10.786144 C 36.499995,9.5413010 36.276626,8.3551469 35.781245,7.2861440 C 35.278844,6.1755772 34.477762,5.2531440 33.468745,4.5986440 C 32.454761,3.9226545 31.264694,3.5986439 29.999995,3.5986440 z " + id="path835" + sodipodi:nodetypes="cccccccccccc" /> + <path + style="color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:5.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none" + d="M 36.506243,49.901522 C 36.506243,53.492972 33.591442,56.407773 29.999991,56.407773 C 26.408541,56.407773 23.493739,53.492972 23.493739,49.901522 C 23.493739,46.310071 26.408541,43.395270 29.999991,43.395270 C 33.591442,43.395270 36.506243,46.310071 36.506243,49.901522 z " + id="path1727" /> + <path + style="color:#000000;fill:#e71c02;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:3.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none" + d="M 36.506243,49.901522 C 36.506243,53.492972 33.591442,56.407773 29.999991,56.407773 C 26.408541,56.407773 23.493739,53.492972 23.493739,49.901522 C 23.493739,46.310071 26.408541,43.395270 29.999991,43.395270 C 33.591442,43.395270 36.506243,46.310071 36.506243,49.901522 z " + id="path1725" /> + </g> +</svg> diff --git a/tests/root/testtheme/layout.html b/tests/root/testtheme/layout.html new file mode 100644 index 00000000..81372be0 --- /dev/null +++ b/tests/root/testtheme/layout.html @@ -0,0 +1,5 @@ +{% extends "basic/layout.html" %} +{% block extrahead %} +<meta name="testopt" content="{{ theme_testopt }}" /> +{{ super() }} +{% endblock %} diff --git a/tests/root/testtheme/static/staticimg.png b/tests/root/testtheme/static/staticimg.png Binary files differnew file mode 100644 index 00000000..1081dc14 --- /dev/null +++ b/tests/root/testtheme/static/staticimg.png diff --git a/tests/root/testtheme/static/statictmpl.html_t b/tests/root/testtheme/static/statictmpl.html_t new file mode 100644 index 00000000..4ab292b4 --- /dev/null +++ b/tests/root/testtheme/static/statictmpl.html_t @@ -0,0 +1,2 @@ +<!-- testing static templates --> +<html><project>{{ project|e }}</project></html> diff --git a/tests/root/testtheme/theme.conf b/tests/root/testtheme/theme.conf new file mode 100644 index 00000000..a8776737 --- /dev/null +++ b/tests/root/testtheme/theme.conf @@ -0,0 +1,7 @@ +[theme] +inherit = basic +stylesheet = default.css +pygments_style = emacs + +[options] +testopt = optdefault diff --git a/tests/root/ziptheme.zip b/tests/root/ziptheme.zip Binary files differnew file mode 100644 index 00000000..8a246ed9 --- /dev/null +++ b/tests/root/ziptheme.zip diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 752c0725..8e011438 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -3,7 +3,7 @@ test_autodoc ~~~~~~~~~~~~ - Test the autodoc extension. This tests mainly the RstGenerator; the auto + Test the autodoc extension. This tests mainly the Documenters; the auto directives are tested in a test source file translated by test_build. :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. @@ -14,16 +14,18 @@ from util import * from docutils.statemachine import ViewList -from sphinx.ext.autodoc import RstGenerator, cut_lines, between +from sphinx.ext.autodoc import AutoDirective, Documenter, add_documenter, \ + ModuleLevelDocumenter, FunctionDocumenter, cut_lines, between, ALL def setup_module(): - global app, lid, options, gen + global app, lid, options, directive app = TestApp() app.builder.env.app = app app.connect('autodoc-process-docstring', process_docstring) app.connect('autodoc-process-signature', process_signature) + app.connect('autodoc-skip-member', skip_member) options = Struct( inherited_members = False, @@ -33,27 +35,27 @@ def setup_module(): synopsis = '', platform = '', deprecated = False, + members = [], + member_order = 'alphabetic', + exclude_members = set(), ) - gen = TestGenerator(options, app) + directive = Struct( + env = app.builder.env, + genopt = options, + result = ViewList(), + warn = warnfunc, + filename_set = set(), + ) def teardown_module(): app.cleanup() -class TestGenerator(RstGenerator): - """Generator that handles warnings without a reporter.""" - - def __init__(self, options, app): - self.options = options - self.env = app.builder.env - self.lineno = 42 - self.filename_set = set() - self.warnings = [] - self.result = ViewList() +_warnings = [] - def warn(self, msg): - self.warnings.append(msg) +def warnfunc(msg): + _warnings.append(msg) processed_docstrings = [] @@ -71,62 +73,73 @@ def process_signature(app, what, name, obj, options, args, retann): return '42', None -def test_resolve_name(): - # for modules - assert gen.resolve_name('module', 'test_autodoc') == \ - ('test_autodoc', 'test_autodoc', [], None, None) - assert gen.resolve_name('module', 'test.test_autodoc') == \ - ('test.test_autodoc', 'test.test_autodoc', [], None, None) +def skip_member(app, what, name, obj, skip, options): + if name.startswith('_'): + return True + if name == 'skipmeth': + return True - assert gen.resolve_name('module', 'test(arg)') == \ - ('test', 'test', [], None, None) - assert 'ignoring signature arguments' in gen.warnings[0] - del gen.warnings[:] + +def test_parse_name(): + def verify(objtype, name, result): + inst = AutoDirective._registry[objtype](directive, name) + assert inst.parse_name() + assert (inst.modname, inst.objpath, inst.args, inst.retann) == result + + # for modules + verify('module', 'test_autodoc', ('test_autodoc', [], None, None)) + verify('module', 'test.test_autodoc', ('test.test_autodoc', [], None, None)) + verify('module', 'test(arg)', ('test', [], 'arg', None)) + assert 'signature arguments' in _warnings[0] + del _warnings[:] # for functions/classes - assert gen.resolve_name('function', 'util.raises') == \ - ('util.raises', 'util', ['raises'], None, None) - assert gen.resolve_name('function', 'util.raises(exc) -> None') == \ - ('util.raises', 'util', ['raises'], 'exc', ' -> None') - gen.env.autodoc_current_module = 'util' - assert gen.resolve_name('function', 'raises') == \ - ('raises', 'util', ['raises'], None, None) - gen.env.autodoc_current_module = None - gen.env.currmodule = 'util' - assert gen.resolve_name('function', 'raises') == \ - ('raises', 'util', ['raises'], None, None) - assert gen.resolve_name('class', 'TestApp') == \ - ('TestApp', 'util', ['TestApp'], None, None) + verify('function', 'util.raises', ('util', ['raises'], None, None)) + verify('function', 'util.raises(exc) -> None', + ('util', ['raises'], 'exc', 'None')) + directive.env.autodoc_current_module = 'util' + verify('function', 'raises', ('util', ['raises'], None, None)) + directive.env.autodoc_current_module = None + directive.env.currmodule = 'util' + verify('function', 'raises', ('util', ['raises'], None, None)) + verify('class', 'TestApp', ('util', ['TestApp'], None, None)) # for members - gen.env.currmodule = 'foo' - assert gen.resolve_name('method', 'util.TestApp.cleanup') == \ - ('util.TestApp.cleanup', 'util', ['TestApp', 'cleanup'], None, None) - gen.env.currmodule = 'util' - gen.env.currclass = 'Foo' - gen.env.autodoc_current_class = 'TestApp' - assert gen.resolve_name('method', 'cleanup') == \ - ('cleanup', 'util', ['TestApp', 'cleanup'], None, None) - assert gen.resolve_name('method', 'TestApp.cleanup') == \ - ('TestApp.cleanup', 'util', ['TestApp', 'cleanup'], None, None) + directive.env.currmodule = 'foo' + verify('method', 'util.TestApp.cleanup', + ('util', ['TestApp', 'cleanup'], None, None)) + directive.env.currmodule = 'util' + directive.env.currclass = 'Foo' + directive.env.autodoc_current_class = 'TestApp' + verify('method', 'cleanup', ('util', ['TestApp', 'cleanup'], None, None)) + verify('method', 'TestApp.cleanup', + ('util', ['TestApp', 'cleanup'], None, None)) # and clean up - gen.env.currmodule = None - gen.env.currclass = None - gen.env.autodoc_current_class = None + directive.env.currmodule = None + directive.env.currclass = None + directive.env.autodoc_current_class = None def test_format_signature(): + def formatsig(objtype, name, obj, args, retann): + inst = AutoDirective._registry[objtype](directive, name) + inst.fullname = name + inst.doc_as_attr = False # for class objtype + inst.object = obj + inst.args = args + inst.retann = retann + return inst.format_signature() + # no signatures for modules - assert gen.format_signature('module', 'test', None, None, None) == '' + assert formatsig('module', 'test', None, None, None) == '' # test for functions def f(a, b, c=1, **d): pass - assert gen.format_signature('function', 'f', f, None, None) == '(a, b, c=1, **d)' - assert gen.format_signature('function', 'f', f, 'a, b, c, d', None) == \ - '(a, b, c, d)' - assert gen.format_signature('function', 'f', f, None, ' -> None') == \ + assert formatsig('function', 'f', f, None, None) == '(a, b, c=1, **d)' + assert formatsig('function', 'f', f, 'a, b, c, d', None) == '(a, b, c, d)' + assert formatsig('function', 'f', f, None, 'None') == \ '(a, b, c=1, **d) -> None' # test for classes @@ -136,15 +149,15 @@ def test_format_signature(): pass # no signature for classes without __init__ for C in (D, E): - assert gen.format_signature('class', 'D', C, None, None) == '' + assert formatsig('class', 'D', C, None, None) == '' class F: def __init__(self, a, b=None): pass class G(F, object): pass for C in (F, G): - assert gen.format_signature('class', 'C', C, None, None) == '(a, b=None)' - assert gen.format_signature('class', 'C', D, 'a, b', ' -> X') == '(a, b) -> X' + assert formatsig('class', 'C', C, None, None) == '(a, b=None)' + assert formatsig('class', 'C', D, 'a, b', 'X') == '(a, b) -> X' # test for methods class H: @@ -152,26 +165,29 @@ def test_format_signature(): pass def foo2(b, *c): pass - assert gen.format_signature('method', 'H.foo', H.foo1, None, None) == '(b, *c)' - assert gen.format_signature('method', 'H.foo', H.foo1, 'a', None) == '(a)' - assert gen.format_signature('method', 'H.foo', H.foo2, None, None) == '(b, *c)' + assert formatsig('method', 'H.foo', H.foo1, None, None) == '(b, *c)' + assert formatsig('method', 'H.foo', H.foo1, 'a', None) == '(a)' + assert formatsig('method', 'H.foo', H.foo2, None, None) == '(b, *c)' # test exception handling - raises(RuntimeError, gen.format_signature, 'function', 'int', int, None, None) + raises(TypeError, formatsig, 'function', 'int', int, None, None) # test processing by event handler - assert gen.format_signature('method', 'bar', H.foo1, None, None) == '42' + assert formatsig('method', 'bar', H.foo1, None, None) == '42' def test_get_doc(): - def getdocl(*args): - # strip the empty line at the end - return list(gen.get_doc(*args))[:-1] + def getdocl(objtype, obj, encoding=None): + inst = AutoDirective._registry[objtype](directive, 'tmp') + inst.object = obj + ds = inst.get_doc(encoding) + # for testing purposes, concat them and strip the empty line at the end + return sum(ds, [])[:-1] # objects without docstring def f(): pass - assert getdocl('function', 'f', f) == [] + assert getdocl('function', f) == [] # standard function, diverse docstring styles... def f(): @@ -181,7 +197,7 @@ def test_get_doc(): Docstring """ for func in (f, g): - assert getdocl('function', 'f', func) == ['Docstring'] + assert getdocl('function', func) == ['Docstring'] # first line vs. other lines indentation def f(): @@ -190,29 +206,29 @@ def test_get_doc(): Other lines """ - assert getdocl('function', 'f', f) == ['First line', '', 'Other', ' lines'] + assert getdocl('function', f) == ['First line', '', 'Other', ' lines'] # charset guessing (this module is encoded in utf-8) def f(): """Döcstring""" - assert getdocl('function', 'f', f) == [u'Döcstring'] + assert getdocl('function', f) == [u'Döcstring'] # already-unicode docstrings must be taken literally def f(): u"""Döcstring""" - assert getdocl('function', 'f', f) == [u'Döcstring'] + assert getdocl('function', f) == [u'Döcstring'] # class docstring: depends on config value which one is taken class C: """Class docstring""" def __init__(self): """Init docstring""" - gen.env.config.autoclass_content = 'class' - assert getdocl('class', 'C', C) == ['Class docstring'] - gen.env.config.autoclass_content = 'init' - assert getdocl('class', 'C', C) == ['Init docstring'] - gen.env.config.autoclass_content = 'both' - assert getdocl('class', 'C', C) == ['Class docstring', '', 'Init docstring'] + directive.env.config.autoclass_content = 'class' + assert getdocl('class', C) == ['Class docstring'] + directive.env.config.autoclass_content = 'init' + assert getdocl('class', C) == ['Init docstring'] + directive.env.config.autoclass_content = 'both' + assert getdocl('class', C) == ['Class docstring', '', 'Init docstring'] class D: """Class docstring""" @@ -224,26 +240,33 @@ def test_get_doc(): """ # Indentation is normalized for 'both' - assert getdocl('class', 'D', D) == ['Class docstring', '', 'Init docstring', - '', 'Other', ' lines'] + assert getdocl('class', D) == ['Class docstring', '', 'Init docstring', + '', 'Other', ' lines'] + + +def test_docstring_processing(): + def process(objtype, name, obj): + inst = AutoDirective._registry[objtype](directive, name) + inst.object = obj + inst.fullname = name + return list(inst.process_doc(inst.get_doc())) class E: def __init__(self): """Init docstring""" # docstring processing by event handler - assert getdocl('class', 'bar', E) == ['Init docstring', '', '42'] + assert process('class', 'bar', E) == ['Init docstring', '', '42', ''] - -def test_docstring_processing_functions(): - lid = app.connect('autodoc-process-docstring', cut_lines(1, 1, ['function'])) + lid = app.connect('autodoc-process-docstring', + cut_lines(1, 1, ['function'])) def f(): """ first line second line third line """ - assert list(gen.get_doc('function', 'f', f)) == ['second line', ''] + assert process('function', 'f', f) == ['second line', ''] app.disconnect(lid) lid = app.connect('autodoc-process-docstring', between('---', ['function'])) @@ -255,118 +278,173 @@ def test_docstring_processing_functions(): --- third line """ - assert list(gen.get_doc('function', 'f', f)) == ['second line', ''] + assert process('function', 'f', f) == ['second line', ''] app.disconnect(lid) +def test_new_documenter(): + class MyDocumenter(ModuleLevelDocumenter): + objtype = 'integer' + directivetype = 'data' + priority = 100 + + @classmethod + def can_document_member(cls, member, membername, isattr, parent): + return isinstance(member, int) + + def document_members(self, all_members=False): + return + + add_documenter(MyDocumenter) + + def assert_result_contains(item, objtype, name, **kw): + inst = AutoDirective._registry[objtype](directive, name) + inst.generate(**kw) + #print '\n'.join(directive.result) + assert len(_warnings) == 0, _warnings + assert item in directive.result + del directive.result[:] + + options.members = ['integer'] + assert_result_contains('.. data:: integer', 'module', 'test_autodoc') + + def test_generate(): - def assert_warns(warn_str, *args): - gen.generate(*args) - assert len(gen.result) == 0, gen.result - assert len(gen.warnings) == 1, gen.warnings - assert warn_str in gen.warnings[0], gen.warnings - del gen.warnings[:] - - def assert_works(*args): - gen.generate(*args) - assert gen.result - assert len(gen.warnings) == 0, gen.warnings - del gen.result[:] - - def assert_processes(items, *args): + def assert_warns(warn_str, objtype, name, **kw): + inst = AutoDirective._registry[objtype](directive, name) + inst.generate(**kw) + assert len(directive.result) == 0, directive.result + assert len(_warnings) == 1, _warnings + assert warn_str in _warnings[0], _warnings + del _warnings[:] + + def assert_works(objtype, name, **kw): + inst = AutoDirective._registry[objtype](directive, name) + inst.generate(**kw) + assert directive.result + assert len(_warnings) == 0, _warnings + del directive.result[:] + + def assert_processes(items, objtype, name, **kw): del processed_docstrings[:] del processed_signatures[:] - assert_works(*args) - assert set(processed_docstrings) | set(processed_signatures) == set(items) + assert_works(objtype, name, **kw) + assert set(processed_docstrings) | set(processed_signatures) == \ + set(items) - def assert_result_contains(item, *args): - gen.generate(*args) - print '\n'.join(gen.result) - assert len(gen.warnings) == 0, gen.warnings - assert item in gen.result - del gen.result[:] + def assert_result_contains(item, objtype, name, **kw): + inst = AutoDirective._registry[objtype](directive, name) + inst.generate(**kw) + #print '\n'.join(directive.result) + assert len(_warnings) == 0, _warnings + assert item in directive.result + del directive.result[:] + + options.members = [] # no module found? assert_warns("import for autodocumenting 'foobar'", - 'function', 'foobar', None, None) + 'function', 'foobar', more_content=None) # importing assert_warns("import/find module 'test_foobar'", - 'module', 'test_foobar', None, None) + 'module', 'test_foobar', more_content=None) # attributes missing assert_warns("import/find function 'util.foobar'", - 'function', 'util.foobar', None, None) + 'function', 'util.foobar', more_content=None) # test auto and given content mixing - gen.env.currmodule = 'test_autodoc' - assert_result_contains(' Function.', 'method', 'Class.meth', [], None) + directive.env.currmodule = 'test_autodoc' + assert_result_contains(' Function.', 'method', 'Class.meth') add_content = ViewList() add_content.append('Content.', '', 0) - assert_result_contains(' Function.', 'method', 'Class.meth', [], add_content) - assert_result_contains(' Content.', 'method', 'Class.meth', [], add_content) + assert_result_contains(' Function.', 'method', + 'Class.meth', more_content=add_content) + assert_result_contains(' Content.', 'method', + 'Class.meth', more_content=add_content) # test check_module - gen.generate('function', 'raises', None, None, check_module=True) - assert len(gen.result) == 0 + inst = FunctionDocumenter(directive, 'raises') + inst.generate(check_module=True) + assert len(directive.result) == 0 # assert that exceptions can be documented - assert_works('exception', 'test_autodoc.CustomEx', ['__all__'], None) - assert_works('exception', 'test_autodoc.CustomEx', [], None) + assert_works('exception', 'test_autodoc.CustomEx', all_members=True) + assert_works('exception', 'test_autodoc.CustomEx') # test diverse inclusion settings for members - should = [('class', 'Class')] - assert_processes(should, 'class', 'Class', [], None) - should.extend([('method', 'Class.meth')]) - assert_processes(should, 'class', 'Class', ['meth'], None) - should.extend([('attribute', 'Class.prop')]) - assert_processes(should, 'class', 'Class', ['__all__'], None) + should = [('class', 'test_autodoc.Class')] + assert_processes(should, 'class', 'Class') + should.extend([('method', 'test_autodoc.Class.meth')]) + options.members = ['meth'] + options.exclude_members = set(['excludemeth']) + assert_processes(should, 'class', 'Class') + should.extend([('attribute', 'test_autodoc.Class.prop'), + ('attribute', 'test_autodoc.Class.attr'), + ('attribute', 'test_autodoc.Class.docattr'), + ('attribute', 'test_autodoc.Class.udocattr')]) + options.members = ALL + assert_processes(should, 'class', 'Class') options.undoc_members = True - should.append(('method', 'Class.undocmeth')) - assert_processes(should, 'class', 'Class', ['__all__'], None) + should.append(('method', 'test_autodoc.Class.undocmeth')) + assert_processes(should, 'class', 'Class') options.inherited_members = True - should.append(('method', 'Class.inheritedmeth')) - assert_processes(should, 'class', 'Class', ['__all__'], None) + should.append(('method', 'test_autodoc.Class.inheritedmeth')) + assert_processes(should, 'class', 'Class') + options.members = [] # test module flags - assert_result_contains('.. module:: test_autodoc', 'module', - 'test_autodoc', [], None) + assert_result_contains('.. module:: test_autodoc', 'module', 'test_autodoc') options.synopsis = 'Synopsis' - assert_result_contains(' :synopsis: Synopsis', 'module', 'test_autodoc', [], None) + assert_result_contains(' :synopsis: Synopsis', 'module', 'test_autodoc') options.deprecated = True - assert_result_contains(' :deprecated:', 'module', 'test_autodoc', [], None) + assert_result_contains(' :deprecated:', 'module', 'test_autodoc') options.platform = 'Platform' - assert_result_contains(' :platform: Platform', 'module', 'test_autodoc', [], None) + assert_result_contains(' :platform: Platform', 'module', 'test_autodoc') # test if __all__ is respected for modules - assert_result_contains('.. class:: Class', 'module', 'test_autodoc', - ['__all__'], None) + options.members = ALL + assert_result_contains('.. class:: Class', 'module', 'test_autodoc') try: - assert_result_contains('.. exception:: CustomEx', 'module', 'test_autodoc', - ['__all__'], None) + assert_result_contains('.. exception:: CustomEx', + 'module', 'test_autodoc') except AssertionError: pass else: assert False, 'documented CustomEx which is not in __all__' # test noindex flag + options.members = [] options.noindex = True - assert_result_contains(' :noindex:', 'module', 'test_autodoc', [], None) - assert_result_contains(' :noindex:', 'class', 'Base', [], None) + assert_result_contains(' :noindex:', 'module', 'test_autodoc') + assert_result_contains(' :noindex:', 'class', 'Base') # okay, now let's get serious about mixing Python and C signature stuff assert_result_contains('.. class:: CustomDict', 'class', 'CustomDict', - ['__all__'], None) + all_members=True) + + # test inner class handling + assert_processes([('class', 'test_autodoc.Outer'), + ('class', 'test_autodoc.Outer.Inner'), + ('method', 'test_autodoc.Outer.Inner.meth')], + 'class', 'Outer', all_members=True) + + # test generation for C modules (which have no source file) + directive.env.currmodule = 'time' + assert_processes([('function', 'time.asctime')], 'function', 'asctime') + assert_processes([('function', 'time.asctime')], 'function', 'asctime') # --- generate fodder ------------ __all__ = ['Class'] +integer = 1 + class CustomEx(Exception): """My custom exception.""" def f(self): """Exception method.""" - class Base(object): def inheritedmeth(self): """Inherited function.""" @@ -380,9 +458,28 @@ class Class(Base): def undocmeth(self): pass - @property + def skipmeth(self): + """Method that should be skipped.""" + + def excludemeth(self): + """Method that should be excluded.""" + + # should not be documented + skipattr = 'foo' + + #: should be documented -- süß + attr = 'bar' + def prop(self): """Property.""" + # stay 2.4 compatible (docstring!) + prop = property(prop, doc="Property.") + + docattr = 'baz' + """should likewise be documented -- süß""" + + udocattr = 'quux' + u"""should be documented as well - süß""" class CustomDict(dict): """Docstring.""" @@ -392,3 +489,16 @@ def function(foo, *args, **kwds): Return spam. """ pass + + +class Outer(object): + """Foo""" + + class Inner(object): + """Foo""" + + def meth(self): + """Foo""" + + # should be documented as an alias + factory = dict diff --git a/tests/test_build.py b/tests/test_build.py index 888063a9..7b33dc09 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -10,6 +10,7 @@ """ import os +import re import sys import difflib import htmlentitydefs @@ -19,29 +20,41 @@ from subprocess import Popen, PIPE from util import * from etree13 import ElementTree as ET -from sphinx.builder import StandaloneHTMLBuilder, LaTeXBuilder -from sphinx.latexwriter import LaTeXTranslator +try: + import pygments +except ImportError: + pygments = None + +from sphinx.builders.html import StandaloneHTMLBuilder +from sphinx.builders.latex import LaTeXBuilder +from sphinx.writers.latex import LaTeXTranslator + + +def teardown_module(): + (test_root / '_build').rmtree(True) html_warnfile = StringIO() latex_warnfile = StringIO() ENV_WARNINGS = """\ -WARNING: %(root)s/images.txt:9: Image file not readable: foo.png -WARNING: %(root)s/images.txt:23: Nonlocal image URI found: http://www.python.org/logo.png -WARNING: %(root)s/includes.txt:: (WARNING/2) Encoding 'utf-8' used for reading included \ -file u'wrongenc.inc' seems to be wrong, try giving an :encoding: option +%(root)s/images.txt:9: WARNING: image file not readable: foo.png +%(root)s/images.txt:23: WARNING: nonlocal image URI found: \ +http://www.python.org/logo.png +%(root)s/includes.txt:: (WARNING/2) Encoding 'utf-8' used for reading \ +included file u'wrongenc.inc' seems to be wrong, try giving an :encoding: option +%(root)s/includes.txt:56: WARNING: download file not readable: nonexisting.png """ HTML_WARNINGS = ENV_WARNINGS + """\ -WARNING: %(root)s/images.txt:: no matching candidate for image URI u'foo.*' -WARNING: %(root)s/markup.txt:: invalid index entry u'' -WARNING: %(root)s/markup.txt:: invalid pair index entry u'' -WARNING: %(root)s/markup.txt:: invalid pair index entry u'keyword; ' +%(root)s/images.txt:20: WARNING: no matching candidate for image URI u'foo.*' +%(root)s/markup.txt:: WARNING: invalid index entry u'' +%(root)s/markup.txt:: WARNING: invalid pair index entry u'' +%(root)s/markup.txt:: WARNING: invalid pair index entry u'keyword; ' """ LATEX_WARNINGS = ENV_WARNINGS + """\ -WARNING: None:: no matching candidate for image URI u'foo.*' +None:None: WARNING: no matching candidate for image URI u'foo.*' WARNING: invalid pair index entry u'' """ @@ -50,18 +63,27 @@ HTML_XPATH = { ".//img[@src='_images/img.png']": '', ".//img[@src='_images/img1.png']": '', ".//img[@src='_images/simg.png']": '', + ".//object[@data='_images/svgimg.svg']": '', + ".//embed[@src='_images/svgimg.svg']": '', }, 'subdir/images.html': { ".//img[@src='../_images/img1.png']": '', + ".//img[@src='../_images/rimg.png']": '', + }, + 'subdir/includes.html': { + ".//pre/span": 'line 1', + ".//pre/span": 'line 2', + ".//a[@href='../_downloads/img.png']": '', }, 'includes.html': { - ".//pre/span[@class='s']": u'üöä', ".//pre": u'Max Strauß', + ".//a[@href='_downloads/img.png']": '', + ".//a[@href='_downloads/img1.png']": '', }, 'autodoc.html': { ".//dt[@id='test_autodoc.Class']": '', - ".//dt[@id='test_autodoc.function']/em": '**kwds', - ".//dd": 'Return spam.', + ".//dt[@id='test_autodoc.function']/em": r'\*\*kwds', + ".//dd": r'Return spam\.', }, 'markup.html': { ".//meta[@name='author'][@content='Me']": '', @@ -69,23 +91,48 @@ HTML_XPATH = { ".//a[@href='contents.html#ref1']": '', ".//div[@id='label']": '', ".//span[@class='option']": '--help', + ".//p": 'A global substitution.', + ".//p": 'In HTML.', + ".//p": 'In both.', + ".//p": 'Always present', }, 'desc.html': { ".//dt[@id='mod.Cls.meth1']": '', ".//dt[@id='errmod.Error']": '', ".//a[@href='#mod.Cls']": '', + ".//dl[@class='userdesc']": '', + ".//dt[@id='userdescrole-myobj']": '', + ".//a[@href='#userdescrole-myobj']": '', }, 'contents.html': { ".//meta[@name='hc'][@content='hcval']": '', - #".//td[@class='label']": '[Ref1]', # docutils 0.5 only + ".//meta[@name='testopt'][@content='testoverride']": '', + #".//td[@class='label']": r'\[Ref1\]', # docutils 0.5 only ".//td[@class='label']": '', ".//li[@class='toctree-l1']/a": 'Testing various markup', ".//li[@class='toctree-l2']/a": 'Admonitions', ".//title": 'Sphinx <Tests>', ".//div[@class='footer']": 'Georg Brandl & Team', + ".//a[@href='http://python.org/']": '', + }, + '_static/statictmpl.html': { + ".//project": 'Sphinx <Tests>', }, } +if pygments: + HTML_XPATH['includes.html'].update({ + ".//pre/span[@class='s']": u'üöä', + ".//div[@class='inc-pyobj1 highlight-text']/div/pre": + r'^class Foo:\n pass\n\s*$', + ".//div[@class='inc-pyobj2 highlight-text']/div/pre": + r'^ def baz\(\):\n pass\n\s*$', + ".//div[@class='inc-lines highlight-text']/div/pre": + r'^class Foo:\n pass\nclass Bar:\n$', + ".//div[@class='inc-startend highlight-text']/div/pre": + ur'^foo = u"Including Unicode characters: üöä"\n$', + }) + class NslessParser(ET.XMLParser): """XMLParser that throws away namespaces in tag names.""" @@ -101,7 +148,27 @@ class NslessParser(ET.XMLParser): return name -@with_app(buildername='html', warning=html_warnfile, cleanenv=True) +def check_xpath(etree, fname, path, check): + nodes = list(etree.findall(path)) + assert nodes != [], ('did not find any node matching xpath ' + '%r in file %s' % (path, fname)) + if hasattr(check, '__call__'): + check(nodes) + elif not check: + # only check for node presence + pass + else: + rex = re.compile(check) + for node in nodes: + if node.text and rex.search(node.text): + break + else: + assert False, ('%r not found in any node matching ' + 'path %s in %s: %r' % (check, path, fname, + [node.text for node in nodes])) + +@gen_with_app(buildername='html', warning=html_warnfile, cleanenv=True, + tags=['testtag']) def test_html(app): app.builder.build_all() html_warnings = html_warnfile.getvalue().replace(os.sep, '/') @@ -114,18 +181,8 @@ def test_html(app): parser = NslessParser() parser.entity.update(htmlentitydefs.entitydefs) etree = ET.parse(os.path.join(app.outdir, fname), parser) - for path, text in paths.iteritems(): - nodes = list(etree.findall(path)) - assert nodes != [] - if not text: - # only check for node presence - continue - for node in nodes: - if node.text and text in node.text: - break - else: - assert False, ('%r not found in any node matching ' - 'path %s in %s' % (text, path, fname)) + for path, check in paths.iteritems(): + yield check_xpath, etree, fname, path, check @with_app(buildername='latex', warning=latex_warnfile, cleanenv=True) @@ -137,6 +194,8 @@ def test_latex(app): assert latex_warnings == latex_warnings_exp, 'Warnings don\'t match:\n' + \ '\n'.join(difflib.ndiff(latex_warnings_exp.splitlines(), latex_warnings.splitlines())) + # file from latex_additional_files + assert (app.outdir / 'svgimg.svg').isfile() # only run latex if all needed packages are there def kpsetest(filename): @@ -155,12 +214,15 @@ def test_latex(app): return True if kpsetest('article.sty') is None: - print >>sys.stderr, 'info: not running latex, it doesn\'t seem to be installed' + print >>sys.stderr, \ + 'info: not running latex, it doesn\'t seem to be installed' return - for filename in ['fancyhdr.sty', 'fancybox.sty', 'titlesec.sty', 'amsmath.sty', - 'framed.sty', 'color.sty', 'fancyvrb.sty', 'threeparttable.sty']: + for filename in ['fancyhdr.sty', 'fancybox.sty', 'titlesec.sty', + 'amsmath.sty', 'framed.sty', 'color.sty', 'fancyvrb.sty', + 'threeparttable.sty']: if not kpsetest(filename): - print >>sys.stderr, 'info: not running latex, the %s package doesn\'t ' \ + print >>sys.stderr, \ + 'info: not running latex, the %s package doesn\'t ' \ 'seem to be installed' % filename return @@ -169,8 +231,8 @@ def test_latex(app): os.chdir(app.outdir) try: try: - p = Popen(['pdflatex', '--interaction=nonstopmode', 'SphinxTests.tex'], - stdout=PIPE, stderr=PIPE) + p = Popen(['pdflatex', '--interaction=nonstopmode', + 'SphinxTests.tex'], stdout=PIPE, stderr=PIPE) except OSError, err: pass # most likely pdflatex was not found else: @@ -184,14 +246,26 @@ def test_latex(app): # just let the remaining ones run for now -@with_app(buildername='linkcheck', cleanenv=True) +@with_app(buildername='pickle') +def test_pickle(app): + app.builder.build_all() + +@with_app(buildername='linkcheck') def test_linkcheck(app): app.builder.build_all() -@with_app(buildername='text', cleanenv=True) +@with_app(buildername='text') def test_text(app): app.builder.build_all() +@with_app(buildername='htmlhelp') +def test_htmlhelp(app): + app.builder.build_all() + +@with_app(buildername='qthelp') +def test_qthelp(app): + app.builder.build_all() + @with_app(buildername='changes', cleanenv=True) def test_changes(app): app.builder.build_all() diff --git a/tests/test_config.py b/tests/test_config.py index bc5ab8a0..b3aa4eea 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -15,7 +15,8 @@ from util import * from sphinx.application import ExtensionError -@with_app(confoverrides={'master_doc': 'master', 'nonexisting_value': 'True'}) +@with_app(confoverrides={'master_doc': 'master', 'nonexisting_value': 'True', + 'latex_elements.docclass': 'scrartcl'}) def test_core_config(app): cfg = app.config @@ -26,6 +27,7 @@ def test_core_config(app): # overrides assert cfg.master_doc == 'master' + assert cfg.latex_elements['docclass'] == 'scrartcl' # simple default values assert 'exclude_dirs' not in cfg.__dict__ @@ -34,7 +36,7 @@ def test_core_config(app): # complex default values assert 'html_title' not in cfg.__dict__ - assert cfg.html_title == 'Sphinx <Tests> v0.4alpha1 documentation' + assert cfg.html_title == 'Sphinx <Tests> v0.6alpha1 documentation' # complex default values mustn't raise for valuename in cfg.config_values: diff --git a/tests/test_env.py b/tests/test_env.py index 390c6999..a06656d6 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -12,7 +12,8 @@ from util import * from sphinx.environment import BuildEnvironment -from sphinx.builder import StandaloneHTMLBuilder, LaTeXBuilder +from sphinx.builders.html import StandaloneHTMLBuilder +from sphinx.builders.latex import LaTeXBuilder app = env = None warnings = [] @@ -21,14 +22,14 @@ def setup_module(): global app, env app = TestApp(srcdir='(temp)') env = BuildEnvironment(app.srcdir, app.doctreedir, app.config) - env.set_warnfunc(warnings.append) + env.set_warnfunc(lambda *args: warnings.append(args)) def teardown_module(): app.cleanup() def warning_emitted(file, text): for warning in warnings: - if file+':' in warning and text in warning: + if len(warning) == 2 and file+':' in warning[1] and text in warning[0]: return True return False @@ -36,8 +37,7 @@ def warning_emitted(file, text): # afford to not run update() in the setup but in its own test def test_first_update(): - it = env.update(app.config, app.srcdir, app.doctreedir, app) - msg = it.next() + msg, num, it = env.update(app.config, app.srcdir, app.doctreedir, app) assert msg.endswith('%d added, 0 changed, 0 removed' % len(env.found_docs)) docnames = set() for docname in it: # the generator does all the work @@ -45,44 +45,51 @@ def test_first_update(): assert docnames == env.found_docs == set(env.all_docs) def test_images(): - assert warning_emitted('images.txt', 'Image file not readable: foo.png') - assert warning_emitted('images.txt', 'Nonlocal image URI found: ' + assert warning_emitted('images.txt', 'image file not readable: foo.png') + assert warning_emitted('images.txt', 'nonlocal image URI found: ' 'http://www.python.org/logo.png') tree = env.get_doctree('images') app._warning.reset() htmlbuilder = StandaloneHTMLBuilder(app, env) htmlbuilder.post_process_images(tree) - assert "no matching candidate for image URI u'foo.*'" in app._warning.content[-1] - assert set(htmlbuilder.images.keys()) == set(['subdir/img.png', 'img.png', - 'subdir/simg.png']) - assert set(htmlbuilder.images.values()) == set(['img.png', 'img1.png', - 'simg.png']) + assert "no matching candidate for image URI u'foo.*'" in \ + app._warning.content[-1] + assert set(htmlbuilder.images.keys()) == \ + set(['subdir/img.png', 'img.png', 'subdir/simg.png', 'svgimg.svg']) + assert set(htmlbuilder.images.values()) == \ + set(['img.png', 'img1.png', 'simg.png', 'svgimg.svg']) app._warning.reset() latexbuilder = LaTeXBuilder(app, env) latexbuilder.post_process_images(tree) - assert "no matching candidate for image URI u'foo.*'" in app._warning.content[-1] - assert set(latexbuilder.images.keys()) == set(['subdir/img.png', 'subdir/simg.png', - 'img.png', 'img.pdf']) - assert set(latexbuilder.images.values()) == set(['img.pdf', 'img.png', - 'img1.png', 'simg.png']) + assert "no matching candidate for image URI u'foo.*'" in \ + app._warning.content[-1] + assert set(latexbuilder.images.keys()) == \ + set(['subdir/img.png', 'subdir/simg.png', 'img.png', 'img.pdf', + 'svgimg.pdf']) + assert set(latexbuilder.images.values()) == \ + set(['img.pdf', 'img.png', 'img1.png', 'simg.png', 'svgimg.pdf']) def test_second_update(): # delete, add and "edit" (change saved mtime) some files and update again env.all_docs['contents'] = 0 root = path(app.srcdir) - (root / 'images.txt').unlink() + # important: using "autodoc" because it is the last one to be included in + # the contents.txt toctree; otherwise section numbers would shift + (root / 'autodoc.txt').unlink() (root / 'new.txt').write_text('New file\n========\n') - it = env.update(app.config, app.srcdir, app.doctreedir, app) - msg = it.next() - assert '1 added, 1 changed, 1 removed' in msg + msg, num, it = env.update(app.config, app.srcdir, app.doctreedir, app) + assert '1 added, 3 changed, 1 removed' in msg docnames = set() for docname in it: docnames.add(docname) - assert docnames == set(['contents', 'new']) - assert 'images' not in env.all_docs - assert 'images' not in env.found_docs + # "includes" and "images" are in there because they contain references + # to nonexisting downloadable or image files, which are given another + # chance to exist + assert docnames == set(['contents', 'new', 'includes', 'images']) + assert 'autodoc' not in env.all_docs + assert 'autodoc' not in env.found_docs def test_object_inventory(): refs = env.descrefs diff --git a/tests/test_highlighting.py b/tests/test_highlighting.py new file mode 100644 index 00000000..ea1f25f1 --- /dev/null +++ b/tests/test_highlighting.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" + test_highlighting + ~~~~~~~~~~~~~~~~~ + + Test the Pygments highlighting bridge. + + :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from util import * + +try: + import pygments +except ImportError: + from nose.plugins.skip import SkipTest + raise SkipTest('pygments not available') + +from pygments.lexer import RegexLexer +from pygments.token import Text, Name +from pygments.formatters.html import HtmlFormatter + +from sphinx.highlighting import PygmentsBridge + + +class MyLexer(RegexLexer): + name = 'testlexer' + + tokens = { + 'root': [ + ('a', Name), + ('b', Text), + ], + } + + +class MyFormatter(HtmlFormatter): + def format(self, tokensource, outfile): + outfile.write('test') + + +class ComplainOnUnhighlighted(PygmentsBridge): + def unhighlighted(self, source): + raise AssertionError("should highlight %r" % source) + + +@with_app() +def test_add_lexer(app): + app.add_lexer('test', MyLexer()) + + bridge = PygmentsBridge('html') + ret = bridge.highlight_block('ab', 'test') + assert '<span class="n">a</span>b' in ret + +def test_detect_interactive(): + bridge = ComplainOnUnhighlighted('html') + blocks = [ + """ + >>> testing() + True + """, + ] + for block in blocks: + ret = bridge.highlight_block(block.lstrip(), 'python') + assert ret.startswith("<div class=\"highlight\">") + +def test_set_formatter(): + PygmentsBridge.html_formatter = MyFormatter + try: + bridge = PygmentsBridge('html') + ret = bridge.highlight_block('foo', 'python') + assert ret == 'test' + finally: + PygmentsBridge.html_formatter = HtmlFormatter diff --git a/tests/test_markup.py b/tests/test_markup.py index 169cdca9..28ec17f5 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -17,11 +17,13 @@ from docutils import frontend, utils, nodes from docutils.parsers import rst from sphinx import addnodes -from sphinx.htmlwriter import HTMLWriter, SmartyPantsHTMLTranslator -from sphinx.latexwriter import LaTeXWriter, LaTeXTranslator +from sphinx.util import texescape +from sphinx.writers.html import HTMLWriter, SmartyPantsHTMLTranslator +from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator def setup_module(): global app, settings, parser + texescape.init() # otherwise done by the latex builder app = TestApp(cleanenv=True) optparser = frontend.OptionParser( components=(rst.Parser, HTMLWriter, LaTeXWriter)) @@ -79,38 +81,39 @@ def test_inline(): # correct interpretation of code with whitespace _html = ('<p><tt class="docutils literal"><span class="pre">' 'code</span> <span class="pre">sample</span></tt></p>') - verify('``code sample``', _html, '\\code{code sample}') - verify(':samp:`code sample`', _html, '\\samp{code sample}') + yield verify, '``code sample``', _html, '\\code{code sample}' + yield verify, ':samp:`code sample`', _html, '\\samp{code sample}' # interpolation of braces in samp and file roles (HTML only) - verify(':samp:`a{b}c`', + yield (verify, ':samp:`a{b}c`', '<p><tt class="docutils literal"><span class="pre">a</span>' - '<em><span class="pre">b</span></em><span class="pre">c</span></tt></p>', + '<em><span class="pre">b</span></em>' + '<span class="pre">c</span></tt></p>', '\\samp{abc}') # interpolation of arrows in menuselection - verify(':menuselection:`a --> b`', + yield (verify, ':menuselection:`a --> b`', u'<p><em>a \N{TRIANGULAR BULLET} b</em></p>', '\\emph{a \\(\\rightarrow\\) b}') # non-interpolation of dashes in option role - verify_re(':option:`--with-option`', - '<p><em( class="xref")?>--with-option</em></p>$', - r'\\emph{\\texttt{-{-}with-option}}$') + yield (verify_re, ':option:`--with-option`', + '<p><em( class="xref")?>--with-option</em></p>$', + r'\\emph{\\texttt{-{-}with-option}}$') # verify smarty-pants quotes - verify('"John"', '<p>“John”</p>', "``John''") + yield verify, '"John"', '<p>“John”</p>', "``John''" # ... but not in literal text - verify('``"John"``', + yield (verify, '``"John"``', '<p><tt class="docutils literal"><span class="pre">' '"John"</span></tt></p>', '\\code{"John"}') def test_latex_escaping(): # correct escaping in normal mode - verify(u'Γ\\\\∞$', None, ur'\(\Gamma\)\textbackslash{}\(\infty\)\$') + yield verify, u'Γ\\\\∞$', None, ur'\(\Gamma\)\textbackslash{}\(\infty\)\$' # in verbatim code fragments - verify(u'::\n\n @Γ\\∞$[]', None, + yield (verify, u'::\n\n @Γ\\∞$[]', None, u'\\begin{Verbatim}[commandchars=@\\[\\]]\n' u'@PYGZat[]@(@Gamma@)\\@(@infty@)@$@PYGZlb[]@PYGZrb[]\n' u'\\end{Verbatim}') diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 4c2d0f3e..ae001eb6 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -70,6 +70,7 @@ def test_do_prompt(): assert d['k5'] == 'no' raises(AssertionError, qs.do_prompt, d, 'k6', 'Q6', validator=qs.boolean) + @with_tempdir def test_quickstart_defaults(tempdir): answers = { @@ -86,29 +87,31 @@ def test_quickstart_defaults(tempdir): ns = {} execfile(conffile, ns) assert ns['extensions'] == [] - assert ns['templates_path'] == ['.templates'] + assert ns['templates_path'] == ['_templates'] assert ns['source_suffix'] == '.rst' assert ns['master_doc'] == 'index' assert ns['project'] == 'Sphinx Test' assert ns['copyright'] == '%s, Georg Brandl' % time.strftime('%Y') assert ns['version'] == '0.1' assert ns['release'] == '0.1' - assert ns['html_static_path'] == ['.static'] + assert ns['html_static_path'] == ['_static'] assert ns['latex_documents'] == [ ('index', 'SphinxTest.tex', 'Sphinx Test Documentation', 'Georg Brandl', 'manual')] - assert (tempdir / '.static').isdir() - assert (tempdir / '.templates').isdir() + assert (tempdir / '_static').isdir() + assert (tempdir / '_templates').isdir() assert (tempdir / 'index.rst').isfile() assert (tempdir / 'Makefile').isfile() + assert (tempdir / 'make.bat').isfile() + @with_tempdir def test_quickstart_all_answers(tempdir): answers = { 'Root path': tempdir, 'Separate source and build': 'y', - 'Name prefix for templates': '_', + 'Name prefix for templates': '.', 'Project name': 'STASI\xe2\x84\xa2', 'Author name': 'Wolfgang Sch\xc3\xa4uble & G. Beckstein', 'Project version': '2.0', @@ -118,7 +121,13 @@ def test_quickstart_all_answers(tempdir): 'autodoc': 'y', 'doctest': 'yes', 'intersphinx': 'no', + 'todo': 'n', + 'coverage': 'no', + 'pngmath': 'N', + 'jsmath': 'no', + 'ifconfig': 'no', 'Create Makefile': 'no', + 'Create Windows command file': 'no', } qs.raw_input = mock_raw_input(answers, needanswer=True) qs.TERM_ENCODING = 'utf-8' @@ -129,7 +138,7 @@ def test_quickstart_all_answers(tempdir): ns = {} execfile(conffile, ns) assert ns['extensions'] == ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] - assert ns['templates_path'] == ['_templates'] + assert ns['templates_path'] == ['.templates'] assert ns['source_suffix'] == '.txt' assert ns['master_doc'] == 'contents' assert ns['project'] == u'STASI™' @@ -137,12 +146,12 @@ def test_quickstart_all_answers(tempdir): time.strftime('%Y') assert ns['version'] == '2.0' assert ns['release'] == '2.0.1' - assert ns['html_static_path'] == ['_static'] + assert ns['html_static_path'] == ['.static'] assert ns['latex_documents'] == [ ('contents', 'STASI.tex', u'STASI™ Documentation', ur'Wolfgang Schäuble \& G. Beckstein', 'manual')] assert (tempdir / 'build').isdir() - assert (tempdir / 'source' / '_static').isdir() - assert (tempdir / 'source' / '_templates').isdir() + assert (tempdir / 'source' / '.static').isdir() + assert (tempdir / 'source' / '.templates').isdir() assert (tempdir / 'source' / 'contents.txt').isfile() diff --git a/tests/test_theming.py b/tests/test_theming.py new file mode 100644 index 00000000..349a9ce4 --- /dev/null +++ b/tests/test_theming.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +""" + test_theming + ~~~~~~~~~~~~ + + Test the Theme class. + + :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import os +import zipfile + +from util import * + +from sphinx.theming import Theme, ThemeError + + +@with_app(confoverrides={'html_theme': 'ziptheme', + 'html_theme_options.testopt': 'foo'}) +def test_theme_api(app): + cfg = app.config + + # test Theme class API + assert set(Theme.themes.keys()) == \ + set(['basic', 'default', 'sphinxdoc', 'traditional', + 'testtheme', 'ziptheme']) + assert Theme.themes['testtheme'][1] is None + assert isinstance(Theme.themes['ziptheme'][1], zipfile.ZipFile) + + # test Theme instance API + theme = app.builder.theme + assert theme.name == 'ziptheme' + assert theme.themedir_created + themedir = theme.themedir + assert theme.base.name == 'basic' + assert len(theme.get_dirchain()) == 2 + + # direct setting + assert theme.get_confstr('theme', 'stylesheet') == 'custom.css' + # inherited setting + assert theme.get_confstr('options', 'nosidebar') == 'false' + # nonexisting setting + assert theme.get_confstr('theme', 'foobar', 'def') == 'def' + raises(ThemeError, theme.get_confstr, 'theme', 'foobar') + + # options API + raises(ThemeError, theme.get_options, {'nonexisting': 'foo'}) + options = theme.get_options(cfg.html_theme_options) + assert options['testopt'] == 'foo' + assert options['nosidebar'] == 'false' + + # cleanup temp directories + theme.cleanup() + assert not os.path.exists(themedir) diff --git a/tests/util.py b/tests/util.py index 2cd2c031..4bb6a653 100644 --- a/tests/util.py +++ b/tests/util.py @@ -19,7 +19,8 @@ except ImportError: # functools is new in 2.4 wraps = lambda f: (lambda w: w) -from sphinx import application, builder +from sphinx import application +from sphinx.ext.autodoc import AutoDirective from path import path @@ -29,7 +30,7 @@ from nose import tools __all__ = [ 'test_root', 'raises', 'raises_msg', 'Struct', - 'ListOutput', 'TestApp', 'with_app', + 'ListOutput', 'TestApp', 'with_app', 'gen_with_app', 'path', 'with_tempdir', 'write_file', 'sprint', ] @@ -97,12 +98,14 @@ class TestApp(application.Sphinx): """ def __init__(self, srcdir=None, confdir=None, outdir=None, doctreedir=None, - buildername='html', confoverrides=None, status=None, warning=None, - freshenv=None, confname='conf.py', cleanenv=False): + buildername='html', confoverrides=None, + status=None, warning=None, freshenv=None, + warningiserror=None, tags=None, + confname='conf.py', cleanenv=False): application.CONFIG_FILENAME = confname - self.cleanup_trees = [] + self.cleanup_trees = [test_root / 'generated'] if srcdir is None: srcdir = test_root @@ -134,12 +137,15 @@ class TestApp(application.Sphinx): warning = ListOutput('stderr') if freshenv is None: freshenv = False + if warningiserror is None: + warningiserror = False application.Sphinx.__init__(self, srcdir, confdir, outdir, doctreedir, buildername, confoverrides, status, warning, - freshenv) + freshenv, warningiserror, tags) def cleanup(self, doctrees=False): + AutoDirective._registry.clear() for tree in self.cleanup_trees: shutil.rmtree(tree, True) @@ -153,10 +159,26 @@ def with_app(*args, **kwargs): @wraps(func) def deco(*args2, **kwargs2): app = TestApp(*args, **kwargs) - try: - func(app, *args2, **kwargs2) - finally: - app.cleanup() + func(app, *args2, **kwargs2) + # don't execute cleanup if test failed + app.cleanup() + return deco + return generator + + +def gen_with_app(*args, **kwargs): + """ + Make a TestApp with args and kwargs, pass it to the test and clean up + properly. + """ + def generator(func): + @wraps(func) + def deco(*args2, **kwargs2): + app = TestApp(*args, **kwargs) + for item in func(app, *args2, **kwargs2): + yield item + # don't execute cleanup if test failed + app.cleanup() return deco return generator |
