WIP! Rework autohelp to use oslo.config
Make use of the smarts built into oslo.config by hacking the autohelp. You can run this like so: cd openstack/nova tox -e venv python ../openstack-doc-tools/autogenerate_config_docs/autohelp2.py Assuming a directory called 'openstack' with the nova and openstack-doc-tools projects inside. Change-Id: Ib06043a17b1b741fbd944142e06d9f46c44fbf00 Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
parent
e136fd3a56
commit
ed33598a53
|
@ -0,0 +1,241 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Generate oslo.config help.
|
||||
|
||||
Hacked version of the 'oslo.config.sphinxext' module that outputs rST files
|
||||
rather than generating the rST dynamically at runtime. This means we don't need
|
||||
to keep the source/environment around at docs build time, but introduces a
|
||||
manual step to updating docs.
|
||||
|
||||
This will do the job until we move the config guides back to the project teams.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_config import generator
|
||||
import oslo_i18n
|
||||
import six
|
||||
|
||||
|
||||
_TYPE_DESCRIPTIONS = {
|
||||
cfg.StrOpt: 'string',
|
||||
cfg.BoolOpt: 'boolean',
|
||||
cfg.IntOpt: 'integer',
|
||||
cfg.FloatOpt: 'floating point',
|
||||
cfg.ListOpt: 'list',
|
||||
cfg.DictOpt: 'dict',
|
||||
cfg.MultiStrOpt: 'multi-valued',
|
||||
cfg.IPOpt: 'ip address',
|
||||
cfg.PortOpt: 'port number',
|
||||
cfg.HostnameOpt: 'hostname',
|
||||
cfg.URIOpt: 'URI',
|
||||
cfg.HostAddressOpt: 'host address',
|
||||
cfg._ConfigFileOpt: 'list of filenames',
|
||||
cfg._ConfigDirOpt: 'list of directory names',
|
||||
}
|
||||
|
||||
|
||||
def _list_table(headers, data, title='', columns=None):
|
||||
"""Build a list-table directive.
|
||||
|
||||
:param add: Function to add one row to output.
|
||||
:param headers: List of header values.
|
||||
:param data: Iterable of row data, yielding lists or tuples with rows.
|
||||
"""
|
||||
yield '.. list-table:: %s' % title
|
||||
yield ' :header-rows: 1'
|
||||
if columns:
|
||||
yield ' :widths: %s' % (','.join(str(c) for c in columns))
|
||||
yield ''
|
||||
yield ' - * %s' % headers[0]
|
||||
for h in headers[1:]:
|
||||
yield ' * %s' % h
|
||||
for row in data:
|
||||
yield ' - * %s' % row[0]
|
||||
for r in row[1:]:
|
||||
yield ' * %s' % r
|
||||
yield ''
|
||||
|
||||
|
||||
def _indent(text, n=2):
|
||||
padding = ' ' * n
|
||||
return '\n'.join(padding + l for l in text.splitlines())
|
||||
|
||||
|
||||
def _get_choice_text(choice):
|
||||
if choice is None:
|
||||
return '<None>'
|
||||
elif choice == '':
|
||||
return "''"
|
||||
return six.text_type(choice)
|
||||
|
||||
|
||||
def _format_group(namespace, group_name, group_obj, opt_list):
|
||||
group_name = group_name or 'DEFAULT'
|
||||
|
||||
yield '.. oslo.config:group:: %s' % group_name
|
||||
if namespace:
|
||||
yield ' :namespace: %s' % namespace
|
||||
yield ''
|
||||
|
||||
if group_obj and group_obj.help:
|
||||
yield _indent(group_obj.help.rstrip())
|
||||
yield ''
|
||||
|
||||
for opt in opt_list:
|
||||
opt_type = _TYPE_DESCRIPTIONS.get(type(opt),
|
||||
'unknown type')
|
||||
yield '.. oslo.config:option:: %s' % opt.dest
|
||||
yield ''
|
||||
yield _indent(':Type: %s' % opt_type)
|
||||
for default in generator._format_defaults(opt):
|
||||
if default:
|
||||
default = '``' + default + '``'
|
||||
yield _indent(':Default: %s' % default)
|
||||
if getattr(opt.type, 'min', None) is not None:
|
||||
yield _indent(':Minimum Value: %s' % opt.type.min)
|
||||
if getattr(opt.type, 'max', None) is not None:
|
||||
yield _indent(':Maximum Value: %s' % opt.type.max)
|
||||
if getattr(opt.type, 'choices', None):
|
||||
choices_text = ', '.join([_get_choice_text(choice)
|
||||
for choice in opt.type.choices])
|
||||
yield _indent(':Valid Values: %s' % choices_text)
|
||||
try:
|
||||
if opt.mutable:
|
||||
yield _indent(
|
||||
':Mutable: This option can be changed without restarting.',
|
||||
)
|
||||
except AttributeError as err:
|
||||
# NOTE(dhellmann): keystoneauth defines its own Opt class,
|
||||
# and neutron (at least) returns instances of those
|
||||
# classes instead of oslo_config Opt instances. The new
|
||||
# mutable attribute is the first property where the API
|
||||
# isn't supported in the external class, so we can use
|
||||
# this failure to emit a warning. See
|
||||
# https://bugs.launchpad.net/keystoneauth/+bug/1548433 for
|
||||
# more details.
|
||||
import warnings
|
||||
if not isinstance(cfg.Opt, opt):
|
||||
warnings.warn(
|
||||
'Incompatible option class for %s (%r): %s' %
|
||||
(opt.dest, opt.__class__, err),
|
||||
)
|
||||
else:
|
||||
warnings.warn('Failed to fully format sample for %s: %s' %
|
||||
(opt.dest, err))
|
||||
if opt.advanced:
|
||||
yield _indent(
|
||||
':Advanced Option: intended for advanced users and not used',)
|
||||
yield _indent(
|
||||
':by the majority of users, and might have a significant',)
|
||||
yield _indent(
|
||||
':effect on stability and/or performance.',)
|
||||
yield ''
|
||||
|
||||
try:
|
||||
help_text = opt.help % {'default': 'the value above'}
|
||||
except (TypeError, KeyError, ValueError):
|
||||
# There is no mention of the default in the help string,
|
||||
# the string had some unknown key, or the string contained
|
||||
# invalid formatting characters
|
||||
help_text = opt.help
|
||||
if help_text:
|
||||
yield _indent(help_text)
|
||||
yield ''
|
||||
|
||||
if opt.deprecated_opts:
|
||||
for line in _list_table(
|
||||
['Group', 'Name'],
|
||||
((d.group or group_name,
|
||||
d.name or opt.dest or 'UNSET')
|
||||
for d in opt.deprecated_opts),
|
||||
title='Deprecated Variations'):
|
||||
yield _indent(line)
|
||||
if opt.deprecated_for_removal:
|
||||
yield _indent('.. warning::')
|
||||
if opt.deprecated_since:
|
||||
yield _indent(' This option is deprecated for removal '
|
||||
'since %s.' % opt.deprecated_since)
|
||||
else:
|
||||
yield _indent(' This option is deprecated for removal.')
|
||||
yield _indent(' Its value may be silently ignored ')
|
||||
yield _indent(' in the future.')
|
||||
yield ''
|
||||
if opt.deprecated_reason:
|
||||
yield _indent(' :Reason: ' + opt.deprecated_reason)
|
||||
yield ''
|
||||
|
||||
yield ''
|
||||
|
||||
|
||||
def _format_option_help(namespaces, group):
|
||||
"""Generate a series of lines of restructuredtext.
|
||||
|
||||
Format the option help as restructuredtext and return it as a list
|
||||
of lines.
|
||||
"""
|
||||
opts = generator._list_opts(namespaces)
|
||||
|
||||
# Merge the options from different namespaces that belong to
|
||||
# the same group together and format them without the
|
||||
# namespace.
|
||||
by_section = {}
|
||||
group_objs = {}
|
||||
for ignore, opt_list in opts:
|
||||
for group, group_opts in opt_list:
|
||||
if isinstance(group, cfg.OptGroup):
|
||||
group_name = group.name
|
||||
else:
|
||||
group_name = group
|
||||
group = None
|
||||
|
||||
# if we specified a certain group, ignore everything else
|
||||
if group and group_name != group:
|
||||
continue
|
||||
|
||||
group_objs.setdefault(group_name, group)
|
||||
by_section.setdefault(group_name, []).extend(group_opts)
|
||||
|
||||
# this will only a single iteration long if we specified a group above
|
||||
for group_name, group_opts in sorted(by_section.items()):
|
||||
lines = _format_group(
|
||||
namespace=None,
|
||||
group_name=group_name,
|
||||
group_obj=group_objs.get(group_name),
|
||||
opt_list=group_opts,
|
||||
)
|
||||
for line in lines:
|
||||
yield line
|
||||
|
||||
|
||||
def main():
|
||||
# TODO(stephenfin): Make things configurable, yo
|
||||
source_path = '/home/sfinucan/Development/openstack/nova'
|
||||
config_file = None # we should support this later (see
|
||||
# 'nova/etc/nova.conf.ini')
|
||||
namespaces = ['nova.conf']
|
||||
project = 'nova'
|
||||
group = 'DEFAULT'
|
||||
|
||||
output_path = '/tmp/table-%s-%s.rst' % (project, group)
|
||||
|
||||
# TODO(stephenfin): do we care about Unicode? Use io.open if so
|
||||
with open(output_path, 'w') as output_file:
|
||||
for line in _format_option_help(namespaces, group):
|
||||
# each "line" may have a newline, or it may not
|
||||
lines = line.splitlines()
|
||||
for line in lines:
|
||||
output_file.write(line.rstrip() + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main() # TODO(stephenfin): Parameters, man
|
Loading…
Reference in New Issue