{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# fastscript\n", "\n", "> API details" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# default_exp core" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "import inspect,functools\n", "import argparse" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def test_eq(a,b): assert a==b,a" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "class Param:\n", " \"A parameter in a function used in `anno_parser` or `call_parse`\"\n", " def __init__(self, help=None, type=None, opt=True, action=None, nargs=None, const=None,\n", " choices=None, required=None):\n", " self.help,self.type,self.opt,self.action,self.nargs = help,type,opt,action,nargs\n", " self.const,self.choices,self.required = const,choices,required\n", " \n", " def set_default(self, d):\n", " if d==inspect.Parameter.empty: self.opt = False\n", " else:\n", " self.default = d\n", " self.help += f\" (default: {d})\"\n", "\n", " @property\n", " def pre(self): return '--' if self.opt else ''\n", " @property\n", " def kwargs(self): return {k:v for k,v in self.__dict__.items() if v is not None and k!='opt'}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Each parameter in your function should have an annotation `Param(...)`. You can pass the following when calling `Param`: `help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required` (i.e. it takes the same parameters as `argparse.ArgumentParser.add_argument`, plus `opt`). Except for `opt`, all of these are just passed directly to `argparse`, so you have all the power of that module at your disposal. Generally you'll want to pass at least `help` (since this is provided as the help string for that parameter) and `type` (to ensure that you get the type of data you expect).\n", "\n", "`opt` is a bool that defines whether a param is optional or required (positional) - but you'll generally not need to set this manually, because fastscript will set it for you automatically based on *default* values. You should provide a default (after the `=`) for any *optional* parameters. If you don't provide a default for a parameter, then it will be a *positional* parameter." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p = Param(help=\"help\", type=int)\n", "p.set_default(1)\n", "test_eq(p.kwargs, {'help': 'help (default: 1)', 'type': int, 'default': 1})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def bool_arg(v):\n", " \"Use as `type` for `Param` to get `bool` behavior\"\n", " if isinstance(v, bool): return v\n", " if v.lower() in ('yes', 'true', 't', 'y', '1'): return True\n", " elif v.lower() in ('no', 'false', 'f', 'n', '0'): return False\n", " else: raise argparse.ArgumentTypeError('Boolean value expected.')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def f(from_name:Param(\"Get args from prog name instead of argparse\", bool_arg)=0,\n", " a:Param(\"param 1\", bool_arg)=1,\n", " b:Param(\"param 2\", str)=\"test\"): ..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def anno_parser(func, prog=None, from_name=False):\n", " \"Look at params (annotated with `Param`) in func and return an `ArgumentParser`\"\n", " p = argparse.ArgumentParser(description=func.__doc__, prog=prog)\n", " for k,v in inspect.signature(func).parameters.items():\n", " param = func.__annotations__.get(k, Param())\n", " param.set_default(v.default)\n", " p.add_argument(f\"{param.pre}{k}\", **param.kwargs)\n", " p.add_argument(f\"--xtra\", help=\"Parse for additional args\", type=str)\n", " return p" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This converts a function with parameter annotations of type `Param` into an `argparse.ArgumentParser` object. Function arguments with a default provided are optional, and other arguments are positional." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "usage: progname [-h] [--a A] [--b B] [--xtra XTRA] required\n", "\n", "my docs\n", "\n", "positional arguments:\n", " required Required param\n", "\n", "optional arguments:\n", " -h, --help show this help message and exit\n", " --a A param 1 (default: 1)\n", " --b B param 2 (default: test)\n", " --xtra XTRA Parse for additional args\n" ] } ], "source": [ "def f(required:Param(\"Required param\", int),\n", " a:Param(\"param 1\", bool_arg)=1,\n", " b:Param(\"param 2\", str)=\"test\"):\n", " \"my docs\"\n", " ...\n", "\n", "p = anno_parser(f, 'progname')\n", "p.print_help()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def args_from_prog(func, prog):\n", " \"Extract args from `prog`\"\n", " if '##' in prog: _,prog = prog.split('##', 1)\n", " progsp = prog.split(\"#\")\n", " args = {progsp[i]:progsp[i+1] for i in range(0, len(progsp), 2)}\n", " for k,v in args.items():\n", " t = func.__annotations__.get(k, Param()).type\n", " if t: args[k] = t(v)\n", " return args" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sometimes it's convenient to extract arguments from the actual name of the called program. `args_from_prog` will do this, assuming that names and values of the params are separated by a `#`. Optionally there can also be a prefix separated by `##` (double underscore)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "exp = {'a': False, 'b': 'baa'}\n", "test_eq(args_from_prog(f, 'foo##a#0#b#baa'), exp)\n", "test_eq(args_from_prog(f, 'a#0#b#baa'), exp)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def call_parse(func):\n", " \"Decorator to create a simple CLI from `func` using `anno_parser`\"\n", " mod = inspect.getmodule(inspect.currentframe().f_back)\n", " if not mod: return func\n", " \n", " @functools.wraps(func)\n", " def _f(*args, **kwargs):\n", " mod = inspect.getmodule(inspect.currentframe().f_back)\n", " if not mod: return func(*args, **kwargs)\n", " \n", " p = anno_parser(func)\n", " args = p.parse_args()\n", " xtra = getattr(args, 'xtra', None)\n", " if xtra is not None:\n", " if xtra==1: xtra = p.prog\n", " for k,v in args_from_prog(func, xtra).items(): setattr(args,k,v)\n", " del(args.xtra)\n", " func(**args.__dict__)\n", " if mod.__name__==\"__main__\":\n", " setattr(mod, func.__name__, _f)\n", " return _f()\n", " else: return _f" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@call_parse\n", "def test_add(a:Param(\"param a\", int), b:Param(\"param 1\",int)):\n", " return a + b" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Test call parse function\n", "This is a test to see if `call_parse` works as a regular function and also as a command-line interface function." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "test_eq(os.system('test_fastscript \"Test if this still works\"'), 0)\n", "test_eq(test_add(1,2), 3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is the main way to use `fastscript`; decorate your function with `call_parse`, add `Param` annotations as shown above, and it can then be used as a script." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Export -" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Converted 00_core.ipynb.\n", "Converted index.ipynb.\n" ] } ], "source": [ "#hide\n", "from nbdev.export import notebook2script\n", "notebook2script()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "jupytext": { "split_at_heading": true }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 2 }