|
1 | | -# aku |
| 1 | +# Aku |
2 | 2 |
|
3 | | -[](https://pypi.org/project/aku/) |
4 | | -[](https://travis-ci.org/speedcell4/aku) |
5 | | -[](https://codecov.io/gh/speedcell4/aku) |
| 3 | +[](https://github.com/speedcell4/aku/actions) |
| 4 | +[](https://badge.fury.io/py/aku) |
| 5 | +[](https://pepy.tech/project/aku) |
6 | 6 |
|
7 | | -setup your argument parser speedily |
| 7 | +An interactive annotation-driven `ArgumentParser` generator |
8 | 8 |
|
9 | | -## Installation |
| 9 | +## Requirements |
10 | 10 |
|
11 | | -```bash |
12 | | -python3.6 -m pip install aku --upgrade |
| 11 | +* Python 3.7 or higher |
| 12 | + |
| 13 | +## Install |
| 14 | + |
| 15 | +```shell script |
| 16 | +python -m pip install aku --upgrade |
13 | 17 | ``` |
14 | 18 |
|
| 19 | +## Type Annotations |
| 20 | + |
| 21 | +* primitive types, |
| 22 | + - e.g., `int`, `bool`, `str`, `float`, `Path`, etc. |
| 23 | +* container types |
| 24 | + - list `List[T]` |
| 25 | + - homogeneous tuple, e.g., `Tuple[T, ...]` |
| 26 | + - heterogeneous tuple, e.g., `Tuple[T1, T2, T3]` |
| 27 | + - literal, e.g., `Literal[42, 1905]` |
| 28 | +* nested types |
| 29 | + - function, e.g., `Type[<func_name>]` |
| 30 | + - union of functions, e.g., `Union[Type[<func1_name>], Type[<func2_name>], Type[<func3_name>]]` |
| 31 | + |
15 | 32 | ## Usage |
16 | 33 |
|
| 34 | +### Primitive Types |
| 35 | + |
| 36 | +The key idea of aku to generate `ArgumentParser` according to the type annotations of functions. For example, to register single function with only primitive types, |
| 37 | + |
17 | 38 | ```python |
18 | | -# file test_single_function.py |
19 | | -import aku |
| 39 | +from pathlib import Path |
| 40 | + |
| 41 | +from aku import Aku |
| 42 | + |
| 43 | +aku = Aku() |
20 | 44 |
|
21 | | -app = aku.Aku() |
22 | 45 |
|
| 46 | +@aku.option |
| 47 | +def foo(a: int, b: bool = True, c: str = '3', d: float = 4.0, e: Path = Path.home()): |
| 48 | + print(f'a => {a}') |
| 49 | + print(f'b => {b}') |
| 50 | + print(f'c => {c}') |
| 51 | + print(f'd => {d}') |
| 52 | + print(f'e => {e}') |
23 | 53 |
|
24 | | -@app.register |
25 | | -def add(a: int, b: int = 2): |
26 | | - print(f'{a} + {b} => {a + b}') |
27 | 54 |
|
| 55 | +aku.run() |
| 56 | +``` |
| 57 | + |
| 58 | +`aku` will generate a `ArgumentParser` which provides your command line interface looks like below, |
28 | 59 |
|
29 | | -app.run() |
| 60 | +```shell script |
| 61 | +~ python examples/foo.py --help |
| 62 | +usage: foo.py [-h] --a int [--b bool] [--c str] [--d float] [--e path] |
| 63 | + |
| 64 | +optional arguments: |
| 65 | + -h, --help show this help message and exit |
| 66 | + --a int a |
| 67 | + --b bool b (default: True) |
| 68 | + --c str c (default: 3) |
| 69 | + --d float d (default: 4.0) |
| 70 | + --e path e (default: /Users/home) |
30 | 71 | ``` |
31 | 72 |
|
32 | | -then `aku` will automatically add argument option according to your function signature. |
| 73 | +Of course you can achieve the same functions by instantiating an `ArgumentParser`, but `aku` certainly makes such steps simple and efficient. |
| 74 | + |
| 75 | +```python |
| 76 | +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, SUPPRESS |
| 77 | +from pathlib import Path |
| 78 | + |
| 79 | + |
| 80 | +def tp_bool(arg_strings: str) -> bool: |
| 81 | + arg_strings = arg_strings.lower().strip() |
| 82 | + if arg_strings in ('t', 'true', 'y', 'yes', '1'): |
| 83 | + return True |
| 84 | + if arg_strings in ('f', 'false', 'n', 'no', '0'): |
| 85 | + return False |
| 86 | + raise ValueError |
| 87 | + |
| 88 | + |
| 89 | +def foo(a: int, b: bool = True, c: str = '3', d: float = 4.0, e: Path = Path.home()): |
| 90 | + print(f'a => {a}') |
| 91 | + print(f'b => {b}') |
| 92 | + print(f'c => {c}') |
| 93 | + print(f'd => {d}') |
| 94 | + print(f'e => {e}') |
| 95 | + |
| 96 | + |
| 97 | +argument_parser = ArgumentParser( |
| 98 | + formatter_class=ArgumentDefaultsHelpFormatter, |
| 99 | +) |
| 100 | +argument_parser.add_argument('--a', type=int, metavar='int', default=SUPPRESS, required=True, help='a') |
| 101 | +argument_parser.add_argument('--b', type=tp_bool, metavar='bool', default=True, help='b') |
| 102 | +argument_parser.add_argument('--c', type=str, metavar='str', default='3', help='c') |
| 103 | +argument_parser.add_argument('--d', type=float, metavar='float', default=4.0, help='d') |
| 104 | +argument_parser.add_argument('--e', type=Path, metavar='path', default=Path.home(), help='e') |
| 105 | + |
| 106 | +args = argument_parser.parse_args() |
| 107 | +foo(a=args.a, b=args.b, c=args.c, d=args.d, e=args.e) |
| 108 | +``` |
| 109 | + |
| 110 | +Moreover, if you register more than one functions, e.g., register function `add`, |
| 111 | + |
| 112 | +```python |
| 113 | +@aku.option |
| 114 | +def add(x: int, y: int): |
| 115 | + print(f'{x} + {y} => {x + y}') |
| 116 | +``` |
| 117 | + |
| 118 | +Then you can choose which one to run by passing its name as the first parameter, |
| 119 | + |
| 120 | +```shell script |
| 121 | +~ python examples/bar.py foo --help |
| 122 | +usage: bar.py foo [-h] --a int [--b bool] [--c str] [--d float] [--e path] |
| 123 | + |
| 124 | +optional arguments: |
| 125 | + -h, --help show this help message and exit |
| 126 | + --a int a |
| 127 | + --b bool b (default: True) |
| 128 | + --c str c (default: 3) |
| 129 | + --d float d (default: 4.0) |
| 130 | + --e path e (default: /Users/home) |
33 | 131 |
|
34 | | -```shell |
35 | | -~ python tests/test_single_function.py --help |
36 | | -usage: aku [-h] --a A [--b B] |
| 132 | +~ python examples/bar.py add --help |
| 133 | +usage: bar.py add [-h] --x int --y int |
37 | 134 |
|
38 | 135 | optional arguments: |
39 | 136 | -h, --help show this help message and exit |
40 | | - --a A a (default: None) |
41 | | - --b B b (default: 2) |
| 137 | + --x int x |
| 138 | + --y int y |
42 | 139 |
|
| 140 | +~ python examples/bar.py add --x 1 --y 2 |
| 141 | +1 + 2 => 3 |
43 | 142 | ``` |
44 | 143 |
|
45 | | -if you registered more than one functions, then sub-parser will be utilized. |
| 144 | +### Container Types |
46 | 145 |
|
47 | 146 | ```python |
48 | | -# file test_multi_functions.py |
49 | | -import aku |
| 147 | +from typing import List, Tuple |
| 148 | + |
| 149 | +from aku import Aku, Literal |
| 150 | + |
| 151 | +aku = Aku() |
| 152 | + |
| 153 | + |
| 154 | +@aku.option |
| 155 | +def baz(a: List[int], b: Tuple[bool, ...], c: Tuple[int, bool, str], d: Literal[42, 1905]): |
| 156 | + print(f'a => {a}') |
| 157 | + print(f'b => {b}') |
| 158 | + print(f'c => {c}') |
| 159 | + print(f'd => {d}') |
| 160 | + |
| 161 | + |
| 162 | +if __name__ == '__main__': |
| 163 | + aku.run() |
| 164 | +``` |
50 | 165 |
|
51 | | -app = aku.App() |
| 166 | +* argument `a` is annotated with `List[int]`, thus every `--a` appends one item at the end of existing list |
| 167 | +* homogenous tuple holds arbitrary number of elements with the same type, while heterogeneous tuple holds specialized number of elements with specialized type |
| 168 | +* literal arguments can be assigned value from the specified ones |
52 | 169 |
|
| 170 | +```shell script |
| 171 | +~ python examples/baz.py --help |
| 172 | +usage: baz.py [-h] --a [int] --b bool, ...) --c (int, bool, str --d int{1905, 42} |
53 | 173 |
|
54 | | -@app.register |
55 | | -def add(a: int, b: int = 2): |
56 | | - print(f'{a} + {b} => {a + b}') |
| 174 | +optional arguments: |
| 175 | + -h, --help show this help message and exit |
| 176 | + --a [int] a |
| 177 | + --b (bool, ...) b |
| 178 | + --c (int, bool, str) c |
| 179 | + --d int{1905, 42} d |
| 180 | + |
| 181 | +~ python examples/baz.py --a 1 --a 2 --a 3 --b "true,true,false,false,true" --c 42,true,"yes" --d 42 |
| 182 | +a => [1, 2, 3] |
| 183 | +b => (True, True, False, False, True) |
| 184 | +c => (42, True, 'yes') |
| 185 | +d => 42 |
57 | 186 |
|
| 187 | +~ python examples/baz.py --a 1 --a 2 --a nice --b "true,wow" --c 42,true,"yes" --d 42 |
| 188 | +usage: baz.py [-h] [--a [int]] --b bool, ...) --c (int, bool, str --d int{1905, 42} |
| 189 | +baz.py: error: argument --a: invalid int value: 'nice' |
58 | 190 |
|
59 | | -@app.register |
60 | | -def say_hello(name: str): |
61 | | - print(f'hello {name}') |
| 191 | +~ python examples/baz.py --a 1 --a 2 --a 3 --b "true,wow" --c 42,true,"yes" --d 42 |
| 192 | +usage: baz.py [-h] [--a [int]] --b bool, ...) --c (int, bool, str --d int{1905, 42} |
| 193 | +baz.py: error: argument --b: invalid fn value: 'true,wow' |
62 | 194 |
|
| 195 | +~ python examples/baz.py --a 1 --a 2 --a 3 --b "true,true,false,false,true" --c 42,true,"yes,43" --d 42 |
| 196 | +usage: baz.py [-h] [--a [int]] [--b bool, ...)] --c (int, bool, str --d int{1905, 42} |
| 197 | +baz.py: error: argument --c: invalid fn value: '42,true,yes,43' |
63 | 198 |
|
64 | | -app.run() |
| 199 | +~ python examples/baz.py --a 1 --a 2 --a 3 --b "true,true,false,false,true" --c 42,true,"yes" --d 43 |
| 200 | +usage: baz.py [-h] [--a [int]] [--b bool, ...)] [--c (int, bool, str] --d int{1905, 42} |
| 201 | +baz.py: error: argument --d: invalid choice: 43 (choose from 42, 1905) |
65 | 202 | ``` |
66 | 203 |
|
67 | | -your argument parser interface will looks like, |
| 204 | +### Nested Types |
| 205 | +
|
| 206 | +Wrap your function in `Type` and then this can be passed as a higher-order type to annotations, then `aku` can recursively analysis them. For `Union` type, you can choose which type to run at command line interface. To avoid name conflicting, you can open a sub-namespace by adding a underline to your argument name. |
| 207 | +
|
| 208 | +```python |
| 209 | +from typing import Type, Union |
| 210 | +from aku import Aku |
| 211 | + |
| 212 | + |
| 213 | +def add(x: int, y: int): |
| 214 | + print(f'{x} + {y} => {x + y}') |
| 215 | + |
68 | 216 |
|
69 | | -```shell |
70 | | -~ python tests/test_multi_functions.py --help |
71 | | -usage: aku [-h] {add,say_hello} ... |
| 217 | +def sub(x: int, y: int): |
| 218 | + print(f'{x} - {y} => {x - y}') |
72 | 219 |
|
73 | | -positional arguments: |
74 | | - {add,say_hello} |
| 220 | + |
| 221 | +aku = Aku() |
| 222 | + |
| 223 | + |
| 224 | +@aku.option |
| 225 | +def one(op: Union[Type[add], Type[sub]]): |
| 226 | + op() |
| 227 | + |
| 228 | + |
| 229 | +@aku.option |
| 230 | +def both(lhs_: Type[add], rhs_: Type[sub]): |
| 231 | + lhs_() |
| 232 | + rhs_() |
| 233 | + |
| 234 | + |
| 235 | +if __name__ == '__main__': |
| 236 | + aku.run() |
| 237 | +``` |
| 238 | +
|
| 239 | +```shell script |
| 240 | +~ python examples/qux.py one --op add --help |
| 241 | +usage: qux.py one [-h] [--op {add, sub}[fn]] |
75 | 242 |
|
76 | 243 | optional arguments: |
77 | | - -h, --help show this help message and exit |
| 244 | + -h, --help show this help message and exit |
| 245 | + --op {add, sub}[fn] op (default: (<function add at 0x7fc1bc223700>, 'op')) |
| 246 | + --x int x |
| 247 | + --y int y |
| 248 | + |
| 249 | +~ python examples/qux.py one --op add --x 1 --y 2 |
| 250 | +1 + 2 => 3 |
78 | 251 |
|
79 | | -~ python tests/test_multi_functions.py say_hello --help |
80 | | -usage: aku say_hello [-h] --name NAME |
| 252 | +~ python examples/qux.py one --op sub --help |
| 253 | +usage: qux.py one [-h] [--op {add, sub}[fn]] |
| 254 | + |
| 255 | +optional arguments: |
| 256 | + -h, --help show this help message and exit |
| 257 | + --op {add, sub}[fn] op (default: (<function sub at 0x7ff968a2db80>, 'op')) |
| 258 | + --x int x |
| 259 | + --y int y |
| 260 | + |
| 261 | +~ python examples/qux.py one --op sub --x 1 --y 2 |
| 262 | +1 - 2 => -1 |
| 263 | + |
| 264 | +~ python examples/qux.py both --help |
| 265 | +usage: qux.py both [-h] --lhs-x int --lhs-y int --rhs-x int --rhs-y int |
81 | 266 |
|
82 | 267 | optional arguments: |
83 | 268 | -h, --help show this help message and exit |
84 | | - --name NAME name (default: None) |
| 269 | + --lhs-x int lhs-x |
| 270 | + --lhs-y int lhs-y |
| 271 | + --rhs-x int rhs-x |
| 272 | + --rhs-y int rhs-y |
85 | 273 |
|
86 | | -~ python tests/test_multi_functions.py say_hello --name aku |
87 | | -hello aku |
88 | | -``` |
| 274 | +~ python examples/qux.py both --lhs-x 1 --lhs-y 2 --rhs-x 3 --rhs-y 4 |
| 275 | +1 + 2 => 3 |
| 276 | +3 - 4 => -1 |
| 277 | +``` |
0 commit comments