Last active
August 22, 2019 18:42
-
-
Save u8sand/b7f9f146c6349316e324d2261e5fd1ac to your computer and use it in GitHub Desktop.
Treat a set of python functions as a command-line application
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# From https://gist.github.com/u8sand/b7f9f146c6349316e324d2261e5fd1ac | |
def inject(ctx=globals()): | |
''' [NOT SECURE]: Exposes a set of python functions as a python command-line application. | |
All functions exposed with docstring, argument names, defaults, annotations, etc... | |
The first line of a docstring is treated as the description; and the command-line spec is | |
automatically converted. | |
Usage: add the following to the bottom of your script | |
``` | |
if __name__ == '__main__': | |
import commandify; commandify.inject(globals()) | |
``` | |
NOT SECURE, because it uses `eval` -- but for simple local scripts, it's pretty convenient | |
for making your python functions command-line interactible. | |
''' | |
def _func_to_spec(func): | |
import inspect | |
spec = func.__name__ | |
argspec = inspect.getfullargspec(func) | |
args_req = len(argspec.args or []) - len(argspec.defaults or []) | |
args_opt = len(argspec.args or []) - args_req | |
kwargs_defaults = dict(zip(argspec.args[args_req:], argspec.defaults or []), **(argspec.kwonlydefaults or {})) | |
if args_req > 0: | |
spec += ' <' | |
for ind, arg in enumerate(argspec.args[:args_req]): | |
if ind > 0: | |
spec += ' ' | |
spec += arg | |
annot = (argspec.annotations or {}).get(arg) | |
if annot: | |
spec += ':' + annot.__name__ | |
spec += '>' | |
if args_opt > 0: | |
spec += ' [' | |
for ind, arg in enumerate(argspec.args[args_req:]): | |
if ind > 0: | |
spec += ' ' | |
spec += arg | |
annot = (argspec.annotations or {}).get(arg) | |
if annot: | |
spec += ':' + annot.__name__ | |
spec += '=' + repr(argspec.defaults[ind]) | |
spec += ']' | |
if argspec.varargs: | |
spec += ' [*' | |
spec += argspec.varargs | |
annot = (argspec.annotations or {}).get(argspec.varargs) | |
if annot: | |
spec += ':' + annot.__name__ | |
spec += ']' | |
if kwargs_defaults: | |
spec += ' [' | |
for ind, (kwarg, kwarg_default) in enumerate(kwargs_defaults.items()): | |
if ind > 0: | |
spec += ' ' | |
spec += '--' | |
spec += kwarg | |
annot = (argspec.annotations or {}).get(kwarg) | |
if annot: | |
spec += ':' + annot.__name__ | |
spec += '=' + repr(kwarg_default) | |
spec += ']' | |
if argspec.varkw: | |
spec += ' [**' | |
spec += argspec.varkw | |
annot = (argspec.annotations or {}).get(argspec.varkw) | |
if annot: | |
spec += ':' + annot.__name__ | |
spec += ']' | |
return spec | |
ctx[_func_to_spec.__name__] = _func_to_spec | |
def _arg_to_py(arg, ctx=ctx): | |
''' [NOT SECURE] Pythonize a variable | |
''' | |
try: | |
return eval(arg, ctx) | |
except: | |
pass | |
return arg | |
ctx[_arg_to_py.__name__] = _arg_to_py | |
def _argv_to_py(argv, ctx=ctx): | |
''' [NOT SECURE] Quick and dirty python-functions-to-command-line. | |
''' | |
import re | |
name = argv[0] | |
if len(argv) < 2: | |
print('usage: {} [command] [<kargs ...> [opt_kargs ...] [--kwarg_key=kwarg_val ...]]'.format(name)) | |
print('command:') | |
for funcname, func in ctx.items(): | |
if funcname.startswith('_') or not callable(func): | |
continue | |
print('', _func_to_spec(func), (func.__doc__ or '').splitlines()[0].strip(), sep='\t') | |
return | |
func, args = ctx[argv[1]], argv[2:] | |
kargs = [_arg_to_py(arg) for arg in args if not arg.startswith('--')] | |
wargs = [arg for arg in args if arg.startswith('--')] | |
kwargs = {key[2:]: _arg_to_py(val) for key, val in map(lambda s: s.split('='), wargs)} | |
result = func(*kargs, **kwargs) | |
if result is not None: | |
print(repr(result)) | |
ctx[_argv_to_py.__name__] = _argv_to_py | |
def help(func): | |
''' View the full docstring for a function | |
''' | |
import textwrap | |
print('usage:', _func_to_spec(func)) | |
print(textwrap.dedent(func.__doc__.strip('\n'))) | |
ctx[help.__name__] = help | |
import sys; _argv_to_py(sys.argv, ctx) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment