diff options
author | Vitaly Gridnev <vgridnev@mirantis.com> | 2016-06-07 18:31:08 +0300 |
---|---|---|
committer | Vitaly Gridnev <vgridnev@mirantis.com> | 2016-06-16 14:22:04 +0000 |
commit | fb5557abb464b554bd16d13fa151085e27f23b13 (patch) | |
tree | cee5cec21e454d37e875229cca08e780b85e522f /doc | |
parent | 7f40491aa207395fe12604931433ecdca8a21124 (diff) | |
download | python-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.py | 2 | ||||
-rw-r--r-- | doc/ext/ext.py | 386 | ||||
-rw-r--r-- | doc/ext/parser.py | 138 |
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 |