Skip to content

Commit

Permalink
refactor: remove ParsedFunc type
Browse files Browse the repository at this point in the history
  • Loading branch information
jnoortheen committed Nov 9, 2020
1 parent 65a43a4 commit 37810d6
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 84 deletions.
4 changes: 1 addition & 3 deletions arger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,4 @@
except DistributionNotFound:
__version__ = "(local)"

__all__ = [
"Arger",
]
__all__ = ["Arger", "Argument", "Option"]
9 changes: 6 additions & 3 deletions arger/docstring.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import functools
import inspect
import re
import typing as tp

Expand Down Expand Up @@ -142,9 +143,11 @@ def get_parsers():
return [NumpyDocParser(), GoogleDocParser(), RstDocParser()]


def parse_docstring(doc: tp.Optional[str]) -> DocstringTp:
if doc:
def parse_docstring(func: tp.Optional[tp.Callable]) -> DocstringTp:
doc = ''
if func:
doc = inspect.getdoc(func)
for parser in get_parsers():
if parser.matches(doc):
return parser.parse(doc)
return DocstringTp(description=doc or '', epilog='', params={})
return DocstringTp(description=doc, epilog='', params={})
50 changes: 1 addition & 49 deletions arger/funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
import argparse
import inspect
import typing as tp
from argparse import Action, ArgumentParser
from collections import OrderedDict
from typing import Any, Tuple, Union

from arger import typing_utils as tp_utils
from arger.docstring import ParamDocTp, parse_docstring
from arger.docstring import ParamDocTp

_EMPTY = inspect.Parameter.empty

