summaryrefslogtreecommitdiff
path: root/doc
diff options
context:
space:
mode:
authorVitaly Gridnev <vgridnev@mirantis.com>2016-06-07 18:31:08 +0300
committerVitaly Gridnev <vgridnev@mirantis.com>2016-06-16 14:22:04 +0000
commitfb5557abb464b554bd16d13fa151085e27f23b13 (patch)
treecee5cec21e454d37e875229cca08e780b85e522f /doc
parent7f40491aa207395fe12604931433ecdca8a21124 (diff)
downloadpython-saharaclient-fb5557abb464b554bd16d13fa151085e27f23b13.tar.gz
avoid additional requirement for building docs
DocImpact Change-Id: I2d4dda2ee6fdfcf82702f4400a2513e7aa83957d closes-bug: 1551292
Diffstat (limited to 'doc')
-rw-r--r--doc/ext/cli.py2
-rw-r--r--doc/ext/ext.py386
-rw-r--r--doc/ext/parser.py138
3 files changed, 525 insertions, 1 deletions
diff --git a/doc/ext/cli.py b/doc/ext/cli.py
index c9d860f..feb28d7 100644
--- a/doc/ext/cli.py
+++ b/doc/ext/cli.py
@@ -19,7 +19,7 @@ import os
import sys
from docutils import nodes
-from sphinxarg import ext
+from . import ext
def _get_command(classes):
diff --git a/doc/ext/ext.py b/doc/ext/ext.py
new file mode 100644
index 0000000..9b07ab1
--- /dev/null
+++ b/doc/ext/ext.py
@@ -0,0 +1,386 @@
+# Copyright (c) 2013 Alex Rudakov
+#
+# 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.
+
+from argparse import ArgumentParser
+import os
+
+from docutils import nodes
+from docutils.statemachine import StringList
+from docutils.parsers.rst.directives import flag, unchanged
+from sphinx.util.compat import Directive
+from sphinx.util.nodes import nested_parse_with_titles
+
+from .parser import parse_parser, parser_navigate
+
+
+def map_nested_definitions(nested_content):
+ if nested_content is None:
+ raise Exception('Nested content should be iterable, not null')
+ # build definition dictionary
+ definitions = {}
+ for item in nested_content:
+ if not isinstance(item, nodes.definition_list):
+ continue
+ for subitem in item:
+ if not isinstance(subitem, nodes.definition_list_item):
+ continue
+ if not len(subitem.children) > 0:
+ continue
+ classifier = '@after'
+ idx = subitem.first_child_matching_class(nodes.classifier)
+ if idx is not None:
+ ci = subitem[idx]
+ if len(ci.children) > 0:
+ classifier = ci.children[0].astext()
+ if classifier is not None and classifier not in (
+ '@replace', '@before', '@after'):
+ raise Exception('Unknown classifier: %s' % classifier)
+ idx = subitem.first_child_matching_class(nodes.term)
+ if idx is not None:
+ ch = subitem[idx]
+ if len(ch.children) > 0:
+ term = ch.children[0].astext()
+ idx = subitem.first_child_matching_class(nodes.definition)
+ if idx is not None:
+ def_node = subitem[idx]
+ def_node.attributes['classifier'] = classifier
+ definitions[term] = def_node
+ return definitions
+
+
+def print_arg_list(data, nested_content):
+ definitions = map_nested_definitions(nested_content)
+ items = []
+ if 'args' in data:
+ for arg in data['args']:
+ my_def = [nodes.paragraph(text=arg['help'])] if arg['help'] else []
+ name = arg['name']
+ my_def = apply_definition(definitions, my_def, name)
+ if len(my_def) == 0:
+ my_def.append(nodes.paragraph(text='Undocumented'))
+ if 'choices' in arg:
+ my_def.append(nodes.paragraph(
+ text=('Possible choices: %s' % ', '.join([str(c) for c in arg['choices']]))))
+ items.append(
+ nodes.option_list_item(
+ '', nodes.option_group('', nodes.option_string(text=name)),
+ nodes.description('', *my_def)))
+ return nodes.option_list('', *items) if items else None
+
+
+def print_opt_list(data, nested_content):
+ definitions = map_nested_definitions(nested_content)
+ items = []
+ if 'options' in data:
+ for opt in data['options']:
+ names = []
+ my_def = [nodes.paragraph(text=opt['help'])] if opt['help'] else []
+ for name in opt['name']:
+ option_declaration = [nodes.option_string(text=name)]
+ if opt['default'] is not None \
+ and opt['default'] != '==SUPPRESS==':
+ option_declaration += nodes.option_argument(
+ '', text='=' + str(opt['default']))
+ names.append(nodes.option('', *option_declaration))
+ my_def = apply_definition(definitions, my_def, name)
+ if len(my_def) == 0:
+ my_def.append(nodes.paragraph(text='Undocumented'))
+ if 'choices' in opt:
+ my_def.append(nodes.paragraph(
+ text=('Possible choices: %s' % ', '.join([str(c) for c in opt['choices']]))))
+ items.append(
+ nodes.option_list_item(
+ '', nodes.option_group('', *names),
+ nodes.description('', *my_def)))
+ return nodes.option_list('', *items) if items else None
+
+
+def print_command_args_and_opts(arg_list, opt_list, sub_list=None):
+ items = []
+ if arg_list:
+ items.append(nodes.definition_list_item(
+ '', nodes.term(text='Positional arguments:'),
+ nodes.definition('', arg_list)))
+ if opt_list:
+ items.append(nodes.definition_list_item(
+ '', nodes.term(text='Options:'),
+ nodes.definition('', opt_list)))
+ if sub_list and len(sub_list):
+ items.append(nodes.definition_list_item(
+ '', nodes.term(text='Sub-commands:'),
+ nodes.definition('', sub_list)))
+ return nodes.definition_list('', *items)
+
+
+def apply_definition(definitions, my_def, name):
+ if name in definitions:
+ definition = definitions[name]
+ classifier = definition['classifier']
+ if classifier == '@replace':
+ return definition.children
+ if classifier == '@after':
+ return my_def + definition.children
+ if classifier == '@before':
+ return definition.children + my_def
+ raise Exception('Unknown classifier: %s' % classifier)
+ return my_def
+
+
+def print_subcommand_list(data, nested_content):
+ definitions = map_nested_definitions(nested_content)
+ items = []
+ if 'children' in data:
+ for child in data['children']:
+ my_def = [nodes.paragraph(
+ text=child['help'])] if child['help'] else []
+ name = child['name']
+ my_def = apply_definition(definitions, my_def, name)
+ if len(my_def) == 0:
+ my_def.append(nodes.paragraph(text='Undocumented'))
+ if 'description' in child:
+ my_def.append(nodes.paragraph(text=child['description']))
+ my_def.append(nodes.literal_block(text=child['usage']))
+ my_def.append(print_command_args_and_opts(
+ print_arg_list(child, nested_content),
+ print_opt_list(child, nested_content),
+ print_subcommand_list(child, nested_content)
+ ))
+ items.append(
+ nodes.definition_list_item(
+ '',
+ nodes.term('', '', nodes.strong(text=name)),
+ nodes.definition('', *my_def)
+ )
+ )
+ return nodes.definition_list('', *items)
+
+
+class ArgParseDirective(Directive):
+ has_content = True
+ option_spec = dict(module=unchanged, func=unchanged, ref=unchanged,
+ prog=unchanged, path=unchanged, nodefault=flag,
+ manpage=unchanged, nosubcommands=unchanged, passparser=flag)
+
+ def _construct_manpage_specific_structure(self, parser_info):
+ """
+ Construct a typical man page consisting of the following elements:
+ NAME (automatically generated, out of our control)
+ SYNOPSIS
+ DESCRIPTION
+ OPTIONS
+ FILES
+ SEE ALSO
+ BUGS
+ """
+ # SYNOPSIS section
+ synopsis_section = nodes.section(
+ '',
+ nodes.title(text='Synopsis'),
+ nodes.literal_block(text=parser_info["bare_usage"]),
+ ids=['synopsis-section'])
+ # DESCRIPTION section
+ description_section = nodes.section(
+ '',
+ nodes.title(text='Description'),
+ nodes.paragraph(text=parser_info.get(
+ 'description', parser_info.get(
+ 'help', "undocumented").capitalize())),
+ ids=['description-section'])
+ nested_parse_with_titles(
+ self.state, self.content, description_section)
+ if parser_info.get('epilog'):
+ # TODO: do whatever sphinx does to understand ReST inside
+ # docstrings magically imported from other places. The nested
+ # parse method invoked above seem to be able to do this but
+ # I haven't found a way to do it for arbitrary text
+ description_section += nodes.paragraph(
+ text=parser_info['epilog'])
+ # OPTIONS section
+ options_section = nodes.section(
+ '',
+ nodes.title(text='Options'),
+ ids=['options-section'])
+ if 'args' in parser_info:
+ options_section += nodes.paragraph()
+ options_section += nodes.subtitle(text='Positional arguments:')
+ options_section += self._format_positional_arguments(parser_info)
+ if 'options' in parser_info:
+ options_section += nodes.paragraph()
+ options_section += nodes.subtitle(text='Optional arguments:')
+ options_section += self._format_optional_arguments(parser_info)
+ items = [
+ # NOTE: we cannot generate NAME ourselves. It is generated by
+ # docutils.writers.manpage
+ synopsis_section,
+ description_section,
+ # TODO: files
+ # TODO: see also
+ # TODO: bugs
+ ]
+ if len(options_section.children) > 1:
+ items.append(options_section)
+ if 'nosubcommands' not in self.options:
+ # SUBCOMMANDS section (non-standard)
+ subcommands_section = nodes.section(
+ '',
+ nodes.title(text='Sub-Commands'),
+ ids=['subcommands-section'])
+ if 'children' in parser_info:
+ subcommands_section += self._format_subcommands(parser_info)
+ if len(subcommands_section) > 1:
+ items.append(subcommands_section)
+ if os.getenv("INCLUDE_DEBUG_SECTION"):
+ import json
+ # DEBUG section (non-standard)
+ debug_section = nodes.section(
+ '',
+ nodes.title(text="Argparse + Sphinx Debugging"),
+ nodes.literal_block(text=json.dumps(parser_info, indent=' ')),
+ ids=['debug-section'])
+ items.append(debug_section)
+ return items
+
+ def _format_positional_arguments(self, parser_info):
+ assert 'args' in parser_info
+ items = []
+ for arg in parser_info['args']:
+ arg_items = []
+ if arg['help']:
+ arg_items.append(nodes.paragraph(text=arg['help']))
+ else:
+ arg_items.append(nodes.paragraph(text='Undocumented'))
+ if 'choices' in arg:
+ arg_items.append(
+ nodes.paragraph(
+ text='Possible choices: ' + ', '.join(arg['choices'])))
+ items.append(
+ nodes.option_list_item(
+ '',
+ nodes.option_group(
+ '', nodes.option(
+ '', nodes.option_string(text=arg['metavar'])
+ )
+ ),
+ nodes.description('', *arg_items)))
+ return nodes.option_list('', *items)
+
+ def _format_optional_arguments(self, parser_info):
+ assert 'options' in parser_info
+ items = []
+ for opt in parser_info['options']:
+ names = []
+ opt_items = []
+ for name in opt['name']:
+ option_declaration = [nodes.option_string(text=name)]
+ if opt['default'] is not None \
+ and opt['default'] != '==SUPPRESS==':
+ option_declaration += nodes.option_argument(
+ '', text='=' + str(opt['default']))
+ names.append(nodes.option('', *option_declaration))
+ if opt['help']:
+ opt_items.append(nodes.paragraph(text=opt['help']))
+ else:
+ opt_items.append(nodes.paragraph(text='Undocumented'))
+ if 'choices' in opt:
+ opt_items.append(
+ nodes.paragraph(
+ text='Possible choices: ' + ', '.join(opt['choices'])))
+ items.append(
+ nodes.option_list_item(
+ '', nodes.option_group('', *names),
+ nodes.description('', *opt_items)))
+ return nodes.option_list('', *items)
+
+ def _format_subcommands(self, parser_info):
+ assert 'children' in parser_info
+ items = []
+ for subcmd in parser_info['children']:
+ subcmd_items = []
+ if subcmd['help']:
+ subcmd_items.append(nodes.paragraph(text=subcmd['help']))
+ else:
+ subcmd_items.append(nodes.paragraph(text='Undocumented'))
+ items.append(
+ nodes.definition_list_item(
+ '',
+ nodes.term('', '', nodes.strong(
+ text=subcmd['bare_usage'])),
+ nodes.definition('', *subcmd_items)))
+ return nodes.definition_list('', *items)
+
+ def _nested_parse_paragraph(self, text):
+ content = nodes.paragraph()
+ self.state.nested_parse(StringList(text.split("\n")), 0, content)
+ return content
+
+ def run(self):
+ if 'module' in self.options and 'func' in self.options:
+ module_name = self.options['module']
+ attr_name = self.options['func']
+ elif 'ref' in self.options:
+ _parts = self.options['ref'].split('.')
+ module_name = '.'.join(_parts[0:-1])
+ attr_name = _parts[-1]
+ else:
+ raise self.error(
+ ':module: and :func: should be specified, or :ref:')
+ mod = __import__(module_name, globals(), locals(), [attr_name])
+ if not hasattr(mod, attr_name):
+ raise self.error((
+ 'Module "%s" has no attribute "%s"\n'
+ 'Incorrect argparse :module: or :func: values?'
+ ) % (module_name, attr_name))
+ func = getattr(mod, attr_name)
+ if isinstance(func, ArgumentParser):
+ parser = func
+ elif 'passparser' in self.options:
+ parser = ArgumentParser()
+ func(parser)
+ else:
+ parser = func()
+ if 'path' not in self.options:
+ self.options['path'] = ''
+ path = str(self.options['path'])
+ if 'prog' in self.options:
+ parser.prog = self.options['prog']
+ result = parse_parser(
+ parser, skip_default_values='nodefault' in self.options)
+ result = parser_navigate(result, path)
+ if 'manpage' in self.options:
+ return self._construct_manpage_specific_structure(result)
+ nested_content = nodes.paragraph()
+ self.state.nested_parse(
+ self.content, self.content_offset, nested_content)
+ nested_content = nested_content.children
+ items = []
+ # add common content between
+ for item in nested_content:
+ if not isinstance(item, nodes.definition_list):
+ items.append(item)
+ if 'description' in result:
+ items.append(self._nested_parse_paragraph(result['description']))
+ items.append(nodes.literal_block(text=result['usage']))
+ items.append(print_command_args_and_opts(
+ print_arg_list(result, nested_content),
+ print_opt_list(result, nested_content),
+ print_subcommand_list(result, nested_content)
+ ))
+ if 'epilog' in result:
+ items.append(self._nested_parse_paragraph(result['epilog']))
+ return items
+
+
+def setup(app):
+ app.add_directive('argparse', ArgParseDirective) \ No newline at end of file
diff --git a/doc/ext/parser.py b/doc/ext/parser.py
new file mode 100644
index 0000000..fbb8feb
--- /dev/null
+++ b/doc/ext/parser.py
@@ -0,0 +1,138 @@
+# Copyright (c) 2013 Alex Rudakov
+#
+# 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.
+
+from argparse import _HelpAction, _SubParsersAction
+import re
+
+
+class NavigationException(Exception):
+ pass
+
+
+def parser_navigate(parser_result, path, current_path=None):
+ if isinstance(path, str):
+ if path == '':
+ return parser_result
+ path = re.split('\s+', path)
+ current_path = current_path or []
+ if len(path) == 0:
+ return parser_result
+ if 'children' not in parser_result:
+ raise NavigationException(
+ 'Current parser have no children elements. (path: %s)' %
+ ' '.join(current_path))
+ next_hop = path.pop(0)
+ for child in parser_result['children']:
+ if child['name'] == next_hop:
+ current_path.append(next_hop)
+ return parser_navigate(child, path, current_path)
+ raise NavigationException(
+ 'Current parser have no children element with name: %s (path: %s)' % (
+ next_hop, ' '.join(current_path)))
+
+
+def _try_add_parser_attribute(data, parser, attribname):
+ attribval = getattr(parser, attribname, None)
+ if attribval is None:
+ return
+ if not isinstance(attribval, str):
+ return
+ if len(attribval) > 0:
+ data[attribname] = attribval
+
+
+def _format_usage_without_prefix(parser):
+ """
+ Use private argparse APIs to get the usage string without
+ the 'usage: ' prefix.
+ """
+ fmt = parser._get_formatter()
+ fmt.add_usage(parser.usage, parser._actions,
+ parser._mutually_exclusive_groups, prefix='')
+ return fmt.format_help().strip()
+
+
+def parse_parser(parser, data=None, **kwargs):
+ if data is None:
+ data = {
+ 'name': '',
+ 'usage': parser.format_usage().strip(),
+ 'bare_usage': _format_usage_without_prefix(parser),
+ 'prog': parser.prog,
+ }
+ _try_add_parser_attribute(data, parser, 'description')
+ _try_add_parser_attribute(data, parser, 'epilog')
+ for action in parser._get_positional_actions():
+ if isinstance(action, _HelpAction):
+ continue
+ if isinstance(action, _SubParsersAction):
+ helps = {}
+ for item in action._choices_actions:
+ helps[item.dest] = item.help
+
+ # commands which share an existing parser are an alias,
+ # don't duplicate docs
+ subsection_alias = {}
+ subsection_alias_names = set()
+ for name, subaction in action._name_parser_map.items():
+ if subaction not in subsection_alias:
+ subsection_alias[subaction] = []
+ else:
+ subsection_alias[subaction].append(name)
+ subsection_alias_names.add(name)
+
+ for name, subaction in action._name_parser_map.items():
+ if name in subsection_alias_names:
+ continue
+ subalias = subsection_alias[subaction]
+ subaction.prog = '%s %s' % (parser.prog, name)
+ subdata = {
+ 'name': name if not subalias else
+ '%s (%s)' % (name, ', '.join(subalias)),
+ 'help': helps.get(name, ''),
+ 'usage': subaction.format_usage().strip(),
+ 'bare_usage': _format_usage_without_prefix(subaction),
+ }
+ parse_parser(subaction, subdata, **kwargs)
+ data.setdefault('children', []).append(subdata)
+ continue
+ if 'args' not in data:
+ data['args'] = []
+ arg = {
+ 'name': action.dest,
+ 'help': action.help or '',
+ 'metavar': action.metavar
+ }
+ if action.choices:
+ arg['choices'] = action.choices
+ data['args'].append(arg)
+ show_defaults = (
+ ('skip_default_values' not in kwargs)
+ or (kwargs['skip_default_values'] is False))
+ for action in parser._get_optional_actions():
+ if isinstance(action, _HelpAction):
+ continue
+ if 'options' not in data:
+ data['options'] = []
+ option = {
+ 'name': action.option_strings,
+ 'default': action.default if show_defaults else '==SUPPRESS==',
+ 'help': action.help or ''
+ }
+ if action.choices:
+ option['choices'] = action.choices
+ if "==SUPPRESS==" not in option['help']:
+ data['options'].append(option)
+ return data