Source code for pykern.pkcli

# -*- coding: utf-8 -*-
u"""Invoke commands from command line interpreter modules.

Any module in ``<root_pkg>.pkcli`` will be found by this module. The
public functions of the module will be executed when called from the
command line. This module is invoked by :mod:`pykern.pykern_console`.
Every project must have its own invocation module.

The basic form is: <project> <simple-module> <function>. <simple-module>
is the module without `<root_pkg>.pkcli`.  <function> is any function
that begins with a letter and contains word characters (\w).

If the module only has one public function named default_command,
the form is: <project> <simple-module>.

The purpose of this module is to simplify command-line modules. There is
no boilerplate. You just create a module with public functions
in a particular package location (e.g. `pykern.pkcli`).
This module does the rest.

`pykern.pkcli.pkexample` is a working example.

:copyright: Copyright (c) 2015-2016 RadiaSoft LLC.  All Rights Reserved.
from __future__ import absolute_import, division, print_function
import argh
import argparse
import importlib
import inspect
import os.path
import pkgutil
import re
import sys

# Avoid pykern imports so avoid dependency issues for pkconfig
from pykern import pkconfig

#: Sub-package to find command line interpreter (cli) modules will be found
CLI_PKG = ['pkcli', 'pykern_cli']

#: If a module only has one command named this, then execute directly.
DEFAULT_COMMAND = 'default_command'

#: Test for first arg to see if user wants help
_HELP_RE = re.compile(r'^-(-?help|h)$', flags=re.IGNORECASE)

[docs]def command_error(fmt, *args, **kwargs): """Raise CommandError with msg Args: fmt (str): how to represent arguments Raises: CommandError: always """ raise argh.CommandError(fmt.format(*args, **kwargs))
[docs]def main(root_pkg, argv=None): """Invokes module functions in :mod:`pykern.pykern_cli` Looks in ``<root_pkg>.pykern_cli`` for the ``argv[1]`` module. It then invokes the ``argv[2]`` method of that module. Args: root_pkg (str): top level package name argv (list of str): Defaults to `sys.argv`. Only used for testing. Returns: int: 0 if ok. 1 if error (missing command, etc.) """ pkconfig.append_load_path(root_pkg) if not argv: argv = list(sys.argv) prog = os.path.basename(argv.pop(0)) if _is_help(argv): return _list_all(root_pkg, prog) module_name = argv.pop(0) cli = _module(root_pkg, module_name) if not cli: return 1 prog = prog + ' ' + module_name parser = argparse.ArgumentParser( prog=prog, formatter_class=argh.PARSER_FORMATTER) cmds = _commands(cli) dc = _default_command(cmds, argv) if dc: argh.set_default_command(parser, dc) else: argh.add_commands(parser, cmds) if len(argv) < 1: # Python 3: parser doesn't exit if not enough commands parser.error('too few arguments') if argv[0][0] != '-': argv[0] = argv[0].replace('_', '-') argh.dispatch(parser, argv=argv) return 0
def _commands(cli): """Extracts all public functions from `cli` Args: cli (module): where commands are executed from Returns: list of function: public functions sorted alphabetically """ res = [] for n, t in inspect.getmembers(cli): if _is_command(t, cli): res.append(t) sorted(res, key=lambda f: f.__name__.lower()) return res def _default_command(cmds, argv): """Evaluate the default command, handling ``**kwargs`` case. `argparse` and `argh` do not understand ``**kwargs``, i.e. pass through command. There's a case (`pykern.pkcli.pytest`) that requires pass through so we wrap the command and clear `argv` in the case of ``default_command(*args, **kwargs)``. Args: cmds (list): List of commands argv (list): arguments (may be edited) Returns: function: default command or None """ if len(cmds) != 1 or cmds[0].__name__ != DEFAULT_COMMAND: return None dc = cmds[0] spec = inspect.getargspec(dc) if not (spec.varargs and spec.keywords): return dc save_argv = argv[:] def _wrap_default_command(): return dc(*save_argv) del argv[:] return _wrap_default_command def _import(root_pkg, name=None): """Dynamically imports ``root_pkg.CLI_PKG[.name]``. Args: root_pkg (str): top level package name (str): cli module Returns: module: imported module Raises: ImportError: if module could not be loaded """ def _imp(path_list): p = '.'.join(path_list).replace('-', '_') return importlib.import_module(p) #TODO(robnagler) remove once all clients support pkcli directory path = None first_e = None m = None for p in CLI_PKG: path = [root_pkg, p] try: m = _imp(path) break except ImportError as e: # Assumes package (foo.pkcli) has an empty so that # the import should always succeed. if not first_e: first_e = e if not path: raise first_e if not name: return m return _imp(path + [name]) def _is_command(obj, cli): """Is this a valid command function? Args: obj (object): candidate cli (module): module to which function should belong Returns: bool: True if obj is a valid command """ if not inspect.isfunction(obj) or obj.__name__.startswith('_'): return False return hasattr(obj, '__module__') and obj.__module__ == cli.__name__; def _is_help(argv): """Does the user want help? Args: argv (list): list of args Returns: bool: True if no args or --help """ if len(argv) == 0: return True return[0]) def _list_all(root_pkg, prog): """Prints a list of importable modules and exits. Searches ``<root_pkg>.pykern_cli` for submodules, and prints their names. Args: root_pkg (str): top level package prog (str): argv[0], name of program invoked Returns: int: 0 if ok. 1 if error. """ res = [] pykern_cli = _import(root_pkg) path = os.path.dirname(pykern_cli.__file__) for _, n, ispkg in pkgutil.iter_modules([path]): if not ispkg: res.append(n.replace('_', '-')) sorted(res, key=str.lower) res = '\n'.join(res) sys.stderr.write( 'usage: {} module command [args...]\nModules:\n{}\n'.format(prog, res), ) return 1 def _module(root_pkg, name): """Imports the module, catching `ImportError` Args: root_pkg (str): top level package name(str): unqualified name of the module to be imported Returns: module: imported module """ try: return _import(root_pkg, name) except Exception as e: sys.stderr.write(str(e) + "\n") return None