Expand Down Expand Up @@ -61,9 +59,6 @@ def __init__(
kwargs['action'] = TypeAction
self.kwargs = kwargs

def add(self, parser: ArgumentParser) -> Action:
return parser.add_argument(*self.flags, **self.kwargs)

def __repr__(self):
"""helps during tests"""
return f"<{self.__class__.__name__}: {self.flags}, {repr(self.kwargs)}>"
Expand Down Expand Up @@ -141,49 +136,6 @@ def update_default(self, typ: tp.Any, default: tp.Any = _EMPTY):
self._update_type(typ)


class ParsedFunc:
fn: tp.Optional[tp.Callable] = None
description: str = ''
epilog: str = ''
args: tp.Dict[str, Argument]

def __init__(self, func: tp.Optional[tp.Callable]):
"""Parse 'func' and adds parser arguments from function signature."""
if func is None:
self.args = {}
return

docstr = parse_docstring(inspect.getdoc(func))

sign = inspect.signature(func)
self.fn = func
self.description = docstr.description
self.epilog = docstr.epilog

option_generator = FlagsGenerator()
self.args = OrderedDict()
for param in sign.parameters.values():
param_doc = docstr.params.get(param.name)
self.args[param.name] = create_argument(param, param_doc, option_generator)

def dispatch(self, ns: argparse.Namespace) -> tp.Any:
if self.fn:
kwargs = {}
args = []
sign = inspect.signature(self.fn)
for arg_name in self.args:
val = getattr(ns, arg_name)
param = sign.parameters[arg_name]
if param.kind == param.POSITIONAL_ONLY:
args.append(val)
elif param.kind == param.VAR_POSITIONAL:
args.extend(val)
else:
kwargs[arg_name] = val
return self.fn(*args, **kwargs)
return None


def create_argument(
param: inspect.Parameter,
pdoc: tp.Optional[ParamDocTp],
Expand Down
73 changes: 53 additions & 20 deletions arger/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import argparse as ap
import copy
import inspect
import sys
import typing as tp
from collections import OrderedDict

from arger.funcs import ParsedFunc
from arger.docstring import DocstringTp, parse_docstring
from arger.funcs import Argument, FlagsGenerator, create_argument

CMD_TITLE = "commands"
LEVEL = '__level__'
Expand All @@ -20,7 +23,7 @@ def __init__(
self,
func: tp.Optional[tp.Callable] = None,
version: tp.Optional[str] = None,
_parsed_fn: tp.Optional[ParsedFunc] = None, # passed from subparser action
_doc_str: tp.Optional[DocstringTp] = None, # passed from subparser action
_level=0, # passed from subparser action
**kwargs,
):
Expand All @@ -39,26 +42,40 @@ def __init__(
kwargs.setdefault('formatter_class', ap.ArgumentDefaultsHelpFormatter)

self.sub_parser_action: tp.Optional[ap._SubParsersAction] = None
self.func = ParsedFunc(func) if _parsed_fn is None else _parsed_fn

if self.func.description:
kwargs.setdefault("description", self.func.description)
self.args: tp.Dict[str, Argument] = OrderedDict()
docstr = parse_docstring(func) if _doc_str is None else _doc_str
kwargs.setdefault("description", docstr.description)
kwargs.setdefault("epilog", docstr.epilog)

super().__init__(**kwargs)

self.set_defaults(**{LEVEL: _level})
if func:
self._add_arguments(func, docstr, _level)

if version:
self.add_argument('--version', action='version', version=version)
self._add_args(_level)

def _add_args(self, level: int):
self.set_defaults(**{f'{FUNC_PREFIX}{level}': self.func.dispatch, LEVEL: level})
def _add_arguments(self, func: tp.Callable, docstr: DocstringTp, level: int):
option_generator = FlagsGenerator()
sign = inspect.signature(func)

for param in sign.parameters.values():
param_doc = docstr.params.get(param.name)
self.args[param.name] = create_argument(param, param_doc, option_generator)

# parser level defaults
self.set_defaults(**{f'{FUNC_PREFIX}{level}': self.dispatch(func)})

for arg_name, arg in self.func.args.items():
if arg_name.startswith(
'_'
): # useful only when `_namespace_` is requested or it is a kwarg
for arg_name, arg in self.args.items():
# useful only when `_namespace_` is requested or it is a kwarg
if arg_name.startswith('_'):
continue
arg.add(self)
self._add_arg(arg)

def _add_arg(self, arg: Argument):
self.add_argument(*arg.flags, **arg.kwargs)

def run(self, *args: str, capture_sys=True) -> ap.Namespace:
"""Parse cli and dispatch functions.
Expand Down Expand Up @@ -99,15 +116,31 @@ def add_cmd(self, func: tp.Callable) -> ap.ArgumentParser:
func: function
"""
if not self.sub_parser_action:
self.sub_parser_action = self.add_subparsers(
# action=CommandAction,
title=CMD_TITLE,
)
self.sub_parser_action = self.add_subparsers(title=CMD_TITLE)

parsed_fn = ParsedFunc(func)
docstr = parse_docstring(func)
return self.sub_parser_action.add_parser(
name=func.__name__,
help=parsed_fn.description,
_parsed_fn=parsed_fn,
help=docstr.description,
func=func,
_doc_str=docstr,
_level=self.get_default(LEVEL) + 1,
)

def dispatch(self, fn: tp.Callable) -> tp.Any:
"""Calls the given function with args parsed from CLI"""

def _dispatch(ns: ap.Namespace):
kwargs = {}
args = []
for arg_name, arg in self.args.items():
val = getattr(ns, arg_name)
if arg.kind == inspect.Parameter.POSITIONAL_ONLY:
args.append(val)
elif arg.kind == inspect.Parameter.VAR_POSITIONAL:
args.extend(val)
else:
kwargs[arg_name] = val
return fn(*args, **kwargs)

return _dispatch
4 changes: 2 additions & 2 deletions docs/examples/1-single-function.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 1,
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"_namespace_ (<class 'argparse.Namespace'>): [('__func_0', 'dispatch'), ('__level__', 0), ('kw1', None), ('kw2', False), ('param1', 10), ('param2', 'p2')]\r\n",
"_namespace_ (<class 'argparse.Namespace'>): [('__func_0', '_dispatch'), ('__level__', 0), ('kw1', None), ('kw2', False), ('param1', 10), ('param2', 'p2')]\r\n",
"kw1 (<class 'NoneType'>): None\r\n",
"kw2 (<class 'bool'>): False\r\n",
"param1 (<class 'int'>): 10\r\n",
Expand Down
4 changes: 2 additions & 2 deletions tests/test_args_opts/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: disable = redefined-outer-name
# pylint: disable = redefined-outer-name, protected-access

import inspect

Expand All @@ -18,7 +18,7 @@ def param_doc(hlp=''):
def add_arger():
def _add(argument):
par = Arger()
argument.add(par)
par._add_arg(argument)
return par

return _add
Expand Down
4 changes: 1 addition & 3 deletions tests/test_docstrings_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import inspect

import pytest

from arger.docstring import ParamDocTp, parse_docstring
Expand Down Expand Up @@ -67,7 +65,7 @@ def func_rst():
],
)
def test_docstring_parser(fn):
result = parse_docstring(inspect.getdoc(fn))
result = parse_docstring(fn)
assert result.description == "Summary line.\n\nExtended description of function."
assert list(result.params) == ['arg1', 'arg2', 'arg3']
assert list(result.params.values()) == [
Expand Down
5 changes: 3 additions & 2 deletions tests/test_parser_funcs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from arger.funcs import ParsedFunc, TypeAction
from arger.funcs import TypeAction
from arger.main import Arger

from .utils import _reprint

Expand All @@ -13,7 +14,7 @@ def main(param1: int, param2: str, kw1=None, kw2=False, *args: int):


def test_parse_function():
docs = ParsedFunc(func=main)
docs = Arger(func=main)
assert (
docs.description == "Example function with types documented in the docstring."
)
Expand Down

0 comments on commit 37810d6

Please sign in to comment.