diff --git a/fastcore/docments.py b/fastcore/docments.py index d5935605..5ab4fd21 100644 --- a/fastcore/docments.py +++ b/fastcore/docments.py @@ -91,9 +91,9 @@ def _get_comment(line, arg, comments, parms): line -= 1 return dedent('\n'.join(reversed(res))) if res else None -def _get_full(anno, name, default, docs): +def _get_full(anno, name, default, docs, kind): if anno==empty and default!=empty: anno = type(default) - return AttrDict(docment=docs.get(name), anno=anno, default=default) + return AttrDict(docment=docs.get(name), anno=anno, default=default, kind=kind) # %% ../nbs/06_docments.ipynb 22 def _merge_doc(dm, npdoc): @@ -142,8 +142,8 @@ def _docments(s, returns=True, eval_str=False): if isinstance(s,str): s = eval(s) sig = signature(s) - res = {arg:_get_full(p.annotation, p.name, p.default, docs) for arg,p in sig.parameters.items()} - if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs) + res = {arg:_get_full(p.annotation, p.name, p.default, docs, p.kind) for arg,p in sig.parameters.items()} + if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs, '') res = _merge_docs(res, nps) if eval_str: hints = type_hints(s) @@ -158,7 +158,6 @@ def docments(elt, full=False, **kwargs): r = {} params = set(signature(elt).parameters) params.add('return') - def _update_docments(f, r): if hasattr(f, '__delwrap__'): _update_docments(f.__delwrap__, r) r.update({k:v for k,v in _docments(f, **kwargs).items() if k in params diff --git a/fastcore/script.py b/fastcore/script.py index cb545791..40204c75 100644 --- a/fastcore/script.py +++ b/fastcore/script.py @@ -10,6 +10,7 @@ from .imports import * from .utils import * from .docments import docments +from inspect import Parameter # %% ../nbs/08_script.ipynb 15 def store_true(): @@ -50,7 +51,6 @@ def set_default(self, d): else: self.default = d if self.default is not None: self.help += f" (default: {self.default})" - @property def pre(self): return '--' if self.opt else '' @property @@ -78,12 +78,13 @@ def anno_parser(func, # Function to get arguments from param = v.anno if not isinstance(param,Param): param = Param(v.docment, v.anno) param.set_default(v.default) - p.add_argument(f"{param.pre}{k}", **param.kwargs) + if getattr(v, 'kind', '') == Parameter.VAR_POSITIONAL: p.add_argument(f"{param.pre}{k}",**param.kwargs, nargs='*') + else: p.add_argument(f"{param.pre}{k}", **param.kwargs) p.add_argument(f"--pdb", help=argparse.SUPPRESS, action='store_true') p.add_argument(f"--xtra", help=argparse.SUPPRESS, type=str) return p -# %% ../nbs/08_script.ipynb 34 +# %% ../nbs/08_script.ipynb 36 def args_from_prog(func, prog): "Extract args from `prog`" if prog is None or '#' not in prog: return {} @@ -96,10 +97,10 @@ def args_from_prog(func, prog): if t: args[k] = t(v) return args -# %% ../nbs/08_script.ipynb 37 +# %% ../nbs/08_script.ipynb 39 SCRIPT_INFO = SimpleNamespace(func=None) -# %% ../nbs/08_script.ipynb 39 +# %% ../nbs/08_script.ipynb 41 def call_parse(func=None, nested=False): "Decorator to create a simple CLI from `func` using `anno_parser`" if func is None: return partial(call_parse, nested=nested) @@ -113,10 +114,12 @@ def _f(*args, **kwargs): p = anno_parser(func) if nested: args, sys.argv[1:] = p.parse_known_args() else: args = p.parse_args() - args = args.__dict__ + nvar = [v for v in list(args.__dict__.values()) + [[]] if isinstance(v, list)] + args = {k:v for k,v in args.__dict__.items() if not isinstance(v, list)} xtra = otherwise(args.pop('xtra', ''), eq(1), p.prog) tfunc = trace(func) if args.pop('pdb', False) else func - return tfunc(**merge(args, args_from_prog(func, xtra))) + tfunc = partial(tfunc, **merge(args, args_from_prog(func, xtra))) + return tfunc(*nvar[0]) mod = inspect.getmodule(inspect.currentframe().f_back) if getattr(mod, '__name__', '') =="__main__": diff --git a/nbs/06_docments.ipynb b/nbs/06_docments.ipynb index 62542f54..0bc9eaff 100644 --- a/nbs/06_docments.ipynb +++ b/nbs/06_docments.ipynb @@ -303,9 +303,9 @@ " line -= 1\n", " return dedent('\\n'.join(reversed(res))) if res else None\n", "\n", - "def _get_full(anno, name, default, docs):\n", + "def _get_full(anno, name, default, docs, kind):\n", " if anno==empty and default!=empty: anno = type(default)\n", - " return AttrDict(docment=docs.get(name), anno=anno, default=default)" + " return AttrDict(docment=docs.get(name), anno=anno, default=default, kind=kind)" ] }, { @@ -415,8 +415,8 @@ "\n", " if isinstance(s,str): s = eval(s)\n", " sig = signature(s)\n", - " res = {arg:_get_full(p.annotation, p.name, p.default, docs) for arg,p in sig.parameters.items()}\n", - " if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs)\n", + " res = {arg:_get_full(p.annotation, p.name, p.default, docs, p.kind) for arg,p in sig.parameters.items()}\n", + " if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs, '')\n", " res = _merge_docs(res, nps)\n", " if eval_str:\n", " hints = type_hints(s)\n", @@ -438,7 +438,6 @@ " r = {}\n", " params = set(signature(elt).parameters)\n", " params.add('return')\n", - "\n", " def _update_docments(f, r):\n", " if hasattr(f, '__delwrap__'): _update_docments(f.__delwrap__, r)\n", " r.update({k:v for k,v in _docments(f, **kwargs).items() if k in params\n", @@ -456,6 +455,28 @@ "The returned `dict` has parameter names as keys, docments as values. The return value comment appears in the `return`, unless `returns=False`. Using the `add` definition above, we get:" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_items([('args', {'docment': None, 'anno': , 'default': , 'kind': <_ParameterKind.VAR_POSITIONAL: 2>})])" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def timesheet(*args):\n", + " pass\n", + "docments(timesheet, full=True, returns=False, eval_str=True).items()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -510,23 +531,31 @@ "```json\n", "{ 'a': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the 1st number to add'},\n", + " 'docment': 'the 1st number to add',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'b': { 'anno': ,\n", " 'default': 0,\n", - " 'docment': 'the 2nd number to add'},\n", + " 'docment': 'the 2nd number to add',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'return': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the result of adding `a` to `b`'}}\n", + " 'docment': 'the result of adding `a` to `b`',\n", + " 'kind': ''}}\n", "```" ], "text/plain": [ "{'a': {'docment': 'the 1st number to add',\n", " 'anno': 'int',\n", - " 'default': inspect._empty},\n", - " 'b': {'docment': 'the 2nd number to add', 'anno': int, 'default': 0},\n", + " 'default': inspect._empty,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", + " 'b': {'docment': 'the 2nd number to add',\n", + " 'anno': int,\n", + " 'default': 0,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'return': {'docment': 'the result of adding `a` to `b`',\n", " 'anno': 'int',\n", - " 'default': inspect._empty}}" + " 'default': inspect._empty,\n", + " 'kind': ''}}" ] }, "execution_count": null, @@ -556,11 +585,15 @@ "```json\n", "{ 'anno': ,\n", " 'default': ,\n", - " 'docment': 'the 1st number to add'}\n", + " 'docment': 'the 1st number to add',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>}\n", "```" ], "text/plain": [ - "{'docment': 'the 1st number to add', 'anno': int, 'default': inspect._empty}" + "{'docment': 'the 1st number to add',\n", + " 'anno': int,\n", + " 'default': inspect._empty,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>}" ] }, "execution_count": null, @@ -806,23 +839,31 @@ "```json\n", "{ 'a': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the first number to add'},\n", + " 'docment': 'the first number to add',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'b': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the 2nd number to add (default: 0)'},\n", + " 'docment': 'the 2nd number to add (default: 0)',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'return': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the result'}}\n", + " 'docment': 'the result',\n", + " 'kind': ''}}\n", "```" ], "text/plain": [ "{'a': {'docment': 'the first number to add',\n", " 'anno': 'int',\n", - " 'default': inspect._empty},\n", + " 'default': inspect._empty,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'b': {'docment': 'the 2nd number to add (default: 0)',\n", " 'anno': 'int',\n", - " 'default': inspect._empty},\n", - " 'return': {'docment': 'the result', 'anno': 'int', 'default': inspect._empty}}" + " 'default': inspect._empty,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", + " 'return': {'docment': 'the result',\n", + " 'anno': 'int',\n", + " 'default': inspect._empty,\n", + " 'kind': ''}}" ] }, "execution_count": null, diff --git a/nbs/08_script.ipynb b/nbs/08_script.ipynb index 2d3b26d9..6cf94dd0 100644 --- a/nbs/08_script.ipynb +++ b/nbs/08_script.ipynb @@ -151,7 +151,8 @@ "from functools import wraps,partial\n", "from fastcore.imports import *\n", "from fastcore.utils import *\n", - "from fastcore.docments import docments" + "from fastcore.docments import docments\n", + "from inspect import Parameter" ] }, { @@ -259,7 +260,6 @@ " else: self.default = d\n", " if self.default is not None:\n", " self.help += f\" (default: {self.default})\"\n", - "\n", " @property\n", " def pre(self): return '--' if self.opt else ''\n", " @property\n", @@ -376,7 +376,8 @@ " param = v.anno\n", " if not isinstance(param,Param): param = Param(v.docment, v.anno)\n", " param.set_default(v.default)\n", - " p.add_argument(f\"{param.pre}{k}\", **param.kwargs)\n", + " if getattr(v, 'kind', '') == Parameter.VAR_POSITIONAL: p.add_argument(f\"{param.pre}{k}\",**param.kwargs, nargs='*')\n", + " else: p.add_argument(f\"{param.pre}{k}\", **param.kwargs)\n", " p.add_argument(f\"--pdb\", help=argparse.SUPPRESS, action='store_true')\n", " p.add_argument(f\"--xtra\", help=argparse.SUPPRESS, type=str)\n", " return p" @@ -433,6 +434,39 @@ "It also works with type annotations and docments:" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: progname [-h] [args ...]\n", + "\n", + "positional arguments:\n", + " args\n", + "\n", + "optional arguments:\n", + " -h, --help show this help message and exit\n" + ] + } + ], + "source": [ + "def timesheet(*args):\n", + " pass\n", + "p = anno_parser(timesheet, 'progname')\n", + "p.print_help()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -544,10 +578,12 @@ " p = anno_parser(func)\n", " if nested: args, sys.argv[1:] = p.parse_known_args()\n", " else: args = p.parse_args()\n", - " args = args.__dict__\n", + " nvar = [v for v in list(args.__dict__.values()) + [[]] if isinstance(v, list)]\n", + " args = {k:v for k,v in args.__dict__.items() if not isinstance(v, list)}\n", " xtra = otherwise(args.pop('xtra', ''), eq(1), p.prog)\n", " tfunc = trace(func) if args.pop('pdb', False) else func\n", - " return tfunc(**merge(args, args_from_prog(func, xtra)))\n", + " tfunc = partial(tfunc, **merge(args, args_from_prog(func, xtra)))\n", + " return tfunc(*nvar[0])\n", "\n", " mod = inspect.getmodule(inspect.currentframe().f_back)\n", " if getattr(mod, '__name__', '') ==\"__main__\":\n", @@ -588,6 +624,17 @@ "test_eq(test_add(1,2), 3)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@call_parse\n", + "def timesheet(*args):\n", + " pass" + ] + }, { "cell_type": "markdown", "metadata": {},