Commit f5e20274 authored by Andreas Tille's avatar Andreas Tille
Browse files

New upstream version 0.1.4

parent cd689573
Loading
Loading
Loading
Loading
+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,
@@ -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
@@ -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
+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,
@@ -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/
+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
+11 −7
Original line number Diff line number Diff line
@@ -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')


@@ -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',
@@ -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',
@@ -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'
)
+168 −45
Original line number Diff line number Diff line
@@ -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
@@ -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)
@@ -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


@@ -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")
@@ -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__
@@ -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):
@@ -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]
@@ -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.')
@@ -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'],
@@ -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])
@@ -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])
@@ -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)

@@ -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):
@@ -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