Skip to content

Instantly share code, notes, and snippets.

@righthandabacus
Created October 8, 2022 17:49
Show Gist options
  • Save righthandabacus/2e548e65f479b819603163856468c9d5 to your computer and use it in GitHub Desktop.
Save righthandabacus/2e548e65f479b819603163856468c9d5 to your computer and use it in GitHub Desktop.
Generating a command line program from a function in Python. Using argparse to construct a command line and infer the argument schema using type hints and docstring.
"""Convert a function into a command line program automatically based on type hints and docstrings
"""
import argparse
import cmath
import math
import typing
from docstring_parser import parse
CLI_ENTRY_POINT = None # a function will be assigned here
def analyzefunc(func):
"""Look into a function and get the arguments, type hints, and docstring"""
props = {
"name": func.__code__.co_name,
"file": func.__code__.co_filename,
"line": func.__code__.co_firstlineno,
"vars": func.__code__.co_varnames,
"n_pos": func.__code__.co_posonlyargcount,
"n_kws": func.__code__.co_kwonlyargcount,
}
n_args = func.__code__.co_argcount
props["n_args"] = n_args + props["n_kws"]
props["args"] = props["vars"][:props["n_args"]]
defaults = func.__defaults__ or []
kwdefaults = func.__kwdefaults__ or {}
props["arg_defaults"] = dict(zip(props["args"][::-1], defaults[::-1])) | kwdefaults
props["doc"] = parse(func.__doc__)
props["hints"] = typing.get_type_hints(func)
return props
def genshort(name, used):
"""Generate a single-character code from an argument name"""
for c in name:
cc = [c, c.upper(), c.lower()]
for x in cc:
if x not in used:
return x
def clidecorator(func):
"""Decorator to convert a function into a command line program with
appropriate command line arguments"""
global CLI_ENTRY_POINT
# Analyze the function `fn`
props = analyzefunc(func)
# Prepare argparse
desc = props["doc"].short_description or props["name"]
prog = f"python {props['file']}"
parser = argparse.ArgumentParser(prog=prog, description=desc,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
shortcodes = set(["h"])
for var in props["args"]:
# each var will be an argument added to parser
char = genshort(var, shortcodes)
if char is not None:
shortcodes.add(char)
# make the kwargs to add_argument()
argargs = ["--" + var]
if char:
argargs.insert(0, "-" + char)
argkwargs = {}
if var in props["arg_defaults"]:
argkwargs["default"] = props["arg_defaults"][var]
else:
argkwargs["required"] = True
if vardoc := [x for x in props["doc"].params if x.arg_name == var]:
argkwargs["help"] = vardoc[0].description
if var in props["hints"]:
if isinstance(props["hints"][var], type):
argkwargs["type"] = props["hints"][var]
elif isinstance(props["hints"][var], typing._GenericAlias):
argkwargs["type"] = typing.get_args(props["hints"][var])[0]
parser.add_argument(*argargs, **argkwargs)
def main():
"define the main program entry points"
args = parser.parse_args()
kwargs = {}
for var in props["args"]:
kwargs[var] = getattr(args, var)
print(func(**kwargs))
CLI_ENTRY_POINT = main
@clidecorator
def quadratic(a: float, b: float, c: float, x: float=None, give_roots: bool=False):
"""Evaluating a quadratic polynomial and find its root
Parameters
----------
a : float
Coefficient of x^2 term
b : float
Coefficient of x term
c : float
Coefficient of constant term
x : float, optional
Value that the quadratic polynomial should evaluate on
give_roots : bool, optional
If True, roots of the polynomial will be returned as well
Returns
-------
A dictionary containing the value of the polynomial evaluated (if provided)
as well as the roots
"""
ret = {}
# evaluate polynomial
if x is not None:
ret["value"] = ((a*x) + b)*x + c
# find roots
if give_roots:
det = b*b - 4*a*c
first = -b / (2*a)
if det < 0:
second = cmath.sqrt(det) / (2*a)
else:
second = math.sqrt(det) / (2*a)
ret["roots"] = [first + second, first - second]
return ret
if __name__ == "__main__":
CLI_ENTRY_POINT()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment