Skip to content

Commit

Permalink
Support Optional[List[T]], remove explicit Nones.
Browse files Browse the repository at this point in the history
Also describe in the README what types are supported.
  • Loading branch information
neighthan committed Jun 26, 2020
1 parent 56aae8d commit 5bdca9b
Show file tree
Hide file tree
Showing 4 changed files with 25 additions and 33 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,21 @@ if __name__ == "__main__":
parse_args_and_run(func)
```

See the docstring for `auto_argparse.make_parser` for more details.
See the docstring for [`auto_argparse.make_parser`] for more details.

## Supported Types

The following types should be fully supported, but any annotation `T` should work if `T(cli_string)` gives the desired value, where `cli_string` is the string entered at the command line.
* `int`
* `float`
* `str`
* `bool`
* `List[T]`, `Sequence[T]` where `T` is any of (`int`, `float`, `str`) or as described in the paragraph above
* `Optional[T]` where `T` could additionally be `List` or `Sequence`. Note that there's no way to explicitly enter a `None` value from the command-line though it can be the default value.

## Alternatives

* [`defopt`] is a more mature library which has the same aims as `auto-argparse` but with a slightly different implementation (e.g. `auto-argparse` adds short names, makes all arguments keyword-only, and puts the part of the doc string for each argument into its help string)

[`auto_argparse.make_parser`]: auto_argparse/auto_argparse.py
[`defopt`]: https://github.com/anntzer/defopt
41 changes: 11 additions & 30 deletions auto_argparse/auto_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
from argparse import ArgumentParser, ArgumentTypeError
from collections.abc import Sequence
from typing import Callable, Optional, TypeVar, Union
from typing import Callable, TypeVar, Union

T = TypeVar("T")

Expand All @@ -17,8 +17,8 @@ def make_parser(func: Callable, add_short_args: bool = True) -> ArgumentParser:
* types: use type annotations. The only supported types from `typing` are listed below.
* `bool` uses `str2bool`; values have to be entered like `--debug True`
* `List[type]` and `Sequence[type]` will use `nargs="+", type=type`.
* `Optional[type]` converts an input `s` to None if `s.strip().lower() == "none"`.
Any other inputs are converted normally using `type`.
* `Optional[type]` converts inputs using `type`; a `None` is only possible if this
is the default value.
* defaults: just use defaults
* required params: this is just the parameters with no default values
Expand Down Expand Up @@ -51,12 +51,18 @@ def make_parser(func: Callable, add_short_args: bool = True) -> ArgumentParser:
kwargs = {}
anno = param.annotation
origin = getattr(anno, "__origin__", None)
if origin == list or origin == Sequence: # e.g. List[int]
if origin in (list, Sequence): # e.g. List[int]
kwargs["type"] = anno.__args__[0]
kwargs["nargs"] = "+"
elif origin == Union: # Optional[T] is converted to Union[T, None]
if len(anno.__args__) == 2 and anno.__args__[1] == type(None):
kwargs["type"] = make_optional(anno.__args__[0])
anno = anno.__args__[0]
origin = getattr(anno, "__origin__", None)
if origin in (list, Sequence):
kwargs["nargs"] = "+"
kwargs["type"] = anno.__args__[0]
else:
kwargs["type"] = anno
else:
if anno is not param.empty:
if anno == bool:
Expand Down Expand Up @@ -102,28 +108,3 @@ def str2bool(v: str) -> bool:
if v == "false":
return False
raise ArgumentTypeError("Boolean value expected.")


def make_optional(type_: Callable[[str], T]) -> Callable[[str], Optional[T]]:
"""
Convert `type_` into a callable which returns an instance of type T or None.
For an input `s`, if `s.strip().lower() == "none"` then None is returned.
Otherwise, `type_(s)` is returned.
"""

def parse_to_type(cli_string: str) -> Optional[T]:
return None if cli_string.strip().lower() == "none" else type_(cli_string)

# If there's an error parsing the type, argparse says
# "invalid <type> value: <value>". Saying "invalid parse_to_type value" is
# confusing, so we rename the function based on the type for clarity.
# Handle a few special cases to make things look nicer (this way we get, e.g.,
# "invalid int value" instead of "invalid <class 'int'> value").
pretty_strings = {
int: "int",
float: "float",
str: "str",
}
parse_to_type.__name__ = pretty_strings.get(type_, str(type_))

return parse_to_type
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name="auto_argparse",
version="0.0.4",
version="0.0.5",
url="https://github.com/neighthan/auto-argparse",
author="Nathan Hunt",
author_email="[email protected]",
Expand Down
2 changes: 1 addition & 1 deletion tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

def func(
x: int,
things: Sequence[int],
things: Optional[Sequence[int]] = None,
y: str = "test",
z: bool = False,
maybe: Optional[float] = 5,
Expand Down

0 comments on commit 5bdca9b

Please sign in to comment.