diff --git a/AUTHORS b/AUTHORS index a24d6a545..2a9dbbac9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,6 +13,7 @@ Other contributors, listed alphabetically, are: * Charles Duffy -- original graphviz extension * Kevin Dunn -- MathJax extension * Josip Dzolonga -- coverage builder +* Hernan Grecco -- search improvements * Horst Gutmann -- internationalization support * Martin Hans -- autodoc improvements * Doug Hellmann -- graphviz improvements diff --git a/CHANGES b/CHANGES index 638bbda77..d7ec12a3a 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,31 @@ Release 1.2 (in development) admonition title ("See Also" instead of "See also"), and spurious indentation in the text builder. +* sphinx-build now has a verbose option :option:`-v` which can be + repeated for greater effect. A single occurrance provides a + slightly more verbose output than normal. Two or more occurrences + of this option provides more detailed output which may be useful for + debugging. + +* sphinx-build now provides more specific error messages when called with + invalid options or arguments. + +* sphinx-build now supports the standard :option:`--help` and + :option:`--version` options. + +* #869: sphinx-build now has the option :option:`-T` for printing the full + traceback after an unhandled exception. + +* #976: Fix gettext does not extract index entries. + +* #940: Fix gettext does not extract figure caption. + +* #1067: Improve the ordering of the JavaScript search results: matches in titles + come before matches in full text, and object results are better categorized. + Also implement a pluggable search scorer. + +* Fix text writer can not handle visit_legend for figure directive contents. + * PR#72: #975: Fix gettext does not extract definition terms before docutils 0.10.0 * PR#25: In inheritance diagrams, the first line of the class docstring @@ -67,6 +92,8 @@ Release 1.2 (in development) * #1041: Fix cpp domain parser fails to parse a const type with a modifier. +* #958: Do not preserve ``environment.pickle`` after a failed build. + * PR#88: Added the "Sphinx Developer's Guide" (:file:`doc/devguide.rst`) which outlines the basic development process of the Sphinx project. diff --git a/doc/config.rst b/doc/config.rst index 0ff6d4051..6174d4871 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -760,6 +760,15 @@ that use Sphinx' HTMLWriter class. .. versionadded:: 1.1 +.. confval:: html_search_scorer + + The name of a javascript file (relative to the configuration directory) that + implements a search results scorer. If empty, the default will be used. + + .. XXX describe interface for scorer here + + .. versionadded:: 1.2 + .. confval:: htmlhelp_basename Output file base name for HTML help builder. Default is ``'pydoc'``. diff --git a/doc/templating.rst b/doc/templating.rst index 05a1346c0..b9dfc683b 100644 --- a/doc/templating.rst +++ b/doc/templating.rst @@ -391,3 +391,6 @@ are in HTML form), these variables are also available: * ``titles_only`` (false by default): if true, put only toplevel document titles in the tree + + * ``includehidden`` (false by default): if true, the TOC tree will also + contain hidden entries. diff --git a/sphinx/application.py b/sphinx/application.py index 9cf90f1dd..7e07f6b91 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -11,6 +11,7 @@ :license: BSD, see LICENSE for details. """ +import os import sys import types import posixpath @@ -60,7 +61,8 @@ class Sphinx(object): def __init__(self, srcdir, confdir, outdir, doctreedir, buildername, confoverrides=None, status=sys.stdout, warning=sys.stderr, - freshenv=False, warningiserror=False, tags=None): + freshenv=False, warningiserror=False, tags=None, verbosity=0): + self.verbosity = verbosity self.next_listener_id = 0 self._extensions = {} self._listeners = {} @@ -203,12 +205,27 @@ class Sphinx(object): else: self.builder.build_update() except Exception, err: + # delete the saved env to force a fresh build next time + envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME) + if path.isfile(envfile): + os.unlink(envfile) self.emit('build-finished', err) raise else: self.emit('build-finished', None) self.builder.cleanup() + def _log(self, message, wfile, nonl=False): + try: + wfile.write(message) + except UnicodeEncodeError: + encoding = getattr(wfile, 'encoding', 'ascii') or 'ascii' + wfile.write(message.encode(encoding, 'replace')) + if not nonl: + wfile.write('\n') + if hasattr(wfile, 'flush'): + wfile.flush() + def warn(self, message, location=None, prefix='WARNING: '): if isinstance(location, tuple): docname, lineno = location @@ -221,26 +238,30 @@ class Sphinx(object): if self.warningiserror: raise SphinxWarning(warntext) self._warncount += 1 - try: - self._warning.write(warntext) - except UnicodeEncodeError: - encoding = getattr(self._warning, 'encoding', 'ascii') or 'ascii' - self._warning.write(warntext.encode(encoding, 'replace')) + self._log(warntext, self._warning, True) def info(self, message='', nonl=False): - try: - self._status.write(message) - except UnicodeEncodeError: - encoding = getattr(self._status, 'encoding', 'ascii') or 'ascii' - self._status.write(message.encode(encoding, 'replace')) - if not nonl: - self._status.write('\n') - self._status.flush() + self._log(message, self._status, nonl) + + def verbose(self, message, *args, **kwargs): + if self.verbosity < 1: + return + if args or kwargs: + message = message % (args or kwargs) + self._log(message, self._warning) + + def debug(self, message, *args, **kwargs): + if self.verbosity < 2: + return + if args or kwargs: + message = message % (args or kwargs) + self._log(message, self._warning) # general extensibility interface def setup_extension(self, extension): """Import and setup a Sphinx extension module. No-op if called twice.""" + self.debug('setting up extension: %r', extension) if extension in self._extensions: return try: @@ -301,9 +322,12 @@ class Sphinx(object): else: self._listeners[event][listener_id] = callback self.next_listener_id += 1 + self.debug('connecting event %r: %r [id=%s]', + event, callback, listener_id) return listener_id def disconnect(self, listener_id): + self.debug('disconnecting event: [id=%s]', listener_id) for event in self._listeners.itervalues(): event.pop(listener_id, None) @@ -323,6 +347,7 @@ class Sphinx(object): # registering addon parts def add_builder(self, builder): + self.debug('adding builder: %r', builder) if not hasattr(builder, 'name'): raise ExtensionError('Builder class %s has no "name" attribute' % builder) @@ -337,6 +362,7 @@ class Sphinx(object): self.builderclasses[builder.name] = builder def add_config_value(self, name, default, rebuild): + self.debug('adding config value: %r', (name, default, rebuild)) if name in self.config.values: raise ExtensionError('Config value %r already present' % name) if rebuild in (False, True): @@ -344,11 +370,13 @@ class Sphinx(object): self.config.values[name] = (default, rebuild) def add_event(self, name): + self.debug('adding event: %r', name) if name in self._events: raise ExtensionError('Event %r already present' % name) self._events[name] = '' def add_node(self, node, **kwds): + self.debug('adding node: %r', (node, kwds)) nodes._add_node_class_names([node.__name__]) for key, val in kwds.iteritems(): try: @@ -388,24 +416,30 @@ class Sphinx(object): return obj def add_directive(self, name, obj, content=None, arguments=None, **options): + self.debug('adding directive: %r', + (name, obj, content, arguments, options)) directives.register_directive( name, self._directive_helper(obj, content, arguments, **options)) def add_role(self, name, role): + self.debug('adding role: %r', (name, role)) roles.register_local_role(name, role) def add_generic_role(self, name, nodeclass): # don't use roles.register_generic_role because it uses # register_canonical_role + self.debug('adding generic role: %r', (name, nodeclass)) role = roles.GenericRole(name, nodeclass) roles.register_local_role(name, role) def add_domain(self, domain): + self.debug('adding domain: %r', domain) if domain.name in self.domains: raise ExtensionError('domain %s already registered' % domain.name) self.domains[domain.name] = domain def override_domain(self, domain): + self.debug('overriding domain: %r', domain) if domain.name not in self.domains: raise ExtensionError('domain %s not yet registered' % domain.name) if not issubclass(domain, self.domains[domain.name]): @@ -415,17 +449,21 @@ class Sphinx(object): def add_directive_to_domain(self, domain, name, obj, content=None, arguments=None, **options): + self.debug('adding directive to domain: %r', + (domain, name, obj, content, arguments, options)) if domain not in self.domains: raise ExtensionError('domain %s not yet registered' % domain) self.domains[domain].directives[name] = \ self._directive_helper(obj, content, arguments, **options) def add_role_to_domain(self, domain, name, role): + self.debug('adding role to domain: %r', (domain, name, role)) if domain not in self.domains: raise ExtensionError('domain %s not yet registered' % domain) self.domains[domain].roles[name] = role def add_index_to_domain(self, domain, index): + self.debug('adding index to domain: %r', (domain, index)) if domain not in self.domains: raise ExtensionError('domain %s not yet registered' % domain) self.domains[domain].indices.append(index) @@ -433,6 +471,9 @@ class Sphinx(object): def add_object_type(self, directivename, rolename, indextemplate='', parse_node=None, ref_nodeclass=None, objname='', doc_field_types=[]): + self.debug('adding object type: %r', + (directivename, rolename, indextemplate, parse_node, + ref_nodeclass, objname, doc_field_types)) StandardDomain.object_types[directivename] = \ ObjType(objname or directivename, rolename) # create a subclass of GenericObject as the new directive @@ -449,6 +490,9 @@ class Sphinx(object): def add_crossref_type(self, directivename, rolename, indextemplate='', ref_nodeclass=None, objname=''): + self.debug('adding crossref type: %r', + (directivename, rolename, indextemplate, ref_nodeclass, + objname)) StandardDomain.object_types[directivename] = \ ObjType(objname or directivename, rolename) # create a subclass of Target as the new directive @@ -459,9 +503,11 @@ class Sphinx(object): StandardDomain.roles[rolename] = XRefRole(innernodeclass=ref_nodeclass) def add_transform(self, transform): + self.debug('adding transform: %r', transform) SphinxStandaloneReader.transforms.append(transform) def add_javascript(self, filename): + self.debug('adding javascript: %r', filename) from sphinx.builders.html import StandaloneHTMLBuilder if '://' in filename: StandaloneHTMLBuilder.script_files.append(filename) @@ -470,6 +516,7 @@ class Sphinx(object): posixpath.join('_static', filename)) def add_stylesheet(self, filename): + self.debug('adding stylesheet: %r', filename) from sphinx.builders.html import StandaloneHTMLBuilder if '://' in filename: StandaloneHTMLBuilder.css_files.append(filename) @@ -478,21 +525,25 @@ class Sphinx(object): posixpath.join('_static', filename)) def add_lexer(self, alias, lexer): + self.debug('adding lexer: %r', (alias, lexer)) from sphinx.highlighting import lexers if lexers is None: return lexers[alias] = lexer def add_autodocumenter(self, cls): + self.debug('adding autodocumenter: %r', cls) from sphinx.ext import autodoc autodoc.add_documenter(cls) self.add_directive('auto' + cls.objtype, autodoc.AutoDirective) def add_autodoc_attrgetter(self, type, getter): + self.debug('adding autodoc attrgetter: %r', (type, getter)) from sphinx.ext import autodoc autodoc.AutoDirective._special_attrgetters[type] = getter def add_search_language(self, cls): + self.debug('adding search language: %r', cls) from sphinx.search import languages, SearchLanguage assert isinstance(cls, SearchLanguage) languages[cls.lang] = cls diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 9a869aa97..97932c4c7 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -119,9 +119,13 @@ class Builder(object): summary = bold(summary) for item in iterable: l += 1 - self.info(term_width_line('%s[%3d%%] %s' % - (summary, 100*l/length, - colorfunc(item))), nonl=1) + s = '%s[%3d%%] %s' % (summary, 100*l/length, + colorfunc(item)) + if self.app.verbosity: + s += '\n' + else: + s = term_width_line(s) + self.info(s, nonl=1) yield item if l > 0: self.info() diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index c07f3fc9d..80a242996 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -15,9 +15,11 @@ from datetime import datetime from collections import defaultdict from sphinx.builders import Builder -from sphinx.util.nodes import extract_messages +from sphinx.util import split_index_msg +from sphinx.util.nodes import extract_messages, traverse_translatable_index from sphinx.util.osutil import SEP, safe_relpath, ensuredir, find_catalog from sphinx.util.console import darkgreen +from sphinx.locale import pairindextypes POHEADER = ur""" # SOME DESCRIPTIVE TITLE. @@ -82,6 +84,16 @@ class I18nBuilder(Builder): for node, msg in extract_messages(doctree): catalog.add(msg, node) + # Extract translatable messages from index entries. + for node, entries in traverse_translatable_index(doctree): + for typ, msg, tid, main in entries: + for m in split_index_msg(typ, msg): + if typ == 'pair' and m in pairindextypes.values(): + # avoid built-in translated message was incorporated + # in 'sphinx.util.nodes.process_index_entry' + continue + catalog.add(m, node) + class MessageCatalogBuilder(I18nBuilder): """ diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 79c66bfb5..573bb9b2c 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -240,7 +240,8 @@ class StandaloneHTMLBuilder(Builder): if not lang or lang not in languages: lang = 'en' self.indexer = IndexBuilder(self.env, lang, - self.config.html_search_options) + self.config.html_search_options, + self.config.html_search_scorer) self.load_indexer(docnames) self.docwriter = HTMLWriter(self) @@ -653,6 +654,8 @@ class StandaloneHTMLBuilder(Builder): self.indexer.feed(pagename, title, doctree) def _get_local_toctree(self, docname, collapse=True, **kwds): + if 'includehidden' not in kwds: + kwds['includehidden'] = False return self.render_partial(self.env.get_toctree_for( docname, self, collapse, **kwds))['fragment'] diff --git a/sphinx/cmdline.py b/sphinx/cmdline.py index 20ee536b2..0a1cab7dd 100644 --- a/sphinx/cmdline.py +++ b/sphinx/cmdline.py @@ -59,6 +59,10 @@ new and changed files -w -- write warnings (and errors) to given file -W -- turn warnings into errors -P -- run Pdb on exception + -T -- show full traceback on exception + -v -- increase verbosity (can be repeated) + --help -- show this help and exit + --version -- show version information and exit Modi: * without -a and without filenames, write new and changed files. * with -a, write all files. @@ -71,8 +75,15 @@ def main(argv): nocolor() try: - opts, args = getopt.getopt(argv[1:], 'ab:t:d:c:CD:A:ng:NEqQWw:P') + opts, args = getopt.getopt(argv[1:], 'ab:t:d:c:CD:A:ng:NEqQWw:PThv', + ['help', 'version']) allopts = set(opt[0] for opt in opts) + if '-h' in allopts or '--help' in allopts: + usage(argv) + return 0 + if '--version' in allopts: + print 'Sphinx (sphinx-build) %s' % __version__ + return 0 srcdir = confdir = abspath(args[0]) if not path.isdir(srcdir): print >>sys.stderr, 'Error: Cannot find source directory `%s\'.' % ( @@ -87,15 +98,18 @@ def main(argv): if not path.isdir(outdir): print >>sys.stderr, 'Making output directory...' os.makedirs(outdir) - except (IndexError, getopt.error): - usage(argv) + except getopt.error, err: + usage(argv, 'Error: %s' % err) + return 1 + except IndexError: + usage(argv, 'Error: Insufficient arguments.') return 1 filenames = args[2:] err = 0 for filename in filenames: if not path.isfile(filename): - print >>sys.stderr, 'Cannot find file %r.' % filename + print >>sys.stderr, 'Error: Cannot find file %r.' % filename err = 1 if err: return 1 @@ -109,6 +123,8 @@ def main(argv): buildername = None force_all = freshenv = warningiserror = use_pdb = False + show_traceback = False + verbosity = 0 status = sys.stdout warning = sys.stderr error = sys.stderr @@ -121,7 +137,7 @@ def main(argv): buildername = val elif opt == '-a': if filenames: - usage(argv, 'Cannot combine -a option and filenames.') + usage(argv, 'Error: Cannot combine -a option and filenames.') return 1 force_all = True elif opt == '-t': @@ -185,6 +201,11 @@ def main(argv): warnfile = val elif opt == '-P': use_pdb = True + elif opt == '-T': + show_traceback = True + elif opt == '-v': + verbosity += 1 + show_traceback = True if warning and warnfile: warnfp = open(warnfile, 'w') @@ -194,17 +215,10 @@ def main(argv): try: app = Sphinx(srcdir, confdir, outdir, doctreedir, buildername, confoverrides, status, warning, freshenv, - warningiserror, tags) + warningiserror, tags, verbosity) app.build(force_all, filenames) return app.statuscode - except KeyboardInterrupt: - if use_pdb: - import pdb - print >>error, red('Interrupted while building, starting debugger:') - traceback.print_exc() - pdb.post_mortem(sys.exc_info()[2]) - return 1 - except Exception, err: + except (Exception, KeyboardInterrupt), err: if use_pdb: import pdb print >>error, red('Exception occurred while building, ' @@ -213,7 +227,12 @@ def main(argv): pdb.post_mortem(sys.exc_info()[2]) else: print >>error - if isinstance(err, SystemMessage): + if show_traceback: + traceback.print_exc(None, error) + print >>error + if isinstance(err, KeyboardInterrupt): + print >>error, 'interrupted!' + elif isinstance(err, SystemMessage): print >>error, red('reST markup error:') print >>error, terminal_safe(err.args[0]) elif isinstance(err, SphinxError): diff --git a/sphinx/config.py b/sphinx/config.py index 1134eb866..b91bd0b11 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -110,6 +110,7 @@ class Config(object): html_secnumber_suffix = ('. ', 'html'), html_search_language = (None, 'html'), html_search_options = ({}, 'html'), + html_search_scorer = ('', None), # HTML help only options htmlhelp_basename = (lambda self: make_filename(self.project), None), diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 89d8ac624..3167d606f 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -169,6 +169,7 @@ class Index(Directive): indexnode = addnodes.index() indexnode['entries'] = ne = [] indexnode['inline'] = False + set_source_info(self, indexnode) for entry in arguments: ne.extend(process_index_entry(entry, targetid)) return [indexnode, targetnode] diff --git a/sphinx/environment.py b/sphinx/environment.py index 85bda8a5f..2ce1a6fc1 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -38,9 +38,9 @@ from docutils.transforms.parts import ContentsFilter from sphinx import addnodes from sphinx.util import url_re, get_matching_docs, docname_join, split_into, \ - FilenameUniqDict + split_index_msg, FilenameUniqDict from sphinx.util.nodes import clean_astext, make_refnode, extract_messages, \ - WarningStream + traverse_translatable_index, WarningStream from sphinx.util.osutil import movefile, SEP, ustrftime, find_catalog, \ fs_encoding from sphinx.util.matching import compile_matchers @@ -71,7 +71,7 @@ default_settings = { # This is increased every time an environment attribute is added # or changed to properly invalidate pickle files. -ENV_VERSION = 41 +ENV_VERSION = 42 default_substitutions = set([ @@ -303,6 +303,23 @@ class Locale(Transform): child.parent = node node.children = patch.children + # Extract and translate messages for index entries. + for node, entries in traverse_translatable_index(self.document): + new_entries = [] + for type, msg, tid, main in entries: + msg_parts = split_index_msg(type, msg) + msgstr_parts = [] + for part in msg_parts: + msgstr = catalog.gettext(part) + if not msgstr: + msgstr = part + msgstr_parts.append(msgstr) + + new_entries.append((type, ';'.join(msgstr_parts), tid, main)) + + node['raw_entries'] = entries + node['entries'] = new_entries + class SphinxStandaloneReader(standalone.Reader): """ @@ -365,9 +382,7 @@ class BuildEnvironment: del self.config.values domains = self.domains del self.domains - # first write to a temporary file, so that if dumping fails, - # the existing environment won't be overwritten - picklefile = open(filename + '.tmp', 'wb') + picklefile = open(filename, 'wb') # remove potentially pickling-problematic values from config for key, val in vars(self.config).items(): if key.startswith('_') or \ @@ -379,7 +394,6 @@ class BuildEnvironment: pickle.dump(self, picklefile, pickle.HIGHEST_PROTOCOL) finally: picklefile.close() - movefile(filename + '.tmp', filename) # reset attributes self.domains = domains self.config.values = values @@ -954,6 +968,7 @@ class BuildEnvironment: filterlevel = self.config.keep_warnings and 2 or 5 for node in doctree.traverse(nodes.system_message): if node['level'] < filterlevel: + self.app.debug('%s [filtered system message]', node.astext()) node.parent.remove(node) @@ -1340,46 +1355,56 @@ class BuildEnvironment: if toctree.get('hidden', False) and not includehidden: return None - def _walk_depth(node, depth, maxdepth): + # For reading the following two helper function, it is useful to keep + # in mind the node structure of a toctree (using HTML-like node names + # for brevity): + # + # + # + # The transformation is made in two passes in order to avoid + # interactions between marking and pruning the tree (see bug #1046). + + def _toctree_prune(node, depth, maxdepth): """Utility: Cut a TOC at a specified depth.""" - - # For reading this function, it is useful to keep in mind the node - # structure of a toctree (using HTML-like node names for brevity): - # - # - for subnode in node.children[:]: if isinstance(subnode, (addnodes.compact_paragraph, nodes.list_item)): - # for

and

  • , just indicate the depth level and - # recurse to children - subnode['classes'].append('toctree-l%d' % (depth-1)) - _walk_depth(subnode, depth, maxdepth) - + # for

    and

  • , just recurse + _toctree_prune(subnode, depth, maxdepth) elif isinstance(subnode, nodes.bullet_list): # for