Created
October 8, 2022 17:49
-
-
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.
This file contains 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
"""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