Skip to content

Commit

Permalink
feat: option to customize sub-commands title
Browse files Browse the repository at this point in the history
one can get parser instance using special argument `_arger_`
  • Loading branch information
jnoortheen committed Dec 5, 2020
1 parent 3ed09fc commit 982dab6
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .mypy.ini
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[mypy]

ignore_missing_imports = true
no_implicit_optional = true
;no_implicit_optional = true
check_untyped_defs = true

cache_dir = .cache/mypy/
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ python test.py 100 param2
- `*args` supported but no `**kwargs` support yet.
- all optional arguments that start with underscore is not passed to `Parser`.
They are considered private to the function implementation.
One can use `_namespace_` to get the output from the `ArgumentParser.parse_args()`
Some parameter names with special meaning
- `_namespace_` -> to get the output from the `ArgumentParser.parse_args()`
- `_arger_` -> to get the parser instance


# Similar Projects

## [argh](https://argh.readthedocs.io/en/latest/tutorial.html)
Expand Down
70 changes: 42 additions & 28 deletions arger/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: disable = protected-access
# pylint: disable = protected-access,unused-argument,redefined-builtin
import argparse as ap
import copy
import inspect
Expand All @@ -9,10 +9,9 @@
from arger import typing_utils as tp_utils
from arger.docstring import DocstringParser, DocstringTp, ParamDocTp

CMD_TITLE = "commands"
LEVEL = '__level__'
FUNC_PREFIX = '__func_'
NS_PREFIX = '_namespace_'
LEVEL = "__level__"
FUNC_PREFIX = "__func_"
NS_PREFIX = "_namespace_"
_EMPTY = inspect.Parameter.empty


Expand All @@ -24,7 +23,7 @@ def __init__(self, prefix: str):
self.used_short_options: tp.Set[str] = set()

