Skip to content

Commit

Permalink
refactor: inherit Option from Argument
Browse files Browse the repository at this point in the history
  • Loading branch information
jnoortheen committed Apr 17, 2020
1 parent 584e530 commit c581cb9
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 116 deletions.
64 changes: 32 additions & 32 deletions arger/parser/actions.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
import argparse
from enum import Enum
from inspect import isclass
from typing import Any, List, Tuple, Union
from typing import Any, Tuple, Union

from ..types import UNDEFINED, VarArg, VarKw
from ..typing_utils import ARGS, get_origin, match_types, unpack_type


def get_nargs(typ: Any) -> Union[int, str]:
if match_types(typ, Tuple) and hasattr(typ, ARGS) and getattr(typ, ARGS):
args = getattr(typ, ARGS)
return '+' if (... in args) else len(args)
return "*"
from ..typing_utils import (
cast,
get_inner_args,
get_origin,
is_enum,
is_iterable,
is_tuple,
unpack_type,
)


def get_nargs(typ: Any) -> Tuple[Any, Union[int, str]]:
inner = unpack_type(typ)
if is_tuple(typ) and get_inner_args(typ):
args = get_inner_args(typ)
inner = inner if len(set(args)) == 1 else str
return inner, '+' if (... in args) else len(args)
return inner, "*"


class TypeAction(argparse.Action):
"""after the parse update the type of value"""

def __init__(self, *args, **kwargs):
typ = kwargs.pop("type", UNDEFINED)
self.container_type = None
self.orig_type = typ
if typ is not UNDEFINED:
origin = get_origin(typ)
if is_iterable(origin) or isinstance(typ, (VarArg, VarKw)):
origin, kwargs["nargs"] = get_nargs(typ)

if origin in {list, tuple} or isinstance(origin, (VarArg, VarKw)):
kwargs["nargs"] = get_nargs(typ)
inner_type = unpack_type(typ)
if match_types(typ, list):
origin = inner_type
else: # tuple
# origin = inner_type[0] if len(set(inner_type)) == 1 else str
origin = str
self.container_type = lambda x: tuple(x)
if isclass(origin) and issubclass(origin, Enum):
kwargs.setdefault("choices", [e.name for e in typ])
if is_enum(origin):
kwargs.setdefault("choices", [e.name for e in origin])
origin = str
self.container_type = lambda x: typ[x]

