Source code for clize.runner

# clize -- A command-line argument parser for Python
# Copyright (C) 2011-2021 by Yann Kaiser and contributors. See AUTHORS and
# COPYING for details.

from __future__ import print_function

import sys
import os
from functools import partial, update_wrapper
import itertools
import shutil

from sigtools.modifiers import annotate, autokwoargs, kwoargs
from sigtools.specifiers import forwards_to_method, signature

from clize import util, errors, parser, parameters


class _BasicHelper(object):
    def __init__(self, description, usages):
        if description is not None:
            self.description = description
        if usages is not None:
            def _usages():
                return usages
            self.usages = _usages

class _CliWrapper(object):
    def __init__(self, obj, description, usages):
        update_wrapper(self, obj)
        if description is not None or usages is not None:
            obj.helper = _BasicHelper(description, usages)
        self.cli = obj

def cli_commands(obj, namef, clizer):
    cmds = util.OrderedDict()
    cmd_by_name = {}
    try:
        names = util.dict_from_names(obj).items()
    except AttributeError:
        raise ValueError("Cannot guess name for anonymous objects "
                         "(lists, dicts, etc)")
    for key, val in names:
        if not key:
            continue
        names = tuple(namef(name) for name in util.maybe_iter(key))
        cli = clizer.get_cli(val)
        cmds[names] = cli
        for name in names:
            cmd_by_name[name] = cli
    return cmds, cmd_by_name

