Change domain-index API: introduce a class.

This commit is contained in:
Georg Brandl
2010-02-28 14:45:43 +01:00
parent 1d5894a35a
commit 04f660d021
8 changed files with 199 additions and 166 deletions

View File

@@ -47,6 +47,7 @@ latex_elements = {
'fontpkg': '\\usepackage{palatino}',
}
autodoc_member_order = 'groupwise'
todo_include_todos = True
man_pages = [

View File

@@ -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.

View File

@@ -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:

View File

@@ -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=''):

View File

@@ -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')

View File

@@ -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_<name>_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_<name>_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

View File

@@ -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

View File

@@ -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)