kwargs["type"] = origin
super().__init__(*args, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
# def __call__(self, parser, namespace, values, option_string=None):
# items = getattr(namespace, self.dest, None)
# items = _copy_items(items)
# items.append(self.const)
# setattr(namespace, self.dest, items)
if self.container_type:
values = self.container_type(values)
setattr(namespace, self.dest, values)
if is_iterable(self.orig_type):
items = getattr(namespace, self.dest, ()) or ()
items = list(items)
items.extend(values)
vals = items
else:
vals = values
setattr(namespace, self.dest, cast(self.orig_type, vals))


# class CommandAction(argparse._SubParsersAction):
Expand Down
102 changes: 56 additions & 46 deletions arger/parser/classes.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
# pylint: disable = W0221
import argparse
import inspect
from typing import Any, List, Optional
from typing import Any, Tuple

from arger.parser.utils import generate_flags

from ..types import UNDEFINED
from .actions import TypeAction


def get_action(
_type, default=None,
):
if default is False:
return "store_true"
if default is True:
return "store_false"
return TypeAction
class Argument:
"""Represent positional argument that are required."""

flags: Tuple[str, ...] = ()

class Option:
def __init__(
self, flags: Optional[List[str]] = None, default: Any = UNDEFINED, **kwargs,
self, **kwargs,
):
"""Represent optional arguments to the command.
Expand All @@ -29,16 +24,6 @@ def __init__(
Args:
flags: Either a name or a list of option strings, e.g. -f, --foo.
default: The value produced if the argument is absent from the command line.
* The default value assigned to a keyword argument helps determine
the type of option and action.
* The default value is assigned directly to the parser's default for that option.
* In addition, it determines the ArgumentParser action
* a default value of False implies store_true, while True implies store_false.
* If the default value is a list, the action is append
(multiple instances of that option are permitted).
* Strings or None imply a store action.
type: The type to which the command-line argument should be converted.
help: A brief description of what the argument does.
metavar: A name for the argument in usage messages.
Expand All @@ -53,24 +38,7 @@ def __init__(
action: The basic type of action to be taken when this argument is encountered at the command line.
:type action: Union[str, Type[argparse.Action]]
"""
name = kwargs.pop("name", None)
self.flags = flags or ([name] if name else [])

tp = kwargs.pop("type", UNDEFINED)
if default is not UNDEFINED:
kwargs["default"] = default

if tp is UNDEFINED and default is not None:
tp = type(default)

action = get_action(tp, default)
kwargs.setdefault("action", action)
if tp is not UNDEFINED and action not in { # bool actions dont need type
"store_false",
"store_true",
}:
kwargs["type"] = tp

kwargs.setdefault('action', TypeAction) # can be overridden by user
self.kwargs = kwargs

def add(self, parser: argparse.ArgumentParser):
Expand All @@ -87,12 +55,54 @@ def __repr__(self):
"""helps during tests"""
return f"{self.__class__.__name__}: {self.flags}, {repr(self.kwargs)}"

def set_flags(self, option_generator, name: str):
hlp = self.kwargs.pop("help").split()
# generate flags
self.flags = generate_flags(name, hlp, option_generator)
self.kwargs["help"] = " ".join(hlp)
def update_flags(self, name: str):
self.flags = (name,)

def update(self, tp: Any = UNDEFINED, **_):
"""Update type externally."""
tp = self.kwargs.pop('type', tp)
if tp is not UNDEFINED:
self.kwargs['type'] = tp


class Option(Argument):
"""Represents optional arguments that has flags.
default: The value produced if the argument is absent from the command line.
* The default value assigned to a keyword argument helps determine
the type of option and action.
* The default value is assigned directly to the parser's default for that option.
* In addition, it determines the ArgumentParser action
* a default value of False implies store_true, while True implies store_false.
* If the default value is a list, the action is append
(multiple instances of that option are permitted).
* Strings or None imply a store action.
"""

def __init__(self, *flags: str, **kwargs):
super().__init__(**kwargs)
self.flags = flags

def update_flags(self, name: str, option_generator: Any = None):
self.kwargs.setdefault('dest', name)
if not self.flags and option_generator is not None:
hlp = self.kwargs.pop("help").split()
# generate flags
self.flags = tuple(generate_flags(name, hlp, option_generator))
self.kwargs["help"] = " ".join(hlp)

def update(self, tp: Any = UNDEFINED, default: Any = UNDEFINED, **_):
"""Update type and default externally"""
default = self.kwargs.pop('default', default)
if default is not UNDEFINED:
self.kwargs["default"] = default

if isinstance(default, bool):
self.kwargs['action'] = (
"store_true" if default is False else "store_false"
)
tp = self.kwargs.pop('type', UNDEFINED)

class Argument(Option):
"""Represent positional argument that are required."""
elif default is not None and tp is UNDEFINED:
tp = type(default)
super().update(tp)
31 changes: 18 additions & 13 deletions arger/parser/funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
Param = namedtuple("Param", ["name", "type", "help"])


def to_dict(p: Param):
return p._asdict()


def prepare_params(func, docs: Dict[str, str]):
(args, kwargs, annotations) = portable_argspec(func)

Expand All @@ -30,16 +26,25 @@ def get_param(param):

def create_option(param: Param, default, option_generator):
if isinstance(default, Argument):
default.flags = [param.name]
option = default
default.update_flags(param.name)
default.update(param.type)
elif isinstance(default, Option):
if "dest" not in default.kwargs:
default.kwargs["dest"] = param.name
if not default.flags:
default.set_flags(option_generator, param.name)
option = default
option.update_flags(param.name, option_generator)
option.update(param.type)
else:
default = Option(dest=param.name, default=default, **param._asdict())
default.set_flags(option_generator, param.name)
return default
option = Option(help=param.help)
option.update_flags(param.name, option_generator)
option.update(param.type, default)
return option


def create_argument(param: Param):
arg = Argument(help=param.help)
arg.update_flags(param.name)
arg.update(tp=param.type)
return arg


def prepare_arguments(func, param_docs) -> Dict[str, Option]:
Expand All @@ -49,7 +54,7 @@ def prepare_arguments(func, param_docs) -> Dict[str, Option]:

arguments: Dict[str, Option] = OrderedDict()
for param in positional_params:
arguments[param.name] = Argument(**param._asdict())
arguments[param.name] = create_argument(param)

for param, default in kw_params:
arguments[param.name] = create_option(param, default, option_generator)
Expand Down
13 changes: 6 additions & 7 deletions arger/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@
class VarArg:
"""Represent variadic arguent."""

container: Any = tuple
__origin__: Any = tuple
__args__ = ()

def __init__(self, tp):
self.type = tp

def __call__(self, *args, **kwargs):
return self.type(*args, **kwargs)
self.__args__ = (tp,)

def __repr__(self):
tp = getattr(self.type, "__name__", self.type)
tp = self.__args__[0]
tp = getattr(tp, "__name__", tp)
return f"{self.__class__.__name__}[{tp}]"

def __eq__(self, other):
Expand All @@ -32,4 +31,4 @@ def __hash__(self):
class VarKw(VarArg):
"""Represent variadic keyword argument."""

container = dict
__original__ = dict
45 changes: 39 additions & 6 deletions arger/typing_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# pylint: disable = W0212
import sys
from enum import Enum
from inspect import isclass
from typing import Any, List, Set, Tuple


Expand Down Expand Up @@ -37,8 +39,12 @@ def match_types(tp, *matches) -> bool:
ARGS = '__args__'


def get_inner_args(tp):
return getattr(tp, ARGS, ())


def unpack_type(tp, default=str) -> Any:
"""Unpack subscripted type.
"""Unpack subscripted type for use with argparser.
Args:
tp:
Expand All @@ -47,10 +53,37 @@ def unpack_type(tp, default=str) -> Any:
Returns:
type inside the container type
"""
if getattr(tp, ARGS, None) is not None:
if get_inner_args(tp):
inner_tp = getattr(tp, ARGS)
if match_types(tp, list):
if str(inner_tp[0]) != '~T':
return inner_tp[0]
return inner_tp
if inner_tp and str(inner_tp[0]) not in {'~T', 'typing.Any'}:
return inner_tp[0]
return default


def is_iterable(tp):
origin = get_origin(tp)
return origin in {list, tuple, set, frozenset}


def is_enum(tp):
return isclass(tp) and issubclass(tp, Enum)


def is_tuple(tp):
return match_types(tp, tuple)


def cast(tp, val) -> Any:
origin = get_origin(tp)

if is_enum(origin):
return origin[val]

if is_iterable(origin):
val = origin(val)
args = get_inner_args(tp)
if origin in {tuple,} and args and Ellipsis not in args:
return tuple(cast(args[idx], v) for idx, v in enumerate(val))
return origin([cast(unpack_type(tp), v) for v in val])

return origin(val)
4 changes: 2 additions & 2 deletions docs/examples/4-supported-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ optional_tpl (<class 'tuple'>): ()
## ran `cmd1` with valid arguments
```sh
$ python main.py cmd1 10 str1 tp1 tp2 tp3 vtp1 -t otp1 otp2 -o ostr -p 100 -a two
$ python main.py cmd1 10 str1 tp1 tp2 tp3 vtp1 -t otp1 otp2 -t otp3 -o ostr -p 100 -a two
a_tuple (<class 'tuple'>): ('tp1', 'tp2', 'tp3')
a_var_tuple (<class 'tuple'>): ('vtp1',)
an_enum (<enum 'Choice'>): Choice.two
an_int (<class 'int'>): 10
an_str (<class 'str'>): str1
optional_int (<class 'int'>): 100
optional_str (<class 'str'>): ostr
optional_tpl (<class 'tuple'>): ('otp1', 'otp2')
optional_tpl (<class 'tuple'>): ('otp1', 'otp2', 'otp3')
```
## ran `cmd2` help
Expand Down

0 comments on commit c581cb9

Please sign in to comment.