From 0ae3a2a163f151334a743e181c524250469655fd Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 16 Jul 2014 17:19:26 +0200 Subject: [PATCH 01/11] replace 'not foo in' with 'foo not in' More of the same changes, just with more recent version of pep8+flake8 Signed-off-by: Zygmunt Krynicki --- sphinxarg/ext.py | 8 ++++---- sphinxarg/parser.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index d876ad3..7776288 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -25,7 +25,7 @@ def map_nested_definitions(nested_content): if len(ci.children) > 0: classifier = ci.children[0].astext() - if classifier is not None and not classifier in ( + if classifier is not None and classifier not in ( '@replace', '@before', '@after'): raise Exception('Unknown classifier: %s' % classifier) @@ -180,12 +180,12 @@ def run(self): parser = func else: parser = func() - if not 'path' in self.options: + if 'path' not in self.options: self.options['path'] = '' path = str(self.options['path']) parser.prog = self.options['prog'] - result = parse_parser(parser, - skip_default_values='nodefault' in self.options) + result = parse_parser( + parser, skip_default_values='nodefault' in self.options) result = parser_navigate(result, path) nested_content = nodes.paragraph() self.state.nested_parse( diff --git a/sphinxarg/parser.py b/sphinxarg/parser.py index 38db559..45c85d2 100644 --- a/sphinxarg/parser.py +++ b/sphinxarg/parser.py @@ -14,7 +14,7 @@ def parser_navigate(parser_result, path, current_path=None): current_path = current_path or [] if len(path) == 0: return parser_result - if not 'children' in parser_result: + if 'children' not in parser_result: raise NavigationException( 'Current parser have no children elements. (path: %s)' % ' '.join(current_path)) @@ -58,11 +58,11 @@ def parse_parser(parser, data=None, **kwargs): 'usage': subaction.format_usage().strip() } parse_parser(subaction, subdata, **kwargs) - if not 'children' in data: + if 'children' not in data: data['children'] = [] data['children'].append(subdata) continue - if not 'args' in data: + if 'args' not in data: data['args'] = [] arg = { 'name': action.dest, @@ -72,12 +72,12 @@ def parse_parser(parser, data=None, **kwargs): arg['choices'] = action.choices data['args'].append(arg) show_defaults = ( - (not 'skip_default_values' in kwargs) + ('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 not 'options' in data: + if 'options' not in data: data['options'] = [] option = { 'name': action.option_strings, From 6ceea3623c3ef3c4604814494c4c24bf77898353 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:08:47 +0200 Subject: [PATCH 02/11] collect metavar name for all arguments The metavar is the name of the "value" available for arguments and for some options. Typical example would be --output FILE. FILE is the metavar string. Signed-off-by: Zygmunt Krynicki --- sphinxarg/parser.py | 3 ++- test/test_parser.py | 36 ++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/sphinxarg/parser.py b/sphinxarg/parser.py index 45c85d2..78e9581 100644 --- a/sphinxarg/parser.py +++ b/sphinxarg/parser.py @@ -66,7 +66,8 @@ def parse_parser(parser, data=None, **kwargs): data['args'] = [] arg = { 'name': action.dest, - 'help': action.help or '' + 'help': action.help or '', + 'metavar': action.metavar } if action.choices: arg['choices'] = action.choices diff --git a/test/test_parser.py b/test/test_parser.py index 9df6e73..58d4a54 100755 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -49,7 +49,8 @@ def test_parse_arg_choices(): { 'name': 'move', 'help': '', - 'choices': ['rock', 'paper', 'scissors'] + 'choices': ['rock', 'paper', 'scissors'], + 'metavar': None } ] @@ -96,10 +97,12 @@ def test_parse_positional(): assert data['args'] == [ { 'name': 'foo', - 'help': 'foo help' + 'help': 'foo help', + 'metavar': None }, { 'name': 'bar', - 'help': '' + 'help': '', + 'metavar': None }, ] @@ -118,10 +121,12 @@ def test_parse_description(): assert data['args'] == [ { 'name': 'foo', - 'help': 'foo help' + 'help': 'foo help', + 'metavar': None }, { 'name': 'bar', - 'help': '' + 'help': '', + 'metavar': None }, ] @@ -142,10 +147,12 @@ def test_parse_nested(): assert data['args'] == [ { 'name': 'foo', - 'help': 'foo help' + 'help': 'foo help', + 'metavar': None }, { 'name': 'bar', - 'help': '' + 'help': '', + 'metavar': None }, ] @@ -157,7 +164,8 @@ def test_parse_nested(): 'args': [ { 'name': 'ref', - 'help': 'foo1 help' + 'help': 'foo1 help', + 'metavar': None }, ], 'options': [ @@ -193,10 +201,12 @@ def test_parse_nested_traversal(): assert data3['args'] == [ { 'name': 'foo', - 'help': 'foo help' + 'help': 'foo help', + 'metavar': None }, { 'name': 'bar', - 'help': '' + 'help': '', + 'metavar': None }, ] @@ -209,11 +219,13 @@ def test_parse_nested_traversal(): 'args': [ { 'name': 'foo', - 'help': 'foo help' + 'help': 'foo help', + 'metavar': None }, { 'name': 'bar', - 'help': '' + 'help': '', + 'metavar': None }, ], } From 8f3af10f0571c91949645b61cf737571a9fb6395 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:09:36 +0200 Subject: [PATCH 03/11] do not collect options that contain the string ==SUPPRESS== ==SUPPRESS== is the magic value used by argparse to hide command line options. We should follow the same behavior. Signed-off-by: Zygmunt Krynicki --- sphinxarg/parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinxarg/parser.py b/sphinxarg/parser.py index 78e9581..f2f8b30 100644 --- a/sphinxarg/parser.py +++ b/sphinxarg/parser.py @@ -87,5 +87,6 @@ def parse_parser(parser, data=None, **kwargs): } if action.choices: option['choices'] = action.choices - data['options'].append(option) + if "==SUPPRESS==" not in option['help']: + data['options'].append(option) return data From 652e9c137820095fdde25141e35ecff5ac835782 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:10:25 +0200 Subject: [PATCH 04/11] collect bare_usage, a vesion of usage without the i18n "usage: " prefix Signed-off-by: Zygmunt Krynicki --- sphinxarg/parser.py | 20 ++++++++++++++++++-- test/test_parser.py | 2 ++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/sphinxarg/parser.py b/sphinxarg/parser.py index f2f8b30..b713641 100644 --- a/sphinxarg/parser.py +++ b/sphinxarg/parser.py @@ -38,9 +38,24 @@ def _try_add_parser_attribute(data, parser, attribname): 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()} + data = { + 'name': '', + 'usage': parser.format_usage().strip(), + 'bare_usage': _format_usage_without_prefix(parser), + } _try_add_parser_attribute(data, parser, 'description') _try_add_parser_attribute(data, parser, 'epilog') for action in parser._get_positional_actions(): @@ -55,7 +70,8 @@ def parse_parser(parser, data=None, **kwargs): subdata = { 'name': name, 'help': helps[name] if name in helps else '', - 'usage': subaction.format_usage().strip() + 'usage': subaction.format_usage().strip(), + 'bare_usage': _format_usage_without_prefix(subaction), } parse_parser(subaction, subdata, **kwargs) if 'children' not in data: diff --git a/test/test_parser.py b/test/test_parser.py index 58d4a54..859e3de 100755 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -161,6 +161,7 @@ def test_parse_nested(): 'name': 'install', 'help': 'install help', 'usage': 'usage: py.test install [-h] [--upgrade] ref', + 'bare_usage': 'py.test install [-h] [--upgrade] ref', 'args': [ { 'name': 'ref', @@ -216,6 +217,7 @@ def test_parse_nested_traversal(): 'name': 'level3', 'help': '', 'usage': 'usage: py.test level1 level2 level3 [-h] foo bar', + 'bare_usage': 'py.test level1 level2 level3 [-h] foo bar', 'args': [ { 'name': 'foo', From 4f3cb33ccf61504b5c9aa87c53e03de1a4a0777d Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:10:42 +0200 Subject: [PATCH 05/11] collect parser 'prog' (program name) for completeness Signed-off-by: Zygmunt Krynicki --- sphinxarg/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinxarg/parser.py b/sphinxarg/parser.py index b713641..1c75af8 100644 --- a/sphinxarg/parser.py +++ b/sphinxarg/parser.py @@ -55,6 +55,7 @@ def parse_parser(parser, data=None, **kwargs): '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') From ed145be680aaa002ac940c9f157a084c2e65347b Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:12:15 +0200 Subject: [PATCH 06/11] do not override parser.prog unconditionally The :prog: argument to the .. argparse:: directive allows programmers to override the program name but prog is typically correctly inferred by argparse, and in some situations, it is already provided to argparse explicitly. To keep code DRY, we should not override it without having an explicit value Signed-off-by: Zygmunt Krynicki --- sphinxarg/ext.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index 7776288..9610cb7 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -183,7 +183,8 @@ def run(self): if 'path' not in self.options: self.options['path'] = '' path = str(self.options['path']) - parser.prog = self.options['prog'] + 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) From cada261b1ab65f39e0ae144d827187a902cd8607 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:12:56 +0200 Subject: [PATCH 07/11] minor formatting changes for closing parentheses Signed-off-by: Zygmunt Krynicki --- sphinxarg/ext.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index 9610cb7..7c3450d 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -58,9 +58,7 @@ def print_arg_list(data, nested_content): items.append( nodes.option_list_item( '', nodes.option_group('', nodes.option_string(text=name)), - nodes.description('', *my_def) - ) - ) + nodes.description('', *my_def))) return nodes.option_list('', *items) if items else None @@ -87,9 +85,7 @@ def print_opt_list(data, nested_content): items.append( nodes.option_list_item( '', nodes.option_group('', *names), - nodes.description('', *my_def) - ) - ) + nodes.description('', *my_def))) return nodes.option_list('', *items) if items else None @@ -98,18 +94,15 @@ def print_command_args_and_opts(arg_list, opt_list, sub_list=None): if arg_list: items.append(nodes.definition_list_item( '', nodes.term(text='Positional arguments:'), - nodes.definition('', arg_list) - )) + nodes.definition('', arg_list))) if opt_list: items.append(nodes.definition_list_item( '', nodes.term(text='Options:'), - nodes.definition('', opt_list) - )) + 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) - )) + nodes.definition('', sub_list))) return nodes.definition_list('', *items) From 29dfe3ce8b0265938eaf42ca1b1e0eacae9f87f2 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:13:11 +0200 Subject: [PATCH 08/11] minor formatting changes, remove some useless newlines Signed-off-by: Zygmunt Krynicki --- sphinxarg/ext.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index 7c3450d..529e1ae 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -24,11 +24,9 @@ def map_nested_definitions(nested_content): 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] From a0b073ad102441e88caf2a656e943be83ff95146 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:07:43 +0200 Subject: [PATCH 09/11] add special :manpage: mode This mode is changing the way the directive is impacting the resulting document. The layout changes to traditional manpage SYNOPSIS, DESCRIPTION, etc sections. Most of the content is expected to be provided either inline in the nested definitions (indented text below .. argparse::) and immediately after that (for sections other than DESCRIPTION). A number of existing sphinxarg features are not supported yet. Specifically none of the @before @after and @replace pseudo-decorators are handled. Some of the new private methods are re-implementations of similar methods from the generic code path but they could not be reused at this time. Signed-off-by: Zygmunt Krynicki --- sphinxarg/ext.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index 529e1ae..a1c748e 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -1,7 +1,11 @@ from argparse import ArgumentParser +import os + from docutils import nodes from sphinx.util.compat import Directive from docutils.parsers.rst.directives import flag, unchanged +from sphinx.util.nodes import nested_parse_with_titles + from sphinxarg.parser import parse_parser, parser_navigate @@ -147,7 +151,151 @@ def print_subcommand_list(data, nested_content): class ArgParseDirective(Directive): has_content = True option_spec = dict(module=unchanged, func=unchanged, ref=unchanged, - prog=unchanged, path=unchanged, nodefault=flag) + prog=unchanged, path=unchanged, nodefault=flag, + manpage=unchanged, nosubcommands=unchanged) + + 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) + if 'children' in parser_info: + subcommands_section += self._format_subcommands(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 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.description(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 run(self): if 'module' in self.options and 'func' in self.options: @@ -179,6 +327,8 @@ def run(self): 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) From 06b7dfa3fda1ede1196e85460caf449d80c96bcf Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:13:17 +0200 Subject: [PATCH 10/11] sort imports Signed-off-by: Zygmunt Krynicki --- sphinxarg/ext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index a1c748e..13829ef 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -2,8 +2,8 @@ import os from docutils import nodes -from sphinx.util.compat import Directive from docutils.parsers.rst.directives import flag, unchanged +from sphinx.util.compat import Directive from sphinx.util.nodes import nested_parse_with_titles from sphinxarg.parser import parse_parser, parser_navigate From 87d25f5dcbf65f1787a75be2782b75ffabdc6b07 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 21 Jul 2014 15:30:16 +0200 Subject: [PATCH 11/11] tox-test on python3.4 Signed-off-by: Zygmunt Krynicki --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 47d6e7c..b1ef914 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33 +envlist = py26,py27,py33,py34 [testenv] deps=pytest # install pytest in the venvs commands=py.test # or 'nosetests' or .