[docs]class Clize(object): """Wraps a function into a CLI object that accepts command-line arguments and translates them to match the wrapped function's parameters.""" @forwards_to_method('__init__', 1) def __new__(cls, fn=None, **kwargs): if fn is None: return partial(cls, **kwargs) else: return super(Clize, cls).__new__(cls) def __init__(self, fn, owner=None, alt=(), extra=(), help_names=('help', 'h'), helper_class=None, hide_help=False): """ :param sequence alt: Alternate actions the CLI will handle. :param help_names: Names to use to trigger the help. :type help_names: sequence of strings :param helper_class: A callable to produce a helper object to be used when the help is triggered. If unset, uses `.ClizeHelp`. :type helper_class: a type like `.ClizeHelp` :param bool hide_help: Mark the parameters used to trigger the help as undocumented. """ update_wrapper(self, fn) self.func = fn self.owner = owner self.alt = util.maybe_iter(alt) self.extra = extra self.help_names = help_names self.help_aliases = [util.name_py2cli(s, kw=True) for s in help_names] self.helper_class = helper_class self.hide_help = hide_help def parameters(self): """Returns the parameters used to instantiate this class, minus the wrapped callable.""" return { 'owner': self.owner, 'alt': self.alt, 'help_names': self.help_names, 'helper_class': self.helper_class, 'hide_help': self.hide_help, } @classmethod def keep(cls, fn=None, **kwargs): """Instead of wrapping the decorated callable, sets its ``cli`` attribute to a `.Clize` instance. Useful if you need to use the decorator but must still be able to call the function regularily. """ if fn is None: return partial(cls.keep, **kwargs) else: fn.cli = cls(fn, **kwargs) return fn @classmethod @kwoargs(start='description') def as_is(cls, obj=None, description=None, usages=None): """Returns a CLI object which uses the given callable with no translation. The following parameters improve the decorated object's compatibility with Clize's help output: :param description: A description for the command. :param usages: A list of usages for the command. .. seealso:: :ref:`interop` """ if obj is None: return partial(cls.as_is, description=description, usages=usages) return _CliWrapper(obj, description, usages) @classmethod def get_cli(cls, obj, **kwargs): """Makes an attempt to discover a command-line interface for the given object. .. _cli-object: The process used is as follows: 1. If the object has a ``cli`` attribute, it is used with no further transformation. 2. If the object is callable, `.Clize` or whichever object this class method is used from is used to build a CLI. ``**kwargs`` are forwarded to its initializer. 3. If the object is iterable, `.SubcommandDispatcher` is used on the object, and its `cli <.SubcommandDispatcher.cli>` method is used. Most notably, `clize.run` uses this class method in order to interpret the given object(s). """ try: cli = obj.cli except AttributeError: if callable(obj): cli = cls(obj, **kwargs) else: try: iter(obj) except TypeError: raise TypeError("Don't know how to build a cli for " + repr(obj)) cli = SubcommandDispatcher(obj, **kwargs).cli return cli @property def cli(self): """Returns the object itself, in order to be selected by `.get_cli`""" return self def __repr__(self): return '<Clize for {0!r}>'.format(self.func) def __get__(self, obj, owner=None): try: func = self.func.__get__(obj, owner) except AttributeError: func = self.func if func is self.func: return self params = self.parameters() params['owner'] = obj return type(self)(func, **params) @util.property_once def helper(self): """A cli object(usually inherited from `.help.Help`) when the user requests a help message. See the constructor for ways to affect this attribute.""" if self.helper_class is None: from clize.help import ClizeHelp as class_ else: class_ = self.helper_class return class_(self, self.owner) @util.property_once def signature(self): """The `.parser.CliSignature` object used to parse arguments.""" return parser.CliSignature.from_signature( self.func_signature, extra=itertools.chain(self._process_alt(self.alt), self.extra)) @util.property_once def func_signature(self): return signature(self.func) def _process_alt(self, alt): if self.help_names: p = parser.FallbackCommandParameter( func=self.helper.cli, undocumented=self.hide_help, aliases=self.help_aliases) yield p for name, func in util.dict_from_names(alt).items(): func = self.get_cli(func) param = parser.AlternateCommandParameter( undocumented=False, func=func, aliases=[util.name_py2cli(name, kw=True)]) yield param def __call__(self, *args): with errors.SetUserErrorContext(cli=self, pname=args[0]): func, name, posargs, kwargs = self.read_commandline(args) return func(*posargs, **kwargs) def read_commandline(self, args): """Reads the command-line arguments from args and returns a tuple with the callable to run, the name of the program, the positional and named arguments to pass to the callable. :raises: `.ArgumentError` """ ba = self.signature.read_arguments(args[1:], args[0]) func, post, posargs, kwargs = ba name = ' '.join([args[0]] + post) return func or self.func, name, posargs, kwargs
def _dispatcher_helper(*args, **kwargs): """alias for clize.help.DispatcherHelper, avoiding circular import""" from clize.help import ClizeHelp, HelpForSubcommands return ClizeHelp(*args, builder=HelpForSubcommands.from_subject, **kwargs)
[docs]class SubcommandDispatcher(object): clizer = Clize def __init__(self, commands=(), description=None, footnotes=None, **kwargs): self.cmds, self.cmds_by_name = cli_commands( commands, namef=util.name_py2cli, clizer=self.clizer) self.description = description self.footnotes = footnotes self.clize_kwargs = kwargs @annotate(name=parameters.pass_name, command=parser.Parameter.LAST_OPTION) def _cli(self, name, command, *args): try: func = self.cmds_by_name[command.lower()] except KeyError: guess = util.closest_option(command, list(self.cmds_by_name)) if guess: raise errors.ArgumentError( 'Unknown command "{0}". Did you mean "{1}"?' .format(command, guess)) raise errors.ArgumentError('Unknown command "{0}"'.format(command)) return func('{0} {1}'.format(name, command), *args) @property def cli(self): c = Clize(self._cli, helper_class=_dispatcher_helper, **self.clize_kwargs) c.owner = self return c
def fix_argv(argv, path, main): """Properly display ``python -m`` invocations""" if not path[0]: try: name = main_module_name(main) except AttributeError: pass else: argv = argv[:] argv[0] = '{0} -m {1}'.format( get_executable(sys.executable, 'python'), name) else: name = get_executable(argv[0], argv[0]) argv = argv[:] argv[0] = name return argv def get_executable(path, default): if not path: return default if path.endswith('.py'): return path basename = os.path.basename(path) try: which = shutil.which except AttributeError: which = None else: if which(basename) == path: return basename try: rel = os.path.relpath(path) except ValueError: return basename if rel.startswith('../'): if which is None and os.path.isabs(path): return basename return path return rel _py27 = sys.version_info >= (2,7) def main_module_name(module): modname = os.path.splitext(os.path.basename(module.__file__))[0] if modname == '__main__' and _py27: return module.__package__ elif not module.__package__: return modname else: return module.__package__ + '.' + modname
[docs]@autokwoargs def run(args=None, catch=(), exit=True, out=None, err=None, *fn, **kwargs): """Runs a function or :ref:`CLI object<cli-object>` with ``args``, prints the return value if not None, or catches the given exception types as well as `clize.UserError` and prints their string representation, then exit with the appropriate status code. :param sequence args: The arguments to pass the CLI, for instance ``('./a_script.py', 'spam', 'ham')``. If unspecified, uses `sys.argv`. :param catch: Catch these exceptions and print their string representation rather than letting Python print an uncaught exception traceback. :type catch: sequence of exception classes :param bool exit: If true, exit with the appropriate status code once the function is done. :param file out: The file in which to print the return value of the command. If unspecified, uses `sys.stdout` :param file err: The file in which to print any exception text. If unspecified, uses `sys.stderr`. """ if len(fn) == 1: fn = fn[0] cli = Clize.get_cli(fn, **kwargs) if args is None: # import __main__ causes double imports when # python2.7 -m apackage # is used module = sys.modules['__main__'] args = fix_argv(sys.argv, sys.path, module) if out is None: out = sys.stdout if err is None: err = sys.stderr try: ret = cli(*args) except tuple(catch) + (errors.UserError,) as exc: print(str(exc), file=err) if exit: sys.exit(2 if isinstance(exc, errors.ArgumentError) else 1) else: if ret is not None: print(ret, file=out) if exit: sys.exit()