def generate(self, param_name: str) -> tp.Iterator[str]:
long_flag = (self.prefix * 2) + param_name.replace('_', '-')
long_flag = (self.prefix * 2) + param_name.replace("_", "-")
for letter in param_name:
if letter not in self.used_short_options:
self.used_short_options.add(letter)
Expand Down Expand Up @@ -91,11 +90,20 @@ def __init__(
kwargs: it is delegated to `ArgumentParser.add_argument` method.
"""
for var_name, value in locals().items():
for var_name in (
"type",
"metavar",
"required",
"nargs",
"const",
"choices",
"action",
):
value = locals()[var_name]
if value is not None:
kwargs[var_name] = value
if "action" not in kwargs:
kwargs['action'] = TypeAction
kwargs["action"] = TypeAction
self.flags = flags
self.kwargs = kwargs

Expand All @@ -109,22 +117,22 @@ def create(
param: inspect.Parameter,
pdoc: tp.Optional[ParamDocTp],
option_generator: FlagsGenerator,
) -> 'Argument':
) -> "Argument":
hlp = pdoc.doc if pdoc else ""

if isinstance(param.annotation, Argument):
arg = param.annotation
else:
arg = Argument()

arg.kwargs.setdefault('help', hlp)
arg.kwargs.setdefault("help", hlp)
arg.update(param, option_generator)
return arg

def update(
self,
param: inspect.Parameter,
option_generator: tp.Optional[FlagsGenerator] = None,
option_generator: FlagsGenerator,
):
self.kind = param.kind
if param.kind == inspect.Parameter.VAR_POSITIONAL:
Expand All @@ -140,26 +148,30 @@ def _update(
if param.default is _EMPTY: # it will become a positional argument
self.flags = (param.name,)
else: # it will become a flat
self.kwargs.setdefault('dest', param.name)
self.kwargs.setdefault("dest", param.name)
if not self.flags:
self.flags = tuple(option_generator.generate(param.name))
self._update_default(param.annotation, param.default)

def _update_type(self, typ: tp.Any):
"""Update type from annotation."""
if typ is not _EMPTY:
self.kwargs.setdefault('type', typ)
if (
(typ is not _EMPTY)
and (not isinstance(typ, Argument))
and ("type" not in self.kwargs)
):
self.kwargs.setdefault("type", typ)

def _update_default(self, typ: tp.Any, default: tp.Any):
"""Update type and default externally"""
if 'default' not in self.kwargs:
if "default" not in self.kwargs:
self.kwargs["default"] = default
else:
default = self.kwargs["default"]

if isinstance(default, bool):
self.kwargs['action'] = "store_true" if default is False else "store_false"
typ = self.kwargs.pop('type', _EMPTY)
self.kwargs["action"] = "store_true" if default is False else "store_false"
typ = self.kwargs.pop("type", _EMPTY)
elif default is not None and typ is _EMPTY:
typ = type(default)

Expand All @@ -172,11 +184,11 @@ def add_to(self, parser: "Arger"):
class Arger(ap.ArgumentParser):
"""Contains one (parser) or more commands (subparsers)."""

# @functools.wraps(ap.ArgumentParser.__init__)
def __init__(
self,
func: tp.Optional[tp.Callable] = None,
version: tp.Optional[str] = None,
sub_commands_title="commands",
_doc_str: tp.Optional[DocstringTp] = None, # passed from subparser action
_level=0, # passed from subparser action
**kwargs,
Expand All @@ -193,8 +205,9 @@ def __init__(
version = '%(prog)s 2.0'
Arger() equals to Arger().add_argument('--version', action='version', version=version)
"""
kwargs.setdefault('formatter_class', ap.ArgumentDefaultsHelpFormatter)
kwargs.setdefault("formatter_class", ap.ArgumentDefaultsHelpFormatter)

self.sub_commands_title = sub_commands_title
self.sub_parser_action: tp.Optional[ap._SubParsersAction] = None

self.args: tp.Dict[str, Argument] = OrderedDict()
Expand All @@ -209,7 +222,7 @@ def __init__(
self._add_arguments(func, docstr, _level)

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

def _add_arguments(self, func: tp.Callable, docstr: DocstringTp, level: int):
option_generator = FlagsGenerator(self.prefix_chars)
Expand All @@ -220,11 +233,11 @@ def _add_arguments(self, func: tp.Callable, docstr: DocstringTp, level: int):
self.args[param.name] = Argument.create(param, param_doc, option_generator)

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

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

Expand All @@ -239,11 +252,12 @@ def run(self, *args: str, capture_sys=True) -> ap.Namespace:
namespace = self.parse_args(args)
kwargs = vars(namespace)
kwargs[NS_PREFIX] = copy.copy(namespace)
kwargs["_arger_"] = self
# dispatch all functions as in hierarchy
for level in range(kwargs.get(LEVEL, 0) + 1):
func_name = f'{FUNC_PREFIX}{level}'
func_name = f"{FUNC_PREFIX}{level}"
if func_name in kwargs:
kwargs[func_name](namespace)
kwargs[func_name](kwargs)

return namespace

Expand All @@ -267,7 +281,7 @@ def add_cmd(self, func: tp.Callable) -> ap.ArgumentParser:
func: function
"""
if not self.sub_parser_action:
self.sub_parser_action = self.add_subparsers(title=CMD_TITLE)
self.sub_parser_action = self.add_subparsers(title=self.sub_commands_title)

docstr = DocstringParser.parse(func)
return self.sub_parser_action.add_parser(
Expand All @@ -281,11 +295,11 @@ def add_cmd(self, func: tp.Callable) -> ap.ArgumentParser:
def dispatch(self, fn: tp.Callable) -> tp.Any:
"""Calls the given function with args parsed from CLI"""

def _dispatch(ns: ap.Namespace):
def _dispatch(ns: tp.Dict[str, tp.Any]):
kwargs = {}
args = []
for arg_name, arg in self.args.items():
val = getattr(ns, arg_name)
val = ns[arg_name]
if arg.kind in {
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
Expand All @@ -305,7 +319,7 @@ def get_nargs(typ: tp.Any) -> tp.Tuple[tp.Any, tp.Union[int, str]]:
if tp_utils.is_tuple(typ) and typ != tuple and tp_utils.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, "+" if (... in args) else len(args)
return inner, "*"


Expand Down
5 changes: 5 additions & 0 deletions tests/test_args_opts/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class Num(Enum):
two = '2. two'


Num2 = Enum("Num2", "one two")


@pytest.mark.parametrize(
'name, tp, input, expected',
[
Expand All @@ -21,6 +24,8 @@ class Num(Enum):
('a_str', str, 'new-str', 'new-str'),
('enum', Num, 'one', Num.one),
('enum', Num, 'two', Num.two),
('enum', Num2, 'one', Num2.one),
('enum', Num2, 'one', Num2.one),
# container types
('a_tuple', tuple, '1 2 3', ('1', '2', '3')),
('a_tuple', Tuple[int, ...], '1 2 3', (1, 2, 3)),
Expand Down

0 comments on commit 982dab6

Please sign in to comment.