Skip to content
This repository has been archived by the owner on Mar 27, 2021. It is now read-only.

Commit

Permalink
Merge pull request #73 from dangle/primcomm
Browse files Browse the repository at this point in the history
Primcomm
  • Loading branch information
Za Wilgustus authored Aug 2, 2017
2 parents 8ac7342 + 5795bfa commit e15f691
Show file tree
Hide file tree
Showing 16 changed files with 529 additions and 146 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,5 @@ ENV/
.sublimelinterrc
*.sublime-project
*.sublime-workspace

.mypy_cache
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ rcli
*Rapidly create full-featured command line interfaces with help, subcommand
dispatch, and validation.*

**rcli** uses docopt_ to give you the control over your usage messages that you
``rcli`` uses docopt_ to give you the control over your usage messages that you
want, but adds functionality such as automatic subcommand dispatching, usage
string wrapping, internationalization, and parameter validation.

Expand Down Expand Up @@ -45,7 +45,7 @@ Upcoming Features
Basic Usage
-----------

To use **rcli**, add ``rcli`` to your ``setup_requires`` argument in your
To use ``rcli``, add ``rcli`` to your ``setup_requires`` argument in your
*setup.py* and set the ``autodetect_commands`` parameter to ``True``.

.. code-block:: python
Expand Down
65 changes: 16 additions & 49 deletions rcli/autodetect.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,23 @@
from __future__ import absolute_import
from __future__ import unicode_literals

from io import open # pylint: disable=redefined-builtin
import ast
import collections
import inspect
import json
import os.path
import re
import typing

import docopt
import setuptools
import six

from . import usage


_EntryPoint = collections.namedtuple( # All data representing an entry point.
'_EntryPoint', ('command', 'subcommand', 'callable'))


_USAGE_PATTERN = re.compile(r'^([^\n]*usage\:[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
re.IGNORECASE | re.MULTILINE)


def setup_keyword(dist, _, value):
# type: (setuptools.dist.Distribution, str, bool) -> None
"""Add autodetected commands as entry points.
Expand All @@ -47,9 +43,11 @@ def setup_keyword(dist, _, value):
if dist.entry_points is None:
dist.entry_points = {}
for command, subcommands in six.iteritems(_get_commands(dist)):
dist.entry_points.setdefault('console_scripts', []).append(
'{command} = rcli.dispatcher:main'.format(command=command)
)
entry_point = '{command} = rcli.dispatcher:main'.format(
command=command)
entry_points = dist.entry_points.setdefault('console_scripts', [])
if entry_point not in entry_points:
entry_points.append(entry_point)
dist.entry_points.setdefault('rcli', []).extend(subcommands)


Expand Down Expand Up @@ -186,8 +184,8 @@ def _get_module_commands(module):
if '__call__' not in methods:
return
docstring = ast.get_docstring(module)
for command, subcommand in _parse_commands(docstring):
yield _EntryPoint(command, subcommand, None)
for commands, _ in usage.parse_commands(docstring):
yield _EntryPoint(commands[0], next(iter(commands[1:]), None), None)


def _get_class_commands(module):
Expand All @@ -208,8 +206,9 @@ def _get_class_commands(module):
methods = (n.name for n in cls.body if isinstance(n, ast.FunctionDef))
if '__call__' in methods:
docstring = ast.get_docstring(cls)
for command, subcommand in _parse_commands(docstring):
yield _EntryPoint(command, subcommand, cls.name)
for commands, _ in usage.parse_commands(docstring):
yield _EntryPoint(commands[0], next(iter(commands[1:]), None),
cls.name)


def _get_function_commands(module):
Expand All @@ -228,38 +227,6 @@ def _get_function_commands(module):
nodes = (n for n in module.body if isinstance(n, ast.FunctionDef))
for func in nodes:
docstring = ast.get_docstring(func)
for command, subcommand in _parse_commands(docstring):
yield _EntryPoint(command, subcommand, func.name)


def _parse_commands(docstring):
# type: (str) -> typing.Generator[typing.Tuple[str, str], None, None]
"""Parse a docopt-style string for commands and subcommands.
Args:
docstring: A docopt-style string to parse. If the string is not a valid
docopt-style string, it will not yield and values.
Yields:
All tuples of commands and subcommands found in the docopt docstring.
"""
try:
docopt.docopt(docstring, argv=())
except (TypeError, docopt.DocoptLanguageError):
return
except docopt.DocoptExit:
pass
match = _USAGE_PATTERN.findall(docstring)[0]
usage = inspect.cleandoc(match.strip()[6:])
usage_sections = [s.strip() for s in usage.split('\n') if s[:1].isalpha()]
for section in usage_sections:
args = section.split()
command = args[0]
subcommand = None
if len(args) > 1 and not (args[1].startswith('<') or
args[1].startswith('-') or
args[1].startswith('[') or
args[1].startswith('(') or
args[1].isupper()):
subcommand = args[1]
yield command, subcommand
for commands, _ in usage.parse_commands(docstring):
yield _EntryPoint(commands[0], next(iter(commands[1:]), None),
func.name)
Empty file added rcli/backports/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions rcli/backports/get_terminal_size.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# pragma pylint: disable=unused-import
"""An automatic command that handles subcommand dispatch.
Functions:
main: The console script entry point set by autodetected CLI scripts.
"""

from __future__ import absolute_import


try:
from shutil import get_terminal_size
except ImportError:
from backports.shutil_get_terminal_size import ( # noqa: F401
get_terminal_size
)
4 changes: 3 additions & 1 deletion rcli/call.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def call(func, args):
try:
value = cast(hints[nk], v)
except TypeError as e:
_LOGGER.exception(e)
six.raise_from(exc.InvalidCliValueError(k, v), e)
if nk == argspec.varargs:
varargs = value
Expand Down Expand Up @@ -148,7 +149,8 @@ def _normalize(args):
"""
for k, v in six.iteritems(args):
nk = re.sub(r'\W|^(?=\d)', '_', k).strip('_').lower()
if keyword.iskeyword(nk):
do_not_shadow = dir(six.moves.builtins) # type: ignore
if keyword.iskeyword(nk) or nk in do_not_shadow:
nk += '_'
_LOGGER.debug('Normalized "%s" to "%s".', k, nk)
yield k, nk, v
76 changes: 5 additions & 71 deletions rcli/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from __future__ import absolute_import
from __future__ import unicode_literals

