Loading PKG-INFO +23 −7 Original line number Diff line number Diff line Metadata-Version: 1.1 Name: sphinxcontrib-autoprogram Version: 0.1.2 Version: 0.1.4 Summary: Documenting CLI programs Home-page: https://bitbucket.org/birkenfeld/sphinx-contrib Home-page: https://github.com/sphinx-contrib/autoprogram Author: Hong Minhee Author-email: minhee@dahlia.kr Author-email: hong.minhee@gmail.com License: BSD Description: ``sphinxcontrib.autoprogram`` --- Documenting CLI programs ========================================================== Description-Content-Type: UNKNOWN Description: ``sphinxcontrib.autoprogram`` ============================= .. image:: https://badge.fury.io/py/sphinxcontrib-autoprogram.svg :target: https://pypi.org/project/sphinxcontrib-autoprogram/ :alt: Latest PyPI version .. image:: https://readthedocs.org/projects/sphinxcontrib-autoprogram/badge/ :target: https://sphinxcontrib-autoprogram.readthedocs.io/ :alt: Documentation Status .. image:: https://travis-ci.org/sphinx-contrib/autoprogram.svg?branch=master :alt: Build Status :target: https://travis-ci.org/sphinx-contrib/autoprogram This contrib extension, ``sphinxcontrib.autoprogram``, provides an automated way to document CLI programs. It scans ``arparser.ArgumentParser`` object, Loading @@ -22,10 +35,10 @@ Description: ``sphinxcontrib.autoprogram`` --- Documenting CLI programs You can find the documentation from the following URL: https://pythonhosted.org/sphinxcontrib-autoprogram/ https://sphinxcontrib-autoprogram.readthedocs.io/ Platform: any Classifier: Development Status :: 4 - Beta Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Loading @@ -35,6 +48,9 @@ Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: Stackless Loading README.rst +15 −3 Original line number Diff line number Diff line ``sphinxcontrib.autoprogram`` --- Documenting CLI programs ========================================================== ``sphinxcontrib.autoprogram`` ============================= .. image:: https://badge.fury.io/py/sphinxcontrib-autoprogram.svg :target: https://pypi.org/project/sphinxcontrib-autoprogram/ :alt: Latest PyPI version .. image:: https://readthedocs.org/projects/sphinxcontrib-autoprogram/badge/ :target: https://sphinxcontrib-autoprogram.readthedocs.io/ :alt: Documentation Status .. image:: https://travis-ci.org/sphinx-contrib/autoprogram.svg?branch=master :alt: Build Status :target: https://travis-ci.org/sphinx-contrib/autoprogram This contrib extension, ``sphinxcontrib.autoprogram``, provides an automated way to document CLI programs. It scans ``arparser.ArgumentParser`` object, Loading @@ -14,4 +26,4 @@ Install using ``pip``: You can find the documentation from the following URL: https://pythonhosted.org/sphinxcontrib-autoprogram/ https://sphinxcontrib-autoprogram.readthedocs.io/ setup.cfg +0 −1 Original line number Diff line number Diff line [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [bdist_wheel] universal = 1 Loading setup.py +11 −7 Original line number Diff line number Diff line Loading @@ -7,11 +7,11 @@ from setuptools import setup, find_packages # Do not change the variable name. It's parsed by doc/conf.py script. version = '0.1.2' version = '0.1.4' requires = ['Sphinx >= 1.2', 'six'] if sys.version_info < (2, 7): if 'bdist_wheel' not in sys.argv and sys.version_info < (2, 7): requires.append('argparse') Loading @@ -23,15 +23,15 @@ def readme(): setup( name='sphinxcontrib-autoprogram', version=version, url='https://bitbucket.org/birkenfeld/sphinx-contrib', url='https://github.com/sphinx-contrib/autoprogram', license='BSD', author='Hong Minhee', author_email='minhee' '@' 'dahlia.kr', author_email='\x68\x6f\x6e\x67.minhee' '@' '\x67\x6d\x61\x69\x6c.com', description='Documenting CLI programs', long_description=readme(), zip_safe=False, classifiers=[ 'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Web Environment', 'Intended Audience :: Developers', Loading @@ -41,6 +41,9 @@ setup( 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: Stackless', Loading @@ -52,6 +55,7 @@ setup( packages=find_packages(), namespace_packages=['sphinxcontrib'], include_package_data=True, test_suite='sphinxcontrib.autoprogram.suite', install_requires=requires install_requires=requires, extras_requires={":python_version=='2.6'": ['argparse']}, test_suite='sphinxcontrib.autoprogram.suite' ) sphinxcontrib/autoprogram.py +168 −45 Original line number Diff line number Diff line Loading @@ -11,27 +11,40 @@ # pylint: disable=protected-access,missing-docstring import argparse import collections try: import builtins except ImportError: import __builtin__ as builtins import functools import os import re import sys import unittest import six from docutils import nodes from docutils.parsers.rst import Directive from docutils.parsers.rst.directives import unchanged from docutils.statemachine import ViewList from sphinx.util.compat import Directive from sphinx.util.nodes import nested_parse_with_titles from docutils.statemachine import StringList, ViewList from six import exec_ from six.moves import builtins, reduce from sphinx.domains import std from sphinx.util.nodes import nested_parse_with_titles __all__ = ('BOOLEAN_OPTIONS', 'AutoprogramDirective', 'ScannerTestCase', __all__ = ('AutoprogramDirective', 'AutoprogramDirectiveTestCase', 'ScannerTestCase', 'import_object', 'scan_programs', 'setup', 'suite') def scan_programs(parser, command=[]): def get_subparser_action(parser): neg1_action = parser._actions[-1] if isinstance(neg1_action, argparse._SubParsersAction): return neg1_action for a in parser._actions: if isinstance(a, argparse._SubParsersAction): return a def scan_programs(parser, command=[], maxdepth=0, depth=0): if maxdepth and depth >= maxdepth: return options = [] for arg in parser._actions: if not (arg.option_strings or Loading @@ -39,14 +52,26 @@ def scan_programs(parser, command=[]): name = (arg.metavar or arg.dest).lower() desc = (arg.help or '') % {'default': arg.default} options.append(([name], desc)) for arg in parser._actions: if arg.option_strings: if arg.option_strings and arg.help is not argparse.SUPPRESS: if isinstance(arg, (argparse._StoreAction, argparse._AppendAction)): if arg.choices is None: metavar = (arg.metavar or arg.dest).lower() names = ['{0} <{1}>'.format(option_string, metavar) for option_string in arg.option_strings] metavar = arg.metavar or arg.dest if isinstance(metavar, tuple): names = [ '{0} <{1}>'.format( option_string, '> <'.join(metavar).lower() ) for option_string in arg.option_strings ] else: names = [ '{0} <{1}>'.format(option_string, metavar.lower()) for option_string in arg.option_strings ] else: choices = '{0}'.format(','.join(arg.choices)) names = ['{0} {{{1}}}'.format(option_string, choices) Loading @@ -55,15 +80,26 @@ def scan_programs(parser, command=[]): names = list(arg.option_strings) desc = (arg.help or '') % {'default': arg.default} options.append((names, desc)) yield command, options, parser.description, parser.epilog or '' yield command, options, parser if parser._subparsers: choices = parser._subparsers._actions[-1].choices.items() choices = () subp_action = get_subparser_action(parser) if subp_action: choices = subp_action.choices.items() if not (hasattr(collections, 'OrderedDict') and isinstance(choices, collections.OrderedDict)): choices = sorted(choices, key=lambda pair: pair[0]) for cmd, sub in choices: if isinstance(sub, argparse.ArgumentParser): for program in scan_programs(sub, command + [cmd]): for program in scan_programs( sub, command + [cmd], maxdepth, depth + 1 ): yield program Loading @@ -87,7 +123,7 @@ def import_object(import_name): with open(f[0]) as fobj: codestring = fobj.read() foo = imp.new_module("foo") six.exec_(codestring, foo.__dict__) exec_(codestring, foo.__dict__) sys.modules["foo"] = foo mod = __import__("foo") Loading @@ -95,8 +131,7 @@ def import_object(import_name): else: raise ImportError("No module named {}".format(module_name)) reduce_ = getattr(functools, 'reduce', None) or reduce mod = reduce_(getattr, module_name.split('.')[1:], mod) mod = reduce(getattr, module_name.split('.')[1:], mod) globals_ = builtins if not isinstance(globals_, dict): globals_ = globals_.__dict__ Loading @@ -107,32 +142,94 @@ class AutoprogramDirective(Directive): has_content = False required_arguments = 1 option_spec = {'prog': unchanged} option_spec = { 'prog': unchanged, 'maxdepth': unchanged, 'start_command': unchanged, 'strip_usage': unchanged, 'no_usage_codeblock': unchanged, } def make_rst(self): import_name, = self.arguments parser = import_object(import_name or '__undefined__') parser.prog = self.options.get('prog', parser.prog) for commands, options, desc, epilog in scan_programs(parser): command = ' '.join(commands) title = '{0} {1}'.format(parser.prog, command).rstrip() prog = self.options.get('prog') if prog: original_prog = parser.prog parser.prog = prog start_command = self.options.get('start_command', '').split(' ') strip_usage = 'strip_usage' in self.options usage_codeblock = 'no_usage_codeblock' not in self.options if start_command[0] == '': start_command.pop(0) if start_command: def get_start_cmd_parser(p): looking_for = start_command.pop(0) action = get_subparser_action(p) if not action: raise ValueError('No actions for command ' + looking_for) subp = action.choices[looking_for] if start_command: return get_start_cmd_parser(subp) return subp parser = get_start_cmd_parser(parser) if prog and parser.prog.startswith(original_prog): parser.prog = parser.prog.replace(original_prog, prog, 1) for commands, options, cmd_parser in scan_programs( parser, maxdepth=int(self.options.get('maxdepth', 0)) ): if prog and cmd_parser.prog.startswith(original_prog): cmd_parser.prog = cmd_parser.prog.replace( original_prog, prog, 1) title = cmd_parser.prog.rstrip() usage = cmd_parser.format_usage() if strip_usage: to_strip = title.rsplit(' ', 1)[0] len_to_strip = len(to_strip) - 4 usage_lines = usage.splitlines() usage = os.linesep.join([ usage_lines[0].replace(to_strip, '...'), ] + [ l[len_to_strip:] for l in usage_lines[1:] ]) yield '' yield '.. program:: ' + title yield '' yield title yield ('!' if commands else '?') * len(title) yield '' yield desc or '' for line in (cmd_parser.description or '').splitlines(): yield line yield '' if usage_codeblock: yield '.. code-block:: console' yield '' yield parser.format_usage() for usage_line in usage.splitlines(): yield ' ' + usage_line else: yield usage yield '' for option_strings, help_ in options: yield '.. option:: {0}'.format(', '.join(option_strings)) yield '' yield ' ' + help_.replace('\n', ' \n') yield '' yield '' for line in epilog.splitlines(): for line in (cmd_parser.epilog or '').splitlines(): yield line or '' def run(self): Loading Loading @@ -175,14 +272,17 @@ class ScannerTestCase(unittest.TestCase): parser.add_argument('--sum', dest='accumulate', action='store_const', const=sum, default=max, help='sum the integers (default: find the max)') parser.add_argument('--key-value', metavar=('KEY', 'VALUE'), nargs=2) parser.add_argument('--max', help=argparse.SUPPRESS) # must be opt-out programs = scan_programs(parser) programs = list(programs) self.assertEqual(1, len(programs)) parser_info, = programs program, options, desc, _ = parser_info program, options, cmd_parser = parser_info self.assertEqual([], program) self.assertEqual('Process some integers.', desc) self.assertEqual(4, len(options)) self.assertEqual('Process some integers.', cmd_parser.description) self.assertEqual(5, len(options)) self.assertEqual( (['n'], 'an integer for the accumulator'), options[0] Loading @@ -200,6 +300,10 @@ class ScannerTestCase(unittest.TestCase): (['--sum'], 'sum the integers (default: find the max)'), options[3] ) self.assertEqual( (['--key-value <key> <value>', ], ''), options[4] ) def test_subcommands(self): parser = argparse.ArgumentParser(description='Process some integers.') Loading @@ -217,9 +321,9 @@ class ScannerTestCase(unittest.TestCase): programs = list(programs) self.assertEqual(3, len(programs)) # main program, options, desc, _ = programs[0] program, options, cmd_parser = programs[0] self.assertEqual([], program) self.assertEqual('Process some integers.', desc) self.assertEqual('Process some integers.', cmd_parser.description) self.assertEqual(1, len(options)) self.assertEqual( (['-h', '--help'], Loading @@ -227,9 +331,9 @@ class ScannerTestCase(unittest.TestCase): options[0] ) # max program, options, desc, _ = programs[1] program, options, cmd_parser = programs[1] self.assertEqual(['max'], program) self.assertEqual('Find the max.', desc) self.assertEqual('Find the max.', cmd_parser.description) self.assertEqual(2, len(options)) self.assertEqual((['n'], 'An integer for the accumulator.'), options[0]) Loading @@ -239,9 +343,9 @@ class ScannerTestCase(unittest.TestCase): options[1] ) # sum program, options, desc, _ = programs[2] program, options, cmd_parser = programs[2] self.assertEqual(['sum'], program) self.assertEqual('Sum the integers.', desc) self.assertEqual('Sum the integers.', cmd_parser.description) self.assertEqual(2, len(options)) self.assertEqual((['n'], 'An integer for the accumulator.'), options[0]) Loading @@ -249,7 +353,7 @@ class ScannerTestCase(unittest.TestCase): def test_choices(self): parser = argparse.ArgumentParser() parser.add_argument("--awesomeness", choices=["meh", "awesome"]) program, options, desc, _ = list(scan_programs(parser))[0] program, options, cmd_parser = list(scan_programs(parser))[0] log_option = options[1] self.assertEqual((["--awesomeness {meh,awesome}"], ''), log_option) Loading @@ -262,8 +366,29 @@ class ScannerTestCase(unittest.TestCase): programs = list(programs) self.assertEqual(1, len(programs)) parser_data, = programs program, options, desc, epilog = parser_data self.assertEqual('The integers will be processed.', epilog) program, options, cmd_parser = parser_data self.assertEqual('The integers will be processed.', cmd_parser.epilog) class AutoprogramDirectiveTestCase(unittest.TestCase): def setUp(self): self.untouched_sys_path = sys.path[:] sample_prog_path = os.path.join(os.path.dirname(__file__), '..', 'doc') sys.path.insert(0, sample_prog_path) self.directive = AutoprogramDirective( 'autoprogram', ['cli:parser'], {'prog': 'cli.py'}, StringList([], items=[]), 1, 0, '.. autoprogram:: cli:parser\n :prog: cli.py\n', None, None ) def tearDown(self): sys.path[:] = self.untouched_sys_path def test_make_rst(self): """Alt least it shouldn't raise errors during making RST string.""" list(self.directive.make_rst()) class UtilTestCase(unittest.TestCase): Loading @@ -284,7 +409,5 @@ class UtilTestCase(unittest.TestCase): suite = unittest.TestSuite() suite.addTests( unittest.defaultTestLoader.loadTestsFromTestCase(ScannerTestCase) ) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(UtilTestCase)) for test_case in ScannerTestCase, AutoprogramDirectiveTestCase, UtilTestCase: suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(test_case)) Loading
PKG-INFO +23 −7 Original line number Diff line number Diff line Metadata-Version: 1.1 Name: sphinxcontrib-autoprogram Version: 0.1.2 Version: 0.1.4 Summary: Documenting CLI programs Home-page: https://bitbucket.org/birkenfeld/sphinx-contrib Home-page: https://github.com/sphinx-contrib/autoprogram Author: Hong Minhee Author-email: minhee@dahlia.kr Author-email: hong.minhee@gmail.com License: BSD Description: ``sphinxcontrib.autoprogram`` --- Documenting CLI programs ========================================================== Description-Content-Type: UNKNOWN Description: ``sphinxcontrib.autoprogram`` ============================= .. image:: https://badge.fury.io/py/sphinxcontrib-autoprogram.svg :target: https://pypi.org/project/sphinxcontrib-autoprogram/ :alt: Latest PyPI version .. image:: https://readthedocs.org/projects/sphinxcontrib-autoprogram/badge/ :target: https://sphinxcontrib-autoprogram.readthedocs.io/ :alt: Documentation Status .. image:: https://travis-ci.org/sphinx-contrib/autoprogram.svg?branch=master :alt: Build Status :target: https://travis-ci.org/sphinx-contrib/autoprogram This contrib extension, ``sphinxcontrib.autoprogram``, provides an automated way to document CLI programs. It scans ``arparser.ArgumentParser`` object, Loading @@ -22,10 +35,10 @@ Description: ``sphinxcontrib.autoprogram`` --- Documenting CLI programs You can find the documentation from the following URL: https://pythonhosted.org/sphinxcontrib-autoprogram/ https://sphinxcontrib-autoprogram.readthedocs.io/ Platform: any Classifier: Development Status :: 4 - Beta Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Loading @@ -35,6 +48,9 @@ Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: Stackless Loading
README.rst +15 −3 Original line number Diff line number Diff line ``sphinxcontrib.autoprogram`` --- Documenting CLI programs ========================================================== ``sphinxcontrib.autoprogram`` ============================= .. image:: https://badge.fury.io/py/sphinxcontrib-autoprogram.svg :target: https://pypi.org/project/sphinxcontrib-autoprogram/ :alt: Latest PyPI version .. image:: https://readthedocs.org/projects/sphinxcontrib-autoprogram/badge/ :target: https://sphinxcontrib-autoprogram.readthedocs.io/ :alt: Documentation Status .. image:: https://travis-ci.org/sphinx-contrib/autoprogram.svg?branch=master :alt: Build Status :target: https://travis-ci.org/sphinx-contrib/autoprogram This contrib extension, ``sphinxcontrib.autoprogram``, provides an automated way to document CLI programs. It scans ``arparser.ArgumentParser`` object, Loading @@ -14,4 +26,4 @@ Install using ``pip``: You can find the documentation from the following URL: https://pythonhosted.org/sphinxcontrib-autoprogram/ https://sphinxcontrib-autoprogram.readthedocs.io/
setup.cfg +0 −1 Original line number Diff line number Diff line [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [bdist_wheel] universal = 1 Loading
setup.py +11 −7 Original line number Diff line number Diff line Loading @@ -7,11 +7,11 @@ from setuptools import setup, find_packages # Do not change the variable name. It's parsed by doc/conf.py script. version = '0.1.2' version = '0.1.4' requires = ['Sphinx >= 1.2', 'six'] if sys.version_info < (2, 7): if 'bdist_wheel' not in sys.argv and sys.version_info < (2, 7): requires.append('argparse') Loading @@ -23,15 +23,15 @@ def readme(): setup( name='sphinxcontrib-autoprogram', version=version, url='https://bitbucket.org/birkenfeld/sphinx-contrib', url='https://github.com/sphinx-contrib/autoprogram', license='BSD', author='Hong Minhee', author_email='minhee' '@' 'dahlia.kr', author_email='\x68\x6f\x6e\x67.minhee' '@' '\x67\x6d\x61\x69\x6c.com', description='Documenting CLI programs', long_description=readme(), zip_safe=False, classifiers=[ 'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Web Environment', 'Intended Audience :: Developers', Loading @@ -41,6 +41,9 @@ setup( 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: Stackless', Loading @@ -52,6 +55,7 @@ setup( packages=find_packages(), namespace_packages=['sphinxcontrib'], include_package_data=True, test_suite='sphinxcontrib.autoprogram.suite', install_requires=requires install_requires=requires, extras_requires={":python_version=='2.6'": ['argparse']}, test_suite='sphinxcontrib.autoprogram.suite' )
sphinxcontrib/autoprogram.py +168 −45 Original line number Diff line number Diff line Loading @@ -11,27 +11,40 @@ # pylint: disable=protected-access,missing-docstring import argparse import collections try: import builtins except ImportError: import __builtin__ as builtins import functools import os import re import sys import unittest import six from docutils import nodes from docutils.parsers.rst import Directive from docutils.parsers.rst.directives import unchanged from docutils.statemachine import ViewList from sphinx.util.compat import Directive from sphinx.util.nodes import nested_parse_with_titles from docutils.statemachine import StringList, ViewList from six import exec_ from six.moves import builtins, reduce from sphinx.domains import std from sphinx.util.nodes import nested_parse_with_titles __all__ = ('BOOLEAN_OPTIONS', 'AutoprogramDirective', 'ScannerTestCase', __all__ = ('AutoprogramDirective', 'AutoprogramDirectiveTestCase', 'ScannerTestCase', 'import_object', 'scan_programs', 'setup', 'suite') def scan_programs(parser, command=[]): def get_subparser_action(parser): neg1_action = parser._actions[-1] if isinstance(neg1_action, argparse._SubParsersAction): return neg1_action for a in parser._actions: if isinstance(a, argparse._SubParsersAction): return a def scan_programs(parser, command=[], maxdepth=0, depth=0): if maxdepth and depth >= maxdepth: return options = [] for arg in parser._actions: if not (arg.option_strings or Loading @@ -39,14 +52,26 @@ def scan_programs(parser, command=[]): name = (arg.metavar or arg.dest).lower() desc = (arg.help or '') % {'default': arg.default} options.append(([name], desc)) for arg in parser._actions: if arg.option_strings: if arg.option_strings and arg.help is not argparse.SUPPRESS: if isinstance(arg, (argparse._StoreAction, argparse._AppendAction)): if arg.choices is None: metavar = (arg.metavar or arg.dest).lower() names = ['{0} <{1}>'.format(option_string, metavar) for option_string in arg.option_strings] metavar = arg.metavar or arg.dest if isinstance(metavar, tuple): names = [ '{0} <{1}>'.format( option_string, '> <'.join(metavar).lower() ) for option_string in arg.option_strings ] else: names = [ '{0} <{1}>'.format(option_string, metavar.lower()) for option_string in arg.option_strings ] else: choices = '{0}'.format(','.join(arg.choices)) names = ['{0} {{{1}}}'.format(option_string, choices) Loading @@ -55,15 +80,26 @@ def scan_programs(parser, command=[]): names = list(arg.option_strings) desc = (arg.help or '') % {'default': arg.default} options.append((names, desc)) yield command, options, parser.description, parser.epilog or '' yield command, options, parser if parser._subparsers: choices = parser._subparsers._actions[-1].choices.items() choices = () subp_action = get_subparser_action(parser) if subp_action: choices = subp_action.choices.items() if not (hasattr(collections, 'OrderedDict') and isinstance(choices, collections.OrderedDict)): choices = sorted(choices, key=lambda pair: pair[0]) for cmd, sub in choices: if isinstance(sub, argparse.ArgumentParser): for program in scan_programs(sub, command + [cmd]): for program in scan_programs( sub, command + [cmd], maxdepth, depth + 1 ): yield program Loading @@ -87,7 +123,7 @@ def import_object(import_name): with open(f[0]) as fobj: codestring = fobj.read() foo = imp.new_module("foo") six.exec_(codestring, foo.__dict__) exec_(codestring, foo.__dict__) sys.modules["foo"] = foo mod = __import__("foo") Loading @@ -95,8 +131,7 @@ def import_object(import_name): else: raise ImportError("No module named {}".format(module_name)) reduce_ = getattr(functools, 'reduce', None) or reduce mod = reduce_(getattr, module_name.split('.')[1:], mod) mod = reduce(getattr, module_name.split('.')[1:], mod) globals_ = builtins if not isinstance(globals_, dict): globals_ = globals_.__dict__ Loading @@ -107,32 +142,94 @@ class AutoprogramDirective(Directive): has_content = False required_arguments = 1 option_spec = {'prog': unchanged} option_spec = { 'prog': unchanged, 'maxdepth': unchanged, 'start_command': unchanged, 'strip_usage': unchanged, 'no_usage_codeblock': unchanged, } def make_rst(self): import_name, = self.arguments parser = import_object(import_name or '__undefined__') parser.prog = self.options.get('prog', parser.prog) for commands, options, desc, epilog in scan_programs(parser): command = ' '.join(commands) title = '{0} {1}'.format(parser.prog, command).rstrip() prog = self.options.get('prog') if prog: original_prog = parser.prog parser.prog = prog start_command = self.options.get('start_command', '').split(' ') strip_usage = 'strip_usage' in self.options usage_codeblock = 'no_usage_codeblock' not in self.options if start_command[0] == '': start_command.pop(0) if start_command: def get_start_cmd_parser(p): looking_for = start_command.pop(0) action = get_subparser_action(p) if not action: raise ValueError('No actions for command ' + looking_for) subp = action.choices[looking_for] if start_command: return get_start_cmd_parser(subp) return subp parser = get_start_cmd_parser(parser) if prog and parser.prog.startswith(original_prog): parser.prog = parser.prog.replace(original_prog, prog, 1) for commands, options, cmd_parser in scan_programs( parser, maxdepth=int(self.options.get('maxdepth', 0)) ): if prog and cmd_parser.prog.startswith(original_prog): cmd_parser.prog = cmd_parser.prog.replace( original_prog, prog, 1) title = cmd_parser.prog.rstrip() usage = cmd_parser.format_usage() if strip_usage: to_strip = title.rsplit(' ', 1)[0] len_to_strip = len(to_strip) - 4 usage_lines = usage.splitlines() usage = os.linesep.join([ usage_lines[0].replace(to_strip, '...'), ] + [ l[len_to_strip:] for l in usage_lines[1:] ]) yield '' yield '.. program:: ' + title yield '' yield title yield ('!' if commands else '?') * len(title) yield '' yield desc or '' for line in (cmd_parser.description or '').splitlines(): yield line yield '' if usage_codeblock: yield '.. code-block:: console' yield '' yield parser.format_usage() for usage_line in usage.splitlines(): yield ' ' + usage_line else: yield usage yield '' for option_strings, help_ in options: yield '.. option:: {0}'.format(', '.join(option_strings)) yield '' yield ' ' + help_.replace('\n', ' \n') yield '' yield '' for line in epilog.splitlines(): for line in (cmd_parser.epilog or '').splitlines(): yield line or '' def run(self): Loading Loading @@ -175,14 +272,17 @@ class ScannerTestCase(unittest.TestCase): parser.add_argument('--sum', dest='accumulate', action='store_const', const=sum, default=max, help='sum the integers (default: find the max)') parser.add_argument('--key-value', metavar=('KEY', 'VALUE'), nargs=2) parser.add_argument('--max', help=argparse.SUPPRESS) # must be opt-out programs = scan_programs(parser) programs = list(programs) self.assertEqual(1, len(programs)) parser_info, = programs program, options, desc, _ = parser_info program, options, cmd_parser = parser_info self.assertEqual([], program) self.assertEqual('Process some integers.', desc) self.assertEqual(4, len(options)) self.assertEqual('Process some integers.', cmd_parser.description) self.assertEqual(5, len(options)) self.assertEqual( (['n'], 'an integer for the accumulator'), options[0] Loading @@ -200,6 +300,10 @@ class ScannerTestCase(unittest.TestCase): (['--sum'], 'sum the integers (default: find the max)'), options[3] ) self.assertEqual( (['--key-value <key> <value>', ], ''), options[4] ) def test_subcommands(self): parser = argparse.ArgumentParser(description='Process some integers.') Loading @@ -217,9 +321,9 @@ class ScannerTestCase(unittest.TestCase): programs = list(programs) self.assertEqual(3, len(programs)) # main program, options, desc, _ = programs[0] program, options, cmd_parser = programs[0] self.assertEqual([], program) self.assertEqual('Process some integers.', desc) self.assertEqual('Process some integers.', cmd_parser.description) self.assertEqual(1, len(options)) self.assertEqual( (['-h', '--help'], Loading @@ -227,9 +331,9 @@ class ScannerTestCase(unittest.TestCase): options[0] ) # max program, options, desc, _ = programs[1] program, options, cmd_parser = programs[1] self.assertEqual(['max'], program) self.assertEqual('Find the max.', desc) self.assertEqual('Find the max.', cmd_parser.description) self.assertEqual(2, len(options)) self.assertEqual((['n'], 'An integer for the accumulator.'), options[0]) Loading @@ -239,9 +343,9 @@ class ScannerTestCase(unittest.TestCase): options[1] ) # sum program, options, desc, _ = programs[2] program, options, cmd_parser = programs[2] self.assertEqual(['sum'], program) self.assertEqual('Sum the integers.', desc) self.assertEqual('Sum the integers.', cmd_parser.description) self.assertEqual(2, len(options)) self.assertEqual((['n'], 'An integer for the accumulator.'), options[0]) Loading @@ -249,7 +353,7 @@ class ScannerTestCase(unittest.TestCase): def test_choices(self): parser = argparse.ArgumentParser() parser.add_argument("--awesomeness", choices=["meh", "awesome"]) program, options, desc, _ = list(scan_programs(parser))[0] program, options, cmd_parser = list(scan_programs(parser))[0] log_option = options[1] self.assertEqual((["--awesomeness {meh,awesome}"], ''), log_option) Loading @@ -262,8 +366,29 @@ class ScannerTestCase(unittest.TestCase): programs = list(programs) self.assertEqual(1, len(programs)) parser_data, = programs program, options, desc, epilog = parser_data self.assertEqual('The integers will be processed.', epilog) program, options, cmd_parser = parser_data self.assertEqual('The integers will be processed.', cmd_parser.epilog) class AutoprogramDirectiveTestCase(unittest.TestCase): def setUp(self): self.untouched_sys_path = sys.path[:] sample_prog_path = os.path.join(os.path.dirname(__file__), '..', 'doc') sys.path.insert(0, sample_prog_path) self.directive = AutoprogramDirective( 'autoprogram', ['cli:parser'], {'prog': 'cli.py'}, StringList([], items=[]), 1, 0, '.. autoprogram:: cli:parser\n :prog: cli.py\n', None, None ) def tearDown(self): sys.path[:] = self.untouched_sys_path def test_make_rst(self): """Alt least it shouldn't raise errors during making RST string.""" list(self.directive.make_rst()) class UtilTestCase(unittest.TestCase): Loading @@ -284,7 +409,5 @@ class UtilTestCase(unittest.TestCase): suite = unittest.TestSuite() suite.addTests( unittest.defaultTestLoader.loadTestsFromTestCase(ScannerTestCase) ) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(UtilTestCase)) for test_case in ScannerTestCase, AutoprogramDirectiveTestCase, UtilTestCase: suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(test_case))