diff --git a/doc/conf.py b/doc/conf.py index 24cf5f3cd..c487f4866 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -47,6 +47,7 @@ latex_elements = { 'fontpkg': '\\usepackage{palatino}', } +autodoc_member_order = 'groupwise' todo_include_todos = True man_pages = [ diff --git a/doc/config.rst b/doc/config.rst index 5dbccb4e2..089026d3a 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -147,9 +147,9 @@ General configuration Directories in which to search for additional Sphinx message catalogs (see :confval:`language`), relative to the source directory. The directories on - this path are searched by the standard :mod:`gettext` module for a domain of - ``sphinx``; so if you add the directory :file:`./locale` to this settting, - the message catalogs must be in + this path are searched by the standard :mod:`gettext` module for a text + domain of ``sphinx``; so if you add the directory :file:`./locale` to this + settting, the message catalogs must be in :file:`./locale/{language}/LC_MESSAGES/sphinx.mo`. The default is ``[]``. @@ -188,8 +188,8 @@ General configuration The name of a reST role (builtin or Sphinx extension) to use as the default role, that is, for text marked up ```like this```. This can be set to - ``'obj'`` to make ```filter``` a cross-reference to the function "filter". - The default is ``None``, which doesn't reassign the default role. + ``'py:obj'`` to make ```filter``` a cross-reference to the Python function + "filter". The default is ``None``, which doesn't reassign the default role. The default role can always be set within individual documents using the standard reST :dir:`default-role` directive. diff --git a/doc/ext/appapi.rst b/doc/ext/appapi.rst index b536fd985..e301cd840 100644 --- a/doc/ext/appapi.rst +++ b/doc/ext/appapi.rst @@ -46,17 +46,22 @@ the following public API: .. method:: Sphinx.add_domain(domain) Make the given *domain* (which must be a class; more precisely, a subclass of - :class:`sphinx.domains.Domain`) known to Sphinx. - - .. XXX where is Domain documented? + :class:`~sphinx.domains.Domain`) known to Sphinx. .. versionadded:: 1.0 .. method:: Sphinx.override_domain(domain) - Make the given *domain* known to Sphinx, assuming that there is already a - domain with its ``.name``. The new domain must be a subclass of the existing - one. + Make the given *domain* class known to Sphinx, assuming that there is already + a domain with its ``.name``. The new domain must be a subclass of the + existing one. + + .. versionadded:: 1.0 + +.. method:: Sphinx.add_index_to_domain(domain, index) + + Add a custom *index* class to the domain named *domain*. *index* must be a + subclass of :class:`~sphinx.domains.Index`. .. versionadded:: 1.0 @@ -445,3 +450,19 @@ The template bridge .. autoclass:: TemplateBridge :members: + + +.. _domain-api: + +Domain API +---------- + +.. module:: sphinx.domains + +.. autoclass:: Domain + :members: + +.. autoclass:: ObjType + +.. autoclass:: Index + :members: diff --git a/sphinx/application.py b/sphinx/application.py index 22ca54d4f..b5ba514c3 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -417,12 +417,11 @@ class Sphinx(object): raise ExtensionError('domain %s not yet registered' % domain) self.domains[domain].roles[name] = role - # XXX needs documentation def add_index_to_domain(self, domain, name, localname, shortname, func): if domain not in self.domains: raise ExtensionError('domain %s not yet registered' % domain) - self.domains[domain].indices.append((name, longname, shortname)) - setattr(self.domains[domain], '_get_%s_index' % name, func) + self.domains[domain].indices.append((name, localname, shortname)) + setattr(self.domains[domain], 'get_%s_index' % name, func) def add_object_type(self, directivename, rolename, indextemplate='', parse_node=None, ref_nodeclass=None, objname=''): diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index cc3a56289..cd5f4f4a4 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -236,8 +236,8 @@ class StandaloneHTMLBuilder(Builder): indices_config = self.config.html_domain_indices if indices_config: for domain in self.env.domains.itervalues(): - for indexinfo in domain.indices: - indexname = '%s-%s' % (domain.name, indexinfo[0]) + for indexcls in domain.indices: + indexname = '%s-%s' % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue @@ -245,8 +245,10 @@ class StandaloneHTMLBuilder(Builder): if indexname == 'py-modindex' and \ not self.config.html_use_modindex: continue - if domain.has_index_entries(indexinfo[0]): - self.domain_indices.append((domain.name,) + indexinfo) + content, collapse = indexcls(domain).generate() + if content: + self.domain_indices.append( + (indexname, indexcls, content, collapse)) # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day @@ -272,9 +274,11 @@ class StandaloneHTMLBuilder(Builder): rellinks = [] if self.config.html_use_index: rellinks.append(('genindex', _('General Index'), 'I', _('index'))) - for index in self.domain_indices: - if index[3]: - rellinks.append(('%s-%s' % index[0:2], index[2], '', index[3])) + for indexname, indexcls, content, collapse in self.domain_indices: + # if it has a short name + if indexcls.shortname: + rellinks.append((indexname, indexcls.localname, + '', indexcls.shortname)) if self.config.html_style is not None: stylename = self.config.html_style @@ -470,14 +474,12 @@ class StandaloneHTMLBuilder(Builder): self.handle_page('genindex', genindexcontext, 'genindex.html') def write_domain_indices(self): - for index in self.domain_indices: - content, collapse = self.env.domains[index[0]].get_index(index[1]) + for indexname, indexcls, content, collapse in self.domain_indices: indexcontext = dict( - indextitle = index[2], + indextitle = indexcls.localname, content = content, collapse_index = collapse, ) - indexname = '%s-%s' % index[0:2] self.info(' ' + indexname, nonl=1) self.handle_page(indexname, indexcontext, 'domainindex.html') diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index a03a74db5..f1e6e8afb 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -10,6 +10,8 @@ :license: BSD, see LICENSE for details. """ +from sphinx.errors import SphinxError + class ObjType(object): """ @@ -23,7 +25,7 @@ class ObjType(object): - *roles*: all the roles that can refer to an object of this type - *attrs*: object attributes -- currently only "searchprio" is known, which defines the object's priority in the full-text search index, - see `Domain.get_objects`. + see :meth:`Domain.get_objects()`. """ known_attrs = { @@ -37,6 +39,63 @@ class ObjType(object): self.attrs.update(attrs) +class Index(object): + """ + An Index is the description for a domain-specific index. To add an index to + a domain, subclass Index, overriding the three name attributes: + + * `name` is an identifier used for generating file names. + * `localname` is the section title for the index. + * `shortname` is a short name for the index, for use in the relation bar in + HTML output. Can be empty to disable entries in the relation bar. + + and providing a :meth:`generate()` method. Then, add the index class to + your domain's `indices` list. Extensions can add indices to existing + domains using :meth:`~sphinx.application.Sphinx.add_index_to_domain()`. + """ + + name = None + localname = None + shortname = None + + def __init__(self, domain): + if self.name is None or self.localname is None: + raise SphinxError('Index subclass %s has no valid name or localname' + % self.__class__.__name__) + self.domain = domain + + def generate(self, docnames=None): + """ + Return entries for the index given by *name*. If *docnames* is given, + restrict to entries referring to these docnames. + + The return value is a tuple of ``(content, collapse)``, where *collapse* + is a boolean that determines if sub-entries should start collapsed (for + output formats that support collapsing sub-entries). + + *content* is a sequence of ``(letter, entries)`` tuples, where *letter* + is the "heading" for the given *entries*, usually the starting letter. + + *entries* is a sequence of single entries, where a single entry is a + sequence ``[name, subtype, docname, anchor, extra, qualifier, descr]``. + The items in this sequence have the following meaning: + + - `name` -- the name of the index entry to be displayed + - `subtype` -- sub-entry related type: + 0 -- normal entry + 1 -- entry with sub-entries + 2 -- sub-entry + - `docname` -- docname where the entry is located + - `anchor` -- anchor for the entry within `docname` + - `extra` -- extra info for the entry + - `qualifier` -- qualifier for the description + - `descr` -- description for the entry + + Qualifier and description are not rendered e.g. in LaTeX output. + """ + return [] + + class Domain(object): """ A Domain is meant to be a group of "object" description directives for @@ -45,9 +104,9 @@ class Domain(object): of a templating language, Sphinx roles and directives, etc. Each domain has a separate storage for information about existing objects - and how to reference them in `data`, which must be a dictionary. It also - must implement several functions that expose the object information in a - uniform way to parts of Sphinx that allow the user to reference or search + and how to reference them in `self.data`, which must be a dictionary. It + also must implement several functions that expose the object information in + a uniform way to parts of Sphinx that allow the user to reference or search for objects in a domain-agnostic way. About `self.data`: since all object and cross-referencing information is @@ -56,8 +115,8 @@ class Domain(object): build process starts, every active domain is instantiated and given the environment object; the `domaindata` dict must then either be nonexistent or a dictionary whose 'version' key is equal to the domain class' - `data_version` attribute. Otherwise, `IOError` is raised and the pickled - environment is discarded. + :attr:`data_version` attribute. Otherwise, `IOError` is raised and the + pickled environment is discarded. """ #: domain name: should be short, but unique @@ -70,12 +129,12 @@ class Domain(object): directives = {} #: role name -> role callable roles = {} - #: (index identifier, localized index name, localized short name) tuples + #: a list of Index subclasses indices = [] #: data value for a fresh environment initial_data = {} - #: data version + #: data version, bump this when the format of `self.data` changes data_version = 0 def __init__(self, env): @@ -153,8 +212,8 @@ class Domain(object): then given to the 'missing-reference' event, and if that yields no resolution, replaced by *contnode*. - The method can also raise `sphinx.environment.NoUri` to suppress the - 'missing-reference' event being emitted. + The method can also raise :exc:`sphinx.environment.NoUri` to suppress + the 'missing-reference' event being emitted. """ pass @@ -170,63 +229,13 @@ class Domain(object): * `priority` -- how "important" the object is (determines placement in search results) - 1: default priority (placed before full-text matches) - 0: object is important (placed before default-priority objects) - 2: object is unimportant (placed after full-text matches) - -1: object should not show up in search at all + - 1: default priority (placed before full-text matches) + - 0: object is important (placed before default-priority objects) + - 2: object is unimportant (placed after full-text matches) + - -1: object should not show up in search at all """ return [] - def has_index_entries(self, name, docnames=None): - """ - Return True if there are entries for the index given by *name*. If - *docnames* is given, restrict to entries referring to these docnames. - - Do not overwrite this method, add a method ``has__entries(self, - docnames=None)`` method for every index. - """ - func = getattr(self, 'has_%s_entries' % name, None) - if not func: - return bool(self.get_index(name, docnames)) - return func(docnames) - - def get_index(self, name, docnames=None): - """ - Return entries for the index given by *name*. If *docnames* is given, - restrict to entries referring to these docnames. - - The return value is a tuple of ``(content, collapse)``, where *collapse* - is a boolean that determines if sub-entries should start collapsed (for - output formats that support collapsing sub-entries). - - *content* is a sequence of ``(letter, entries)`` tuples, where *letter* - is the "heading" for the given *entries*, usually the starting letter. - - *entries* is a sequence of single entries, where a single entry is a - sequence ``[name, subtype, docname, anchor, extra, qualifier, descr]``. - The items in this sequence have the following meaning: - - - `name` -- the name of the index entry to be displayed - - `subtype` -- sub-entry related type: - 0 -- normal entry - 1 -- entry with sub-entries - 2 -- sub-entry - - `docname` -- docname where the entry is located - - `anchor` -- anchor for the entry within `docname` - - `extra` -- extra info for the entry - - `qualifier` -- qualifier for the description - - `descr` -- description for the entry - - Qualifier and description are not rendered e.g. in LaTeX output. - - Do not overwrite this method, add a method ``get__index(self, - docnames=None)`` method for every index. - """ - func = getattr(self, 'get_%s_index' % name, None) - if not func: - return [] - return func(docnames) - from sphinx.domains.c import CDomain from sphinx.domains.std import StandardDomain diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index af1392a60..af8b2eda1 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -17,7 +17,7 @@ from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.roles import XRefRole from sphinx.locale import l_, _ -from sphinx.domains import Domain, ObjType +from sphinx.domains import Domain, ObjType, Index from sphinx.directives import ObjectDescription from sphinx.util.nodes import make_refnode from sphinx.util.compat import Directive @@ -419,6 +419,75 @@ class PyXRefRole(XRefRole): return title, target +class PythonModuleIndex(Index): + """ + Index subclass to provide the Python module index. + """ + + name = 'modindex' + localname = l_('Python Module Index') + shortname = l_('modules') + + def generate(self, docnames=None): + content = {} + # list of prefixes to ignore + ignores = self.domain.env.config['modindex_common_prefix'] + ignores = sorted(ignores, key=len, reverse=True) + # list of all modules, sorted by module name + modules = sorted(self.domain.data['modules'].iteritems(), + key=lambda x: x[0].lower()) + # sort out collapsable modules + prev_modname = '' + num_toplevels = 0 + for modname, (docname, synopsis, platforms, deprecated) in modules: + if docnames and docname not in docnames: + continue + + for ignore in ignores: + if modname.startswith(ignore): + modname = modname[len(ignore):] + stripped = ignore + break + else: + stripped = '' + + # we stripped the whole module name? + if not modname: + modname, stripped = stripped, '' + + entries = content.setdefault(modname[0].lower(), []) + + package = modname.split('.')[0] + if package != modname: + # it's a submodule + if prev_modname == package: + # first submodule - make parent a group head + entries[-1][1] = 1 + elif not prev_modname.startswith(package): + # submodule without parent in list, add dummy entry + entries.append([stripped + package, 1, '', '', '', '', '']) + subtype = 2 + else: + num_toplevels += 1 + subtype = 0 + + qualifier = deprecated and _('Deprecated') or '' + entries.append([stripped + modname, subtype, docname, + 'module-' + stripped + modname, platforms, + qualifier, synopsis]) + prev_modname = modname + + # apply heuristics when to collapse modindex at page load: + # only collapse if number of toplevel modules is larger than + # number of submodules + collapse = len(modules) - num_toplevels < num_toplevels + + # sort by first letter + content = sorted(content.iteritems()) + + return content, collapse + + class PythonDomain(Domain): """Python language domain.""" name = 'py' @@ -463,7 +532,7 @@ class PythonDomain(Domain): 'modules': {}, # modname -> docname, synopsis, platform, deprecated } indices = [ - ('modindex', l_('Python Module Index'), l_('modules')), + PythonModuleIndex, ] def clear_doc(self, docname): @@ -548,71 +617,3 @@ class PythonDomain(Domain): yield (modname, 'module', info[0], 'module-' + modname, 0) for refname, (docname, type) in self.data['objects'].iteritems(): yield (refname, type, docname, refname, 1) - - def has_modindex_entries(self, docnames=None): - if not docnames: - return bool(self.data['modules']) - else: - for modname, info in self.data['modules'].iteritems(): - if info[0] in docnames: - return True - return False - - def get_modindex_index(self, docnames=None): - content = {} - # list of prefixes to ignore - ignores = self.env.config['modindex_common_prefix'] - ignores = sorted(ignores, key=len, reverse=True) - # list of all modules, sorted by module name - modules = sorted(self.data['modules'].iteritems(), - key=lambda x: x[0].lower()) - # sort out collapsable modules - prev_modname = '' - num_toplevels = 0 - for modname, (docname, synopsis, platforms, deprecated) in modules: - if docnames and docname not in docnames: - continue - - for ignore in ignores: - if modname.startswith(ignore): - modname = modname[len(ignore):] - stripped = ignore - break - else: - stripped = '' - - # we stripped the whole module name? - if not modname: - modname, stripped = stripped, '' - - entries = content.setdefault(modname[0].lower(), []) - - package = modname.split('.')[0] - if package != modname: - # it's a submodule - if prev_modname == package: - # first submodule - make parent a group head - entries[-1][1] = 1 - elif not prev_modname.startswith(package): - # submodule without parent in list, add dummy entry - entries.append([stripped + package, 1, '', '', '', '', '']) - subtype = 2 - else: - num_toplevels += 1 - subtype = 0 - - qualifier = deprecated and _('Deprecated') or '' - entries.append([stripped + modname, subtype, docname, - 'module-' + stripped + modname, platforms, - qualifier, synopsis]) - prev_modname = modname - - # apply heuristics when to collapse modindex at page load: - # only collapse if number of toplevel modules is larger than - # number of submodules - collapse = len(modules) - num_toplevels < num_toplevels - - # sort by first letter - content = sorted(content.iteritems()) - - return content, collapse diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index fd33caec0..0343687b4 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -276,8 +276,8 @@ class LaTeXTranslator(nodes.NodeVisitor): indices_config = self.builder.config.latex_domain_indices if indices_config: for domain in self.builder.env.domains.itervalues(): - for indexinfo in domain.indices: - indexname = '%s-%s' % (domain.name, indexinfo[0]) + for indexcls in domain.indices: + indexname = '%s-%s' % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue @@ -285,13 +285,13 @@ class LaTeXTranslator(nodes.NodeVisitor): if indexname == 'py-modindex' and \ not self.builder.config.latex_use_modindex: continue - if not domain.has_index_entries(indexinfo[0], - self.builder.docnames): + content, collapsed = indexcls(domain).generate( + self.builder.docnames) + if not content: continue ret.append('\\renewcommand{\\indexname}{%s}\n' % - indexinfo[1]) - generate(*domain.get_index(indexinfo[0], - self.builder.docnames)) + indexcls.localname) + generate(content, collapsed) return ''.join(ret)