Skip to content

Instantly share code, notes, and snippets.

@u8sand
Last active August 22, 2019 18:42
Show Gist options
  • Save u8sand/b7f9f146c6349316e324d2261e5fd1ac to your computer and use it in GitHub Desktop.
Save u8sand/b7f9f146c6349316e324d2261e5fd1ac to your computer and use it in GitHub Desktop.
Treat a set of python functions as a command-line application
# 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