enhance sphinx integration to allow links to option definitions

Use a Sphinx domain to manage the locations of options so we can produce
links back to them with some custom roles. Add documentation for using
the three primary interfaces into Sphinx.

Change-Id: Ibe2363d3ec688c7294cdd41cced853bed17c26bb
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2016-01-16 00:37:10 +00:00
parent 016198b8d7
commit 6884457891
4 changed files with 189 additions and 9 deletions

View File

@ -22,10 +22,11 @@ Contents
namespaces
styleguide
generator
builtins
sphinxext
sphinxconfiggen
faq
contributing
builtins
sphinxconfiggen
Release Notes
=============

33
doc/source/sphinxext.rst Normal file
View File

@ -0,0 +1,33 @@
====================
Sphinx Integration
====================
The ``oslo_config.sphinxext`` module defines a custom domain for
documenting configuration options. The domain includes a directive and
two roles.
.. rst:directive:: show-options
Given a namespace, show all of the options exported from that
namespace.
::
.. show-options:: oslo.config
.. rst:role:: option
Link to an option.
::
:oslo.config:option:`config_file`
:oslo.config:option:`DEFAULT.config_file`
.. rst:role:: group
Link to an option group.
::
:oslo.config:group:`DEFAULT`

View File

@ -1915,6 +1915,10 @@ class ConfigOpts(collections.Mapping):
ConfigOpts is a configuration option manager with APIs for registering
option schemas, grouping options, parsing option values and retrieving
the values of options.
It has built-in support for :oslo.config:option:`config_file` and
:oslo.config:option:`config_dir` options.
"""
def __init__(self):

View File

@ -13,6 +13,12 @@
from docutils import nodes
from docutils.parsers import rst
from docutils.statemachine import ViewList
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain
from sphinx.domains import ObjType
from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode
from sphinx.util.nodes import nested_parse_with_titles
from oslo_config import cfg
@ -48,6 +54,14 @@ def _indent(text, n=2):
return '\n'.join(padding + l for l in text.splitlines())
def _make_anchor_target(group_name, option_name):
# We need to ensure this is unique across entire documentation
# http://www.sphinx-doc.org/en/stable/markup/inline.html#ref-role
target = '%s.%s' % (cfg._normalize_group_name(group_name),
option_name.lower())
return target
class ShowOptionsDirective(rst.Directive):
# option_spec = {}
@ -98,18 +112,14 @@ class ShowOptionsDirective(rst.Directive):
for group_name, opt_list in sorted(by_section.items()):
group_name = group_name or 'DEFAULT'
app.info('[oslo.config] %s %s' % (namespace, group_name))
_add(group_name)
_add('=' * len(group_name))
_add('.. oslo.config:group:: %s' % group_name)
_add('')
for opt in opt_list:
opt_type = self._TYPE_DESCRIPTIONS.get(type(opt),
'unknown type')
# We need to ensure this is unique across entire documentation
# http://www.sphinx-doc.org/en/stable/markup/inline.html#ref-role
_add('.. _opt-%s-%s:' % (group_name.lower(), opt.dest.lower()))
_add('')
_add('``%s``' % opt.dest)
_add('.. oslo.config:option:: %s' % opt.dest)
_add('')
_add_indented(':Type: %s' % opt_type)
for default in generator._format_defaults(opt):
@ -168,5 +178,137 @@ class ShowOptionsDirective(rst.Directive):
return six.text_type(choice)
class ConfigGroupXRefRole(XRefRole):
"Handles :oslo.config:group: roles pointing to configuration groups."
def __init__(self):
super(ConfigGroupXRefRole, self).__init__(
warn_dangling=True,
)
def process_link(self, env, refnode, has_explicit_title, title, target):
# The anchor for the group link is the group name.
return target, target
class ConfigOptXRefRole(XRefRole):
"Handles :oslo.config:option: roles pointing to configuration options."
def __init__(self):
super(ConfigOptXRefRole, self).__init__(
warn_dangling=True,
)
def process_link(self, env, refnode, has_explicit_title, title, target):
if not has_explicit_title:
title = target
if '.' in target:
group, opt_name = target.split('.')
else:
group = 'DEFAULT'
opt_name = target
anchor = _make_anchor_target(group, opt_name)
return title, anchor
class ConfigGroup(ObjectDescription):
"Description of a configuration group (.. group)."
def handle_signature(self, sig, signode):
"""Transform a group description into RST nodes."""
group_name = sig
signode += addnodes.desc_name(
group_name,
'Option Group: %s' % group_name,
)
signode['allnames'] = [group_name]
return group_name
def add_target_and_index(self, firstname, sig, signode):
cached_groups = self.env.domaindata['oslo.config']['groups']
# Store the current group for use later in option directives
self.env.temp_data['oslo.config:group'] = sig
# Store the location where this group is being defined
# for use when resolving cross-references later.
# FIXME: This should take the source namespace into account, too
cached_groups[sig] = self.env.docname
# Compute the normalized target and set the node to have that
# as an id
target_name = cfg._normalize_group_name(sig)
signode['ids'].append(target_name)
self.state.document.note_explicit_target(signode)
class ConfigOption(ObjectDescription):
"Description of a configuration option (.. option)."
def handle_signature(self, sig, signode):
"""Transform an option description into RST nodes."""
optname = sig
# Insert a node into the output showing the option name
signode += addnodes.desc_name(optname, optname)
signode['allnames'] = [optname]
return optname
def add_target_and_index(self, firstname, sig, signode):
cached_options = self.env.domaindata['oslo.config']['options']
# Look up the current group name from the processing context
currgroup = self.env.temp_data.get('oslo.config:group')
# Compute the normalized target name for the option and give
# that to the node as an id
target_name = _make_anchor_target(currgroup, sig)
signode['ids'].append(target_name)
self.state.document.note_explicit_target(signode)
# Store the location of the option definition for later use in
# resolving cross-references
# FIXME: This should take the source namespace into account, too
cached_options[target_name] = self.env.docname
class ConfigDomain(Domain):
"""oslo.config domain."""
name = 'oslo.config'
label = 'oslo.config'
object_types = {
'configoption': ObjType('configuration option', 'option'),
}
directives = {
'group': ConfigGroup,
'option': ConfigOption,
}
roles = {
'option': ConfigOptXRefRole(),
'group': ConfigGroupXRefRole(),
}
initial_data = {
'options': {},
'groups': {},
}
def resolve_xref(self, env, fromdocname, builder,
typ, target, node, contnode):
if typ == 'option':
group_name, option_name = target.split('.', 1)
return make_refnode(
builder,
fromdocname,
env.domaindata['oslo.config']['options'][target],
target,
contnode,
option_name,
)
if typ == 'group':
return make_refnode(
builder,
fromdocname,
env.domaindata['oslo.config']['groups'][target],
target,
contnode,
target,
)
return None
def setup(app):
app.add_directive('show-options', ShowOptionsDirective)
app.add_domain(ConfigDomain)