Skip to content

Commit

Permalink
refactor: reduce number of modules
Browse files Browse the repository at this point in the history
  • Loading branch information
jnoortheen committed Nov 3, 2020
1 parent 81c148c commit ca4471f
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 202 deletions.
2 changes: 1 addition & 1 deletion arger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pkg_resources import DistributionNotFound, get_distribution

from .main import Arger
from .parser.classes import Argument, Option
from .parser.funcs import Argument, Option

try:
__version__ = get_distribution("arger").version
Expand Down
2 changes: 1 addition & 1 deletion arger/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import typing as tp

from .parser.funcs import ParsedFunc, parse_function
from .types import VarArg
from .typing_utils import VarArg

CMD_TITLE = "commands"
LEVEL = '__level__'
Expand Down
36 changes: 13 additions & 23 deletions arger/parser/actions.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
import argparse
from typing import Any, Tuple, Union

from ..types import VarArg, VarKw
from ..typing_utils import (
UNDEFINED,
cast,
get_inner_args,
get_origin,
is_enum,
is_iterable,
is_tuple,
unpack_type,
)
from .. import typing_utils as tp_utils


def get_nargs(typ: Any) -> Tuple[Any, Union[int, str]]:
inner = unpack_type(typ)
inner = tp_utils.unpack_type(typ)
if (
is_tuple(typ)
tp_utils.is_tuple(typ)
and typ != tuple
and not isinstance(typ, (VarKw, VarArg))
and get_inner_args(typ)
and not isinstance(typ, (tp_utils.VarKw, tp_utils.VarArg))
and tp_utils.get_inner_args(typ)
):
args = get_inner_args(typ)
args = tp_utils.get_inner_args(typ)
inner = inner if len(set(args)) == 1 else str
return inner, '+' if (... in args) else len(args)
return inner, "*"
Expand All @@ -32,26 +22,26 @@ class TypeAction(argparse.Action):
"""After the parse update the type of value"""

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

if is_enum(origin):
if tp_utils.is_enum(origin):
kwargs.setdefault("choices", [e.name for e in origin])
origin = str

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

def __call__(self, parser, namespace, values, option_string=None):
if is_iterable(self.orig_type):
if tp_utils.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))
setattr(namespace, self.dest, tp_utils.cast(self.orig_type, vals))
102 changes: 0 additions & 102 deletions arger/parser/classes.py

This file was deleted.

146 changes: 128 additions & 18 deletions arger/parser/funcs.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
import inspect
import typing as tp
from argparse import Action, ArgumentParser
from collections import OrderedDict
from itertools import filterfalse, tee
from typing import Callable, Dict, Iterable, List, NamedTuple, Optional, Tuple

from arger.parser.docstring import parse_docstring

from ..types import VarArg, VarKw
from ..typing_utils import UNDEFINED, T
from .classes import Argument, Option
from .utils import FlagsGenerator
from ..typing_utils import UNDEFINED, T, VarArg, VarKw
from .actions import TypeAction


class Param(NamedTuple):
class Param(tp.NamedTuple):
name: str
type: str
help: Optional[str]
flags: List[str]
help: tp.Optional[str]
flags: tp.List[str]


class ParsedFunc(NamedTuple):
args: Dict[str, Argument]
fn: Optional[Callable] = None
description: str = ''
epilog: str = ''


def partition(pred, iterable: Iterable[T]) -> Tuple[Iterable[T], Iterable[T]]:
def partition(
pred, iterable: tp.Iterable[T]
) -> tp.Tuple[tp.Iterable[T], tp.Iterable[T]]:
"""Use a predicate to partition entries into false entries and true entries"""
# partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9
t1, t2 = tee(iterable)
Expand Down Expand Up @@ -63,6 +57,122 @@ def get_param(param: inspect.Parameter) -> Param:
)


class FlagsGenerator:
"""To identify short options that haven't been used yet based on the parameter name."""

def __init__(self):
self.used_short_options: tp.Set[str] = set()

def generate(self, param_name: str) -> tp.Iterator[str]:
long_flag = "--" + param_name.replace('_', '-')
for letter in param_name:
if letter not in self.used_short_options:
self.used_short_options.add(letter)
yield "-" + letter
break

yield long_flag


class Argument:
flags: tp.Tuple[str, ...] = ()

def __init__(
self,
**kwargs,
):
"""Represent positional arguments to the command that are required by default.
Analogous to [ArgumentParser.add_argument](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument)
Args:
type (Union[Callable[[str], T], FileType]): The type to which the command-line argument should be converted. Got from annotation.
help (str): A brief description of what the argument does. From docstring.
metavar (str): A name for the argument in usage messages.
required (bool): Whether or not the command-line option may be omitted (optionals only).
nargs (Union[int, str]): The number of command-line arguments that should be consumed.
to be generated from the type-hint.
const (Any): covered by type-hint and default value given
choices (Iterable[str]): covered by enum type
action (Union[str, Type[Action]]): The basic type of action to be taken
when this argument is encountered at the command line.
"""
if "action" not in kwargs:
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)}>"

def update_flags(self, name: str):
self.flags = (name,)

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


class Option(Argument):
def __init__(self, *flags: str, **kwargs):
"""Represent optional arguments that has flags.
Args:
*flags: The option's flags
**kwargs: they are passed to `Argument`.
default (Any): 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 if it is not type annotated.
* 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.
Examples:
- Option('-f', '--foo', default="value")
"""
super().__init__(**kwargs)
self.flags = flags

def update_flags(
self, name: str, option_generator: tp.Optional[FlagsGenerator] = None
):
self.kwargs.setdefault('dest', name)
if not self.flags and option_generator is not None:
self.flags = tuple(option_generator.generate(name))

def update(self, tp: tp.Any = UNDEFINED, default: tp.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)
elif default is not None and tp is UNDEFINED:
tp = type(default)
super().update(tp)


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


def create_option(param: Param, default, option_generator: FlagsGenerator):
if isinstance(default, Option):
default.kwargs.setdefault('help', param.help)
Expand Down Expand Up @@ -91,15 +201,15 @@ def create_argument(param: Param) -> Argument:
return arg


def parse_function(func: Optional[Callable]) -> ParsedFunc:
def parse_function(func: tp.Optional[tp.Callable]) -> ParsedFunc:
"""Parse 'func' and adds parser arguments from function signature."""
if func is None:
return ParsedFunc({})

docstr, positional_params, kw_params = prepare_params(func)
option_generator = FlagsGenerator()

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

Expand Down
18 changes: 0 additions & 18 deletions arger/parser/utils.py

This file was deleted.

0 comments on commit ca4471f

Please sign in to comment.