import inspect
import logging
import sys
import types # noqa: F401 pylint: disable=unused-import
Expand All @@ -22,31 +21,15 @@
exceptions as exc,
log,
call,
config
config,
usage
)
from .config import settings


_LOGGER = logging.getLogger(__name__)


_DEFAULT_DOC = """
Usage:
{command} [--help] [--version] [--log-level <level> | --debug | --verbose]
<command> [<args>...]
Options:
-h, --help Display this help message and exit.
-V, --version Display the version and exit.
-d, --debug Set the log level to DEBUG.
-v, --verbose Set the log level to INFO.
--log-level <level> Set the log level to one of DEBUG, INFO, WARN, or ERROR.
{{message}}
'{command} help -a' lists all available subcommands.
See '{command} help <command>' for more information on a specific command.
""".format(command=settings.command)


def main():
# type: () -> typing.Any
"""Parse the command line options and launch the requested command.
Expand All @@ -55,12 +38,7 @@ def main():
no subcommand is given, print the standard help message.
"""
colorama.init(wrap=six.PY3)
if None not in settings.subcommands:
msg = '\n{}\n'.format(settings.message) if settings.message else ''
doc = _DEFAULT_DOC.format(message=msg)
else:
doc = settings.subcommands[None].__doc__
doc = _get_usage(doc)
doc = usage.get_primary_command_usage()
allow_subcommands = '<command>' in doc
args = docopt(doc, version=settings.version,
options_first=allow_subcommands)
Expand All @@ -70,7 +48,7 @@ def main():
if (args.get('<command>') == 'help' and
None not in settings.subcommands):
subcommand = next(iter(args.get('<args>', default_args)), None)
return _help(subcommand)
return usage.get_help_usage(subcommand)
argv = [args.get('<command>')] + args.get('<args>', default_args)
return _run_command(argv)
except exc.InvalidCliValueError as e:
Expand Down Expand Up @@ -123,7 +101,7 @@ def _run_command(argv):
command_name, argv)
subcommand = _get_subcommand(command_name)
func = call.get_callable(subcommand)
doc = _get_usage(subcommand.__doc__)
doc = usage.format_usage(subcommand.__doc__)
args = _get_parsed_args(command_name, doc, argv)
return call.call(func, args) or 0

Expand Down Expand Up @@ -166,47 +144,3 @@ def _get_parsed_args(command_name, doc, argv):
if command_name == settings.command:
args[command_name] = True
return args


def _help(command):
# type: (str) -> None
"""Print out a help message and exit the program.
Args:
command: If a command value is supplied then print the help message for
the command module if available. If the command is '-a' or '--all',
then print the standard help message but with a full list of
available commands.
Raises:
ValueError: Raised if the help message is requested for an invalid
command or an unrecognized option is passed to help.
"""
if not command:
doc = _DEFAULT_DOC.format(message='')
elif command in ('-a', '--all'):
subcommands = [k for k in settings.subcommands if k is not None]
available_commands = subcommands + ['help']
command_doc = '\nAvailable commands:\n{}\n'.format(
'\n'.join(' {}'.format(c) for c in sorted(available_commands)))
doc = _DEFAULT_DOC.format(message=command_doc)
elif command.startswith('-'):
raise ValueError("Unrecognized option '{}'.".format(command))
else:
subcommand = _get_subcommand(command)
doc = _get_usage(subcommand.__doc__)
docopt(doc, argv=('--help',))


def _get_usage(doc):
# type: (str) -> str
"""Format the docstring for display to the user.
Args:
doc: The docstring to reformat for display.
Returns:
The docstring formatted to parse and display to the user. This includes
dedenting, rewrapping, and translating the docstring if necessary.
"""
return inspect.cleandoc(doc)
7 changes: 1 addition & 6 deletions rcli/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,7 @@
)
from tqdm import tqdm

try:
from shutil import get_terminal_size # type: ignore
except ImportError:
from backports.shutil_get_terminal_size import ( # type: ignore
get_terminal_size
)
from .backports.get_terminal_size import get_terminal_size


_LOGGER = logging.getLogger(__name__)
Expand Down
1 change: 1 addition & 0 deletions rcli/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from __future__ import print_function
from __future__ import unicode_literals

from io import open # pylint: disable=redefined-builtin
import datetime
import logging
import os.path
Expand Down
Loading

0 comments on commit e15f691

Please sign in to comment.