log21 3.3.2__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {log21-3.3.2 → log21-3.4.0}/PKG-INFO +20 -21
- {log21-3.3.2 → log21-3.4.0}/README.md +15 -17
- {log21-3.3.2 → log21-3.4.0}/pyproject.toml +5 -4
- {log21-3.3.2 → log21-3.4.0}/src/log21/__init__.py +1 -1
- {log21-3.3.2 → log21-3.4.0}/src/log21/argparse.py +31 -4
- {log21-3.3.2 → log21-3.4.0}/src/log21/argumentify.py +312 -10
- {log21-3.3.2 → log21-3.4.0}/src/log21/_argparse.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/_module_helper.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/colors.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/crash_reporter/__init__.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/crash_reporter/formatters.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/crash_reporter/reporters.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/file_handler.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/formatters.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/helper_types.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/levels.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/logger.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/logging_window.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/manager.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/pprint.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/progress_bar.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/stream_handler.py +0 -0
- {log21-3.3.2 → log21-3.4.0}/src/log21/tree_print.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: log21
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: A simple logging package
|
|
5
5
|
Keywords: python,log,colorize,color,logging,Python3,CodeWriter21
|
|
6
6
|
Author: CodeWriter21(Mehrad Pooryoussof)
|
|
@@ -18,19 +18,18 @@ Classifier: Operating System :: MacOS :: MacOS X
|
|
|
18
18
|
Requires-Dist: webcolors
|
|
19
19
|
Requires-Dist: docstring-parser
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
|
-
Project-URL: Donations, https://
|
|
22
|
-
Project-URL: Homepage, https://
|
|
23
|
-
Project-URL:
|
|
21
|
+
Project-URL: Donations, https://gitlab.com/CodeWriter21/log21/-/blob/master/DONATE.md
|
|
22
|
+
Project-URL: Homepage, https://gitlab.com/CodeWriter21/log21
|
|
23
|
+
Project-URL: Repository, https://gitlab.com/CodeWriter21/log21
|
|
24
|
+
Project-URL: Source, https://gitlab.com/CodeWriter21/log21
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
|
|
26
27
|
log21
|
|
27
28
|
=====
|
|
28
29
|
|
|
29
30
|

|
|
30
|
-

|
|
33
|
-
[](https://www.codefactor.io/repository/github/mpcodewriter21/log21)
|
|
31
|
+
[](https://gitlab.com/CodeWriter21/log21/-/commits/master)
|
|
32
|
+
[](https://gitlab.com/CodeWriter21/log21)
|
|
34
33
|
|
|
35
34
|
A simple logging package that helps you log colorized messages in Windows console and
|
|
36
35
|
other operating systems.
|
|
@@ -59,11 +58,11 @@ Features
|
|
|
59
58
|
reporter functions and use them instead!
|
|
60
59
|
+ Argumentify : You can use the argumentify feature to decrease the number of lines you
|
|
61
60
|
need to write to parse command-line arguments. It's colored by the way!
|
|
62
|
-
+ Any idea? Feel free to [open an issue](https://
|
|
63
|
-
or submit a
|
|
61
|
+
+ Any idea? Feel free to [open an issue](https://gitlab.com/CodeWriter21/log21/-/issues)
|
|
62
|
+
or submit a merge request.
|
|
64
63
|
|
|
65
|
-
](https://gitlab.com/CodeWriter21/log21/-/issues)
|
|
65
|
+
[](https://gitlab.com/CodeWriter21/log21/-/graphs/master)
|
|
67
66
|
|
|
68
67
|
Installation
|
|
69
68
|
------------
|
|
@@ -79,7 +78,7 @@ Then you can install log21 using pip module:
|
|
|
79
78
|
python -m pip install log21 -U
|
|
80
79
|
```
|
|
81
80
|
|
|
82
|
-
Or you can clone [the repository](https://
|
|
81
|
+
Or you can clone [the repository](https://gitlab.com/CodeWriter21/log21) and run:
|
|
83
82
|
|
|
84
83
|
```bash
|
|
85
84
|
pip install .
|
|
@@ -88,7 +87,7 @@ pip install .
|
|
|
88
87
|
Or let the pip get it using git:
|
|
89
88
|
|
|
90
89
|
```bash
|
|
91
|
-
pip install git+https://
|
|
90
|
+
pip install git+https://gitlab.com/CodeWriter21/log21.git
|
|
92
91
|
```
|
|
93
92
|
|
|
94
93
|
Changelog
|
|
@@ -155,25 +154,25 @@ if __name__ == "__main__":
|
|
|
155
154
|
log21.argumentify(show_percentage)
|
|
156
155
|
```
|
|
157
156
|
|
|
158
|
-
[Full CHANGELOG](https://
|
|
157
|
+
[Full CHANGELOG](https://gitlab.com/CodeWriter21/log21/-/blob/master/CHANGELOG.md)
|
|
159
158
|
|
|
160
159
|
Usage Examples
|
|
161
160
|
---------------
|
|
162
161
|
|
|
163
|
-
See [EXAMPLES.md](https://
|
|
162
|
+
See [EXAMPLES.md](https://gitlab.com/CodeWriter21/log21/-/blob/master/EXAMPLES.md)
|
|
164
163
|
|
|
165
164
|
About
|
|
166
165
|
-----
|
|
167
166
|
|
|
168
167
|
Author: CodeWriter21 (Mehrad Pooryoussof)
|
|
169
168
|
|
|
170
|
-
|
|
169
|
+
GitLab: [CodeWriter21](https://gitlab.com/CodeWriter21)
|
|
171
170
|
|
|
172
171
|
Telegram Channel: [@CodeWriter21](https://t.me/CodeWriter21)
|
|
173
172
|
|
|
174
173
|
### License
|
|
175
174
|
|
|
176
|
-
](https://gitlab.com/CodeWriter21/log21/-/blob/master/LICENSE.txt)
|
|
177
176
|
|
|
178
177
|
[apache-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
|
179
178
|
|
|
@@ -181,10 +180,10 @@ Telegram Channel: [@CodeWriter21](https://t.me/CodeWriter21)
|
|
|
181
180
|
|
|
182
181
|
In order to support this project you can donate some crypto of your choice 8D
|
|
183
182
|
|
|
184
|
-
[Donate Addresses](https://
|
|
183
|
+
[Donate Addresses](https://gitlab.com/CodeWriter21/log21/-/blob/master/DONATE.md)
|
|
185
184
|
|
|
186
|
-
Or if you can't, give [this project](https://
|
|
187
|
-
|
|
185
|
+
Or if you can't, give [this project](https://gitlab.com/CodeWriter21/log21) a star on
|
|
186
|
+
GitLab :)
|
|
188
187
|
|
|
189
188
|
References
|
|
190
189
|
----------
|
|
@@ -2,10 +2,8 @@ log21
|
|
|
2
2
|
=====
|
|
3
3
|
|
|
4
4
|

|
|
5
|
-

|
|
8
|
-
[](https://www.codefactor.io/repository/github/mpcodewriter21/log21)
|
|
5
|
+
[](https://gitlab.com/CodeWriter21/log21/-/commits/master)
|
|
6
|
+
[](https://gitlab.com/CodeWriter21/log21)
|
|
9
7
|
|
|
10
8
|
A simple logging package that helps you log colorized messages in Windows console and
|
|
11
9
|
other operating systems.
|
|
@@ -34,11 +32,11 @@ Features
|
|
|
34
32
|
reporter functions and use them instead!
|
|
35
33
|
+ Argumentify : You can use the argumentify feature to decrease the number of lines you
|
|
36
34
|
need to write to parse command-line arguments. It's colored by the way!
|
|
37
|
-
+ Any idea? Feel free to [open an issue](https://
|
|
38
|
-
or submit a
|
|
35
|
+
+ Any idea? Feel free to [open an issue](https://gitlab.com/CodeWriter21/log21/-/issues)
|
|
36
|
+
or submit a merge request.
|
|
39
37
|
|
|
40
|
-
](https://gitlab.com/CodeWriter21/log21/-/issues)
|
|
39
|
+
[](https://gitlab.com/CodeWriter21/log21/-/graphs/master)
|
|
42
40
|
|
|
43
41
|
Installation
|
|
44
42
|
------------
|
|
@@ -54,7 +52,7 @@ Then you can install log21 using pip module:
|
|
|
54
52
|
python -m pip install log21 -U
|
|
55
53
|
```
|
|
56
54
|
|
|
57
|
-
Or you can clone [the repository](https://
|
|
55
|
+
Or you can clone [the repository](https://gitlab.com/CodeWriter21/log21) and run:
|
|
58
56
|
|
|
59
57
|
```bash
|
|
60
58
|
pip install .
|
|
@@ -63,7 +61,7 @@ pip install .
|
|
|
63
61
|
Or let the pip get it using git:
|
|
64
62
|
|
|
65
63
|
```bash
|
|
66
|
-
pip install git+https://
|
|
64
|
+
pip install git+https://gitlab.com/CodeWriter21/log21.git
|
|
67
65
|
```
|
|
68
66
|
|
|
69
67
|
Changelog
|
|
@@ -130,25 +128,25 @@ if __name__ == "__main__":
|
|
|
130
128
|
log21.argumentify(show_percentage)
|
|
131
129
|
```
|
|
132
130
|
|
|
133
|
-
[Full CHANGELOG](https://
|
|
131
|
+
[Full CHANGELOG](https://gitlab.com/CodeWriter21/log21/-/blob/master/CHANGELOG.md)
|
|
134
132
|
|
|
135
133
|
Usage Examples
|
|
136
134
|
---------------
|
|
137
135
|
|
|
138
|
-
See [EXAMPLES.md](https://
|
|
136
|
+
See [EXAMPLES.md](https://gitlab.com/CodeWriter21/log21/-/blob/master/EXAMPLES.md)
|
|
139
137
|
|
|
140
138
|
About
|
|
141
139
|
-----
|
|
142
140
|
|
|
143
141
|
Author: CodeWriter21 (Mehrad Pooryoussof)
|
|
144
142
|
|
|
145
|
-
|
|
143
|
+
GitLab: [CodeWriter21](https://gitlab.com/CodeWriter21)
|
|
146
144
|
|
|
147
145
|
Telegram Channel: [@CodeWriter21](https://t.me/CodeWriter21)
|
|
148
146
|
|
|
149
147
|
### License
|
|
150
148
|
|
|
151
|
-
](https://gitlab.com/CodeWriter21/log21/-/blob/master/LICENSE.txt)
|
|
152
150
|
|
|
153
151
|
[apache-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
|
154
152
|
|
|
@@ -156,10 +154,10 @@ Telegram Channel: [@CodeWriter21](https://t.me/CodeWriter21)
|
|
|
156
154
|
|
|
157
155
|
In order to support this project you can donate some crypto of your choice 8D
|
|
158
156
|
|
|
159
|
-
[Donate Addresses](https://
|
|
157
|
+
[Donate Addresses](https://gitlab.com/CodeWriter21/log21/-/blob/master/DONATE.md)
|
|
160
158
|
|
|
161
|
-
Or if you can't, give [this project](https://
|
|
162
|
-
|
|
159
|
+
Or if you can't, give [this project](https://gitlab.com/CodeWriter21/log21) a star on
|
|
160
|
+
GitLab :)
|
|
163
161
|
|
|
164
162
|
References
|
|
165
163
|
----------
|
|
@@ -23,16 +23,17 @@ dependencies = [
|
|
|
23
23
|
"webcolors",
|
|
24
24
|
"docstring-parser"
|
|
25
25
|
]
|
|
26
|
-
version = "3.
|
|
26
|
+
version = "3.4.0"
|
|
27
27
|
|
|
28
28
|
[build-system]
|
|
29
29
|
requires = ["uv_build>=0.8.15,<0.9.0"]
|
|
30
30
|
build-backend = "uv_build"
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
|
-
Homepage = "https://
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
Homepage = "https://gitlab.com/CodeWriter21/log21"
|
|
34
|
+
Repository = "https://gitlab.com/CodeWriter21/log21"
|
|
35
|
+
Donations = "https://gitlab.com/CodeWriter21/log21/-/blob/master/DONATE.md"
|
|
36
|
+
Source = "https://gitlab.com/CodeWriter21/log21"
|
|
36
37
|
|
|
37
38
|
[tool.yapf]
|
|
38
39
|
column_limit = 88
|
|
@@ -33,7 +33,7 @@ from .stream_handler import StreamHandler, ColorizingStreamHandler
|
|
|
33
33
|
# yapf: enable
|
|
34
34
|
|
|
35
35
|
__author__ = 'CodeWriter21 (Mehrad Pooryoussof)'
|
|
36
|
-
__version__ = '3.
|
|
36
|
+
__version__ = '3.4.0'
|
|
37
37
|
__github__ = 'https://GitHub.com/MPCodeWriter21/log21'
|
|
38
38
|
__all__ = [
|
|
39
39
|
'ColorizingStreamHandler', 'DecolorizingFileHandler', 'ColorizingFormatter',
|
|
@@ -575,9 +575,23 @@ class _ActionsContainer(_argparse._ActionsContainer): # novm
|
|
|
575
575
|
and not isinstance(func_type, (_types.UnionType, tuple))):
|
|
576
576
|
raise ValueError(f'{func_type} is not callable; level={level}')
|
|
577
577
|
|
|
578
|
-
# Handle `UnionType` as a type (e.g. `int|str`)
|
|
578
|
+
# Handle `UnionType` as a type (e.g. `int|str` and `int|None`)
|
|
579
579
|
if hasattr(_types, 'UnionType') and isinstance(func_type, _types.UnionType):
|
|
580
|
-
|
|
580
|
+
# `X | None` is the PEP 604 spelling of `Optional[X]` and must
|
|
581
|
+
# behave identically: mark the action as not required and unwrap
|
|
582
|
+
# to `X`. `types.UnionType` exposes `__args__` just like
|
|
583
|
+
# `typing.Optional`, e.g. `(str | None).__args__ == (str, NoneType)`.
|
|
584
|
+
args = func_type.__args__ # type: ignore
|
|
585
|
+
if (hasattr(_types, 'NoneType') and len(args) == 2
|
|
586
|
+
and args[1] is _types.NoneType):
|
|
587
|
+
action.required = False
|
|
588
|
+
func_type = args[0]
|
|
589
|
+
elif (hasattr(_types, 'NoneType') and len(args) == 2
|
|
590
|
+
and args[0] is _types.NoneType):
|
|
591
|
+
action.required = False
|
|
592
|
+
func_type = args[1]
|
|
593
|
+
else:
|
|
594
|
+
func_type = args
|
|
581
595
|
|
|
582
596
|
# Handle `Literal` as a type (e.g. `Literal[1, 2, 3]`)
|
|
583
597
|
elif (hasattr(_typing, '_LiteralGenericAlias')
|
|
@@ -598,13 +612,17 @@ class _ActionsContainer(_argparse._ActionsContainer): # novm
|
|
|
598
612
|
else:
|
|
599
613
|
func_type = func_type.__args__ # type: ignore
|
|
600
614
|
|
|
601
|
-
# Handle `List` as a type (e.g. `List[int]`)
|
|
615
|
+
# Handle `List` as a type (e.g. `List[int]` and builtin `list[int]`)
|
|
602
616
|
elif (hasattr(_typing, '_GenericAlias')
|
|
603
617
|
and isinstance(func_type, _typing._GenericAlias) # type: ignore
|
|
604
618
|
and func_type.__origin__ is list) or (
|
|
605
619
|
hasattr(_typing, '_GenericAlias')
|
|
606
620
|
and isinstance(func_type, _typing._GenericAlias) # type: ignore
|
|
607
|
-
and func_type.__origin__ is collections.abc.Sequence)
|
|
621
|
+
and func_type.__origin__ is collections.abc.Sequence) or (
|
|
622
|
+
hasattr(_types, 'GenericAlias')
|
|
623
|
+
and isinstance(func_type, _types.GenericAlias)
|
|
624
|
+
and getattr(func_type, '__origin__', None) in
|
|
625
|
+
(list, collections.abc.Sequence)):
|
|
608
626
|
func_type = func_type.__args__[0]
|
|
609
627
|
if kwargs.get('nargs') is None:
|
|
610
628
|
action.nargs = '+'
|
|
@@ -654,6 +672,15 @@ class _ActionsContainer(_argparse._ActionsContainer): # novm
|
|
|
654
672
|
_types.UnionType,
|
|
655
673
|
))):
|
|
656
674
|
func_type = self._validate_func_type(action, func_type, kwargs, level + 1)
|
|
675
|
+
elif (hasattr(_types, 'GenericAlias')
|
|
676
|
+
and isinstance(func_type, _types.GenericAlias)
|
|
677
|
+
and getattr(func_type, '__origin__', None) in
|
|
678
|
+
(list, collections.abc.Sequence)):
|
|
679
|
+
# Builtin `list[T]` unwrapped from a PEP 604 optional (e.g.
|
|
680
|
+
# `list[str] | None`): route it through the `List` branch above.
|
|
681
|
+
# Restricted to list/Sequence origins so other generics (e.g.
|
|
682
|
+
# `dict[str, int]`) cannot recurse forever.
|
|
683
|
+
func_type = self._validate_func_type(action, func_type, kwargs, level + 1)
|
|
657
684
|
else:
|
|
658
685
|
func_type = (func_type, )
|
|
659
686
|
|
|
@@ -7,6 +7,8 @@ import re as _re
|
|
|
7
7
|
import string as _string
|
|
8
8
|
import asyncio as _asyncio
|
|
9
9
|
import inspect as _inspect
|
|
10
|
+
import types as _stdlib_types
|
|
11
|
+
import collections.abc as _collections_abc
|
|
10
12
|
from typing import (Any as _Any, Set as _Set, Dict as _Dict, List as _List,
|
|
11
13
|
Tuple as _Tuple, Union as _Union, Callable as _Callable,
|
|
12
14
|
Optional as _Optional, Awaitable as _Awaitable,
|
|
@@ -331,16 +333,231 @@ def generate_flag( # pylint: disable=too-many-branches
|
|
|
331
333
|
return flags
|
|
332
334
|
|
|
333
335
|
|
|
336
|
+
def _list_element_type(annotation: _Any) -> _Tuple[bool, _Any]:
|
|
337
|
+
"""Check whether an annotation is a `list[T]` and return its element type.
|
|
338
|
+
|
|
339
|
+
Handles `list[T]`, `typing.List[T]`, `Sequence[T]` as well as
|
|
340
|
+
`Optional[...]` / `X | None` wrapped variants of those. Bare `list`
|
|
341
|
+
(without a subscript) returns `(True, None)`, meaning no per-item
|
|
342
|
+
conversion.
|
|
343
|
+
|
|
344
|
+
:param annotation: The parameter annotation to inspect.
|
|
345
|
+
:return: A `(is_list, element_type)` tuple.
|
|
346
|
+
"""
|
|
347
|
+
if annotation is None:
|
|
348
|
+
return False, None
|
|
349
|
+
# Unwrap `Optional[X]` / `X | None` (both `typing` and PEP 604 spellings).
|
|
350
|
+
args = getattr(annotation, '__args__', None)
|
|
351
|
+
none_type = getattr(_stdlib_types, 'NoneType', None)
|
|
352
|
+
if (args is not None and none_type is not None and len(args) == 2
|
|
353
|
+
and (args[0] is none_type or args[1] is none_type)):
|
|
354
|
+
annotation = args[1] if args[0] is none_type else args[0]
|
|
355
|
+
args = getattr(annotation, '__args__', None)
|
|
356
|
+
if annotation is list:
|
|
357
|
+
return True, None
|
|
358
|
+
origin = getattr(annotation, '__origin__', None)
|
|
359
|
+
if origin is list or origin is _collections_abc.Sequence:
|
|
360
|
+
if args:
|
|
361
|
+
return True, args[0]
|
|
362
|
+
return True, None
|
|
363
|
+
if (hasattr(_stdlib_types, 'GenericAlias')
|
|
364
|
+
and isinstance(annotation, _stdlib_types.GenericAlias)
|
|
365
|
+
and getattr(annotation, '__origin__', None) in
|
|
366
|
+
(list, _collections_abc.Sequence)):
|
|
367
|
+
args = getattr(annotation, '__args__', None) or ()
|
|
368
|
+
return True, args[0] if args else None
|
|
369
|
+
return False, None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _is_repeatable_flag(argument: Argument) -> bool:
|
|
373
|
+
"""Check whether an argument maps to a repeatable `--flag` option.
|
|
374
|
+
|
|
375
|
+
Only non-positional parameters annotated with `list[T]` are repeatable
|
|
376
|
+
flags. Positional-only `list[T]` parameters become multi-value
|
|
377
|
+
positionals instead, and `*args` is handled separately.
|
|
378
|
+
"""
|
|
379
|
+
if argument.kind in (_inspect._ParameterKind.POSITIONAL_ONLY,
|
|
380
|
+
_inspect._ParameterKind.VAR_POSITIONAL,
|
|
381
|
+
_inspect._ParameterKind.VAR_KEYWORD):
|
|
382
|
+
return False
|
|
383
|
+
is_list, _ = _list_element_type(argument.annotation)
|
|
384
|
+
return is_list
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _normalize_exclusive_groups(
|
|
388
|
+
mutually_exclusive: _Any
|
|
389
|
+
) -> _List[_Tuple[_List[str], bool]]:
|
|
390
|
+
"""Normalize the `mutually_exclusive` argument into `(names, required)` groups.
|
|
391
|
+
|
|
392
|
+
Each group is either a list/tuple of parameter names (an optional group)
|
|
393
|
+
or a `{"names": [...], "required": bool}` mapping. Groups must name at
|
|
394
|
+
least two parameters.
|
|
395
|
+
|
|
396
|
+
:param mutually_exclusive: The raw `mutually_exclusive` value.
|
|
397
|
+
:raises ValueError: If a group is malformed.
|
|
398
|
+
:return: A list of `(names, required)` tuples.
|
|
399
|
+
"""
|
|
400
|
+
if mutually_exclusive is None:
|
|
401
|
+
return []
|
|
402
|
+
if isinstance(mutually_exclusive, _Dict):
|
|
403
|
+
if 'names' in mutually_exclusive:
|
|
404
|
+
# A single group passed without a wrapping list.
|
|
405
|
+
mutually_exclusive = [mutually_exclusive]
|
|
406
|
+
else:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
'mutually_exclusive must be a list of groups, not a mapping. '
|
|
409
|
+
'To target groups at specific commands, pass a '
|
|
410
|
+
'{command_name: groups} mapping as documented in `argumentify`.'
|
|
411
|
+
)
|
|
412
|
+
groups: _List[_Tuple[_List[str], bool]] = []
|
|
413
|
+
if all(isinstance(item, str) for item in mutually_exclusive):
|
|
414
|
+
raise ValueError(
|
|
415
|
+
'mutually_exclusive must be a list of groups, e.g. '
|
|
416
|
+
f'[{list(mutually_exclusive)!r}], not a bare list of names.'
|
|
417
|
+
)
|
|
418
|
+
for group in mutually_exclusive:
|
|
419
|
+
required = False
|
|
420
|
+
if isinstance(group, _Dict):
|
|
421
|
+
names = group.get('names', [])
|
|
422
|
+
required = bool(group.get('required', False))
|
|
423
|
+
elif isinstance(group, (list, tuple)):
|
|
424
|
+
names = list(group)
|
|
425
|
+
else:
|
|
426
|
+
raise ValueError(
|
|
427
|
+
'Each mutually exclusive group must be a list of parameter '
|
|
428
|
+
f'names or a {{"names": [...], "required": bool}} mapping, '
|
|
429
|
+
f'got: {group!r}'
|
|
430
|
+
)
|
|
431
|
+
names = list(names)
|
|
432
|
+
if any(not isinstance(name, str) for name in names):
|
|
433
|
+
raise ValueError(
|
|
434
|
+
'Mutually exclusive group members must be parameter names '
|
|
435
|
+
f'(strings), got: {names!r}'
|
|
436
|
+
)
|
|
437
|
+
if len(names) < 2:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
'Each mutually exclusive group must name at least two '
|
|
440
|
+
f'parameters, got: {names!r}'
|
|
441
|
+
)
|
|
442
|
+
if len(set(names)) != len(names):
|
|
443
|
+
raise ValueError(
|
|
444
|
+
'Duplicate parameter names in mutually exclusive group: '
|
|
445
|
+
f'{names!r}'
|
|
446
|
+
)
|
|
447
|
+
groups.append((names, required))
|
|
448
|
+
return groups
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _resolve_command_groups(
|
|
452
|
+
mutually_exclusive: _Any, command: _Optional[str],
|
|
453
|
+
argument_names: _Set[str]
|
|
454
|
+
) -> _List[_Tuple[_List[str], bool]]:
|
|
455
|
+
"""Resolve which exclusive groups apply to a single command parser.
|
|
456
|
+
|
|
457
|
+
`mutually_exclusive` may be a plain list of groups (broadcast to every
|
|
458
|
+
command that defines all named parameters) or a `{command_name: groups}`
|
|
459
|
+
mapping for targeted groups. Unknown parameter names and groups that
|
|
460
|
+
match no command raise `ValueError`.
|
|
461
|
+
|
|
462
|
+
:param mutually_exclusive: The raw `mutually_exclusive` value.
|
|
463
|
+
:param command: The command name, or `None` for a single-function parser.
|
|
464
|
+
:param argument_names: The parameter names of the command.
|
|
465
|
+
:raises ValueError: If a group references unknown parameters.
|
|
466
|
+
:return: The groups applying to this command.
|
|
467
|
+
"""
|
|
468
|
+
if mutually_exclusive is None:
|
|
469
|
+
return []
|
|
470
|
+
if isinstance(mutually_exclusive, _Dict):
|
|
471
|
+
if command is None:
|
|
472
|
+
raise ValueError(
|
|
473
|
+
'mutually_exclusive as a {command_name: groups} mapping is '
|
|
474
|
+
'only supported for multiple entry points; pass a plain list '
|
|
475
|
+
'of groups for a single function.'
|
|
476
|
+
)
|
|
477
|
+
if command not in mutually_exclusive:
|
|
478
|
+
return []
|
|
479
|
+
groups = _normalize_exclusive_groups(mutually_exclusive[command])
|
|
480
|
+
for names, _ in groups:
|
|
481
|
+
unknown = [name for name in names if name not in argument_names]
|
|
482
|
+
if unknown:
|
|
483
|
+
raise ValueError(
|
|
484
|
+
f'Mutually exclusive group {names!r} references unknown '
|
|
485
|
+
f'parameters of command {command!r}: {unknown!r}'
|
|
486
|
+
)
|
|
487
|
+
_check_group_overlap(groups, command)
|
|
488
|
+
return groups
|
|
489
|
+
groups = _normalize_exclusive_groups(mutually_exclusive)
|
|
490
|
+
applicable = [
|
|
491
|
+
group for group in groups
|
|
492
|
+
if all(name in argument_names for name in group[0])
|
|
493
|
+
]
|
|
494
|
+
if groups and not applicable:
|
|
495
|
+
# A broadcast list that matches nowhere is almost certainly a typo:
|
|
496
|
+
# fail fast instead of silently ignoring it.
|
|
497
|
+
missing = [
|
|
498
|
+
name for names, _ in groups for name in names
|
|
499
|
+
if name not in argument_names
|
|
500
|
+
]
|
|
501
|
+
label = f'command {command!r} ' if command is not None else ''
|
|
502
|
+
raise ValueError(
|
|
503
|
+
f'Mutually exclusive groups reference unknown parameters for '
|
|
504
|
+
f'{label}: {sorted(set(missing))!r}'
|
|
505
|
+
)
|
|
506
|
+
_check_group_overlap(applicable, command)
|
|
507
|
+
return applicable
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _check_group_overlap(
|
|
511
|
+
groups: _List[_Tuple[_List[str], bool]], command: _Optional[str]
|
|
512
|
+
) -> None:
|
|
513
|
+
"""Reject parameters shared between two groups of the same parser."""
|
|
514
|
+
seen: _Dict[str, _List[str]] = {}
|
|
515
|
+
for names, _ in groups:
|
|
516
|
+
for name in names:
|
|
517
|
+
seen.setdefault(name, []).append(names)
|
|
518
|
+
for name, owners in seen.items():
|
|
519
|
+
if len(owners) > 1:
|
|
520
|
+
label = f'command {command!r} ' if command is not None else ''
|
|
521
|
+
raise ValueError(
|
|
522
|
+
f'Parameter {name!r} for {label}is in more than one mutually '
|
|
523
|
+
'exclusive group.'
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _apply_repeatable_defaults(cli_args: _Any, info: FunctionInfo) -> None:
|
|
528
|
+
"""Restore declared `list[T]` defaults for absent repeatable flags.
|
|
529
|
+
|
|
530
|
+
Repeatable flags are registered with an internal `None` default (so
|
|
531
|
+
`argparse` never mutates the function signature's default list via
|
|
532
|
+
`action="append"`). When the flag was not used and the parameter declares
|
|
533
|
+
a default, substitute it back here.
|
|
534
|
+
"""
|
|
535
|
+
for argument in info.arguments.values():
|
|
536
|
+
if _is_repeatable_flag(argument) and getattr(
|
|
537
|
+
cli_args, argument.name, None
|
|
538
|
+
) is None and argument.default is not None:
|
|
539
|
+
setattr(cli_args, argument.name, argument.default)
|
|
540
|
+
|
|
541
|
+
|
|
334
542
|
def _add_arguments(
|
|
335
543
|
parser: _Union[_argparse.ColorizingArgumentParser, _argparse._ArgumentGroup],
|
|
336
544
|
info: FunctionInfo,
|
|
337
|
-
reserved_flags: _Optional[_Set[str]] = None
|
|
545
|
+
reserved_flags: _Optional[_Set[str]] = None,
|
|
546
|
+
exclusive_groups: _Optional[_List[_Tuple[_List[str], bool]]] = None
|
|
338
547
|
) -> None:
|
|
339
548
|
"""Add the arguments to the parser.
|
|
340
549
|
|
|
550
|
+
`list[T]` parameters become multi-value arguments: positional-only ones
|
|
551
|
+
accept several values (`nargs='+'`, or `'*'` when a default is declared),
|
|
552
|
+
while other parameters become repeatable `--flag` options
|
|
553
|
+
(`action="append"`, each occurrence converted to `T`). Parameters listed
|
|
554
|
+
in `exclusive_groups` are added to `argparse` mutually exclusive groups.
|
|
555
|
+
|
|
341
556
|
:param parser: The parser to add the arguments to.
|
|
342
557
|
:param info: The function info.
|
|
343
558
|
:param reserved_flags: The reserved flags.
|
|
559
|
+
:param exclusive_groups: Normalized `(names, required)` groups applying
|
|
560
|
+
to this parser.
|
|
344
561
|
"""
|
|
345
562
|
if reserved_flags is None:
|
|
346
563
|
reserved_flags = RESERVED_FLAGS.copy()
|
|
@@ -367,6 +584,9 @@ def _add_arguments(
|
|
|
367
584
|
)
|
|
368
585
|
|
|
369
586
|
# Add the arguments
|
|
587
|
+
containers: _Dict[int, _Any] = {}
|
|
588
|
+
for index, (_, required) in enumerate(exclusive_groups or []):
|
|
589
|
+
containers[index] = parser.add_mutually_exclusive_group(required=required)
|
|
370
590
|
for argument in info.arguments.values():
|
|
371
591
|
config: _Dict[str, _Any] = {
|
|
372
592
|
'action': 'store',
|
|
@@ -376,32 +596,69 @@ def _add_arguments(
|
|
|
376
596
|
flags = generate_flag(argument, reserved_flags=reserved_flags)
|
|
377
597
|
if argument.annotation is bool:
|
|
378
598
|
config['action'] = 'store_true'
|
|
599
|
+
elif _is_repeatable_flag(argument):
|
|
600
|
+
_, element = _list_element_type(argument.annotation)
|
|
601
|
+
config['action'] = 'append'
|
|
602
|
+
if element is not None:
|
|
603
|
+
config['type'] = element
|
|
604
|
+
# Registered with an internal `None` default so `argparse` never
|
|
605
|
+
# mutates the signature's default list; the declared default is
|
|
606
|
+
# restored by `_apply_repeatable_defaults` after parsing.
|
|
607
|
+
config['default'] = None
|
|
379
608
|
elif argument.annotation:
|
|
380
609
|
config['type'] = argument.annotation
|
|
381
610
|
if argument.kind == _inspect._ParameterKind.POSITIONAL_ONLY:
|
|
382
611
|
flags = [config.pop('dest')]
|
|
612
|
+
is_list, _ = _list_element_type(argument.annotation)
|
|
613
|
+
if is_list and config.get('nargs') is None and argument.default is not None:
|
|
614
|
+
# A declared default (e.g. `= []`) makes the multi-value
|
|
615
|
+
# positional optional instead of requiring 1+ values.
|
|
616
|
+
config['nargs'] = '*'
|
|
617
|
+
if any(argument.name in names for names, _ in exclusive_groups or []):
|
|
618
|
+
# `argparse` only accepts optional actions in mutually
|
|
619
|
+
# exclusive groups: single-value positionals become `nargs='?'`
|
|
620
|
+
# and multi-value ones `nargs='*'` (group-level `required`
|
|
621
|
+
# still enforces "exactly one" when requested).
|
|
622
|
+
if config.get('nargs') is None:
|
|
623
|
+
config['nargs'] = '?'
|
|
624
|
+
elif config.get('nargs') == '+':
|
|
625
|
+
config['nargs'] = '*'
|
|
383
626
|
if argument.kind == _inspect._ParameterKind.VAR_POSITIONAL:
|
|
384
627
|
config['nargs'] = '*'
|
|
385
628
|
flags = [config.pop('dest')]
|
|
386
629
|
if argument.kind == _inspect._ParameterKind.POSITIONAL_OR_KEYWORD and keyword_only_exists:
|
|
387
630
|
config['required'] = True
|
|
388
|
-
if argument.default is not None:
|
|
631
|
+
if argument.default is not None and config.get('action') != 'append':
|
|
389
632
|
config['default'] = argument.default
|
|
390
|
-
parser
|
|
633
|
+
target = parser
|
|
634
|
+
for index, (names, _) in enumerate(exclusive_groups or []):
|
|
635
|
+
if argument.name in names:
|
|
636
|
+
target = containers[index]
|
|
637
|
+
break
|
|
638
|
+
target.add_argument(*flags, **config)
|
|
391
639
|
|
|
392
640
|
|
|
393
|
-
def _argumentify_one(
|
|
641
|
+
def _argumentify_one(
|
|
642
|
+
func: Callable, mutually_exclusive: _Any = None
|
|
643
|
+
) -> None:
|
|
394
644
|
"""This function argumentifies one function as the entry point of the script.
|
|
395
645
|
|
|
396
646
|
:param function: The function to argumentify.
|
|
647
|
+
:param mutually_exclusive: A list of mutually exclusive groups. Each
|
|
648
|
+
group is a list of parameter names or a
|
|
649
|
+
`{"names": [...], "required": bool}` mapping.
|
|
397
650
|
"""
|
|
398
651
|
info = FunctionInfo(func)
|
|
399
652
|
|
|
400
653
|
# Create the parser
|
|
401
654
|
parser = _argparse.ColorizingArgumentParser(description=info.docstring.description)
|
|
402
655
|
# Add the arguments
|
|
403
|
-
|
|
656
|
+
groups = _resolve_command_groups(
|
|
657
|
+
mutually_exclusive, None, set(info.arguments)
|
|
658
|
+
)
|
|
659
|
+
_add_arguments(parser, info, exclusive_groups=groups)
|
|
404
660
|
cli_args = parser.parse_args()
|
|
661
|
+
_apply_repeatable_defaults(cli_args, info)
|
|
405
662
|
args = []
|
|
406
663
|
kwargs = {}
|
|
407
664
|
for argument in info.arguments.values():
|
|
@@ -421,11 +678,18 @@ def _argumentify_one(func: Callable) -> None:
|
|
|
421
678
|
parser.error(error.message)
|
|
422
679
|
|
|
423
680
|
|
|
424
|
-
def _argumentify(
|
|
681
|
+
def _argumentify(
|
|
682
|
+
functions: _Dict[str, Callable], mutually_exclusive: _Any = None
|
|
683
|
+
) -> None:
|
|
425
684
|
"""This function argumentifies one or more functions as the entry point of the
|
|
426
685
|
script.
|
|
427
686
|
|
|
428
687
|
:param functions: A dictionary of functions to argumentify.
|
|
688
|
+
:param mutually_exclusive: A list of mutually exclusive groups broadcast
|
|
689
|
+
to every command defining all named parameters, or a
|
|
690
|
+
`{command_name: groups}` mapping for targeted groups. Each group is a
|
|
691
|
+
list of parameter names or a `{"names": [...], "required": bool}`
|
|
692
|
+
mapping.
|
|
429
693
|
:raises RuntimeError:
|
|
430
694
|
"""
|
|
431
695
|
functions_info: _Dict[str, _Tuple[Callable, FunctionInfo]] = {}
|
|
@@ -445,7 +709,10 @@ def _argumentify(functions: _Dict[str, Callable]) -> None:
|
|
|
445
709
|
subparsers = parser.add_subparsers(required=True)
|
|
446
710
|
for name, (_, info) in functions_info.items():
|
|
447
711
|
subparser = subparsers.add_parser(name, help=info.docstring.description)
|
|
448
|
-
|
|
712
|
+
groups = _resolve_command_groups(
|
|
713
|
+
mutually_exclusive, name, set(info.arguments)
|
|
714
|
+
)
|
|
715
|
+
_add_arguments(subparser, info, exclusive_groups=groups)
|
|
449
716
|
subparser.set_defaults(func=info.function)
|
|
450
717
|
cli_args = parser.parse_args()
|
|
451
718
|
args = []
|
|
@@ -456,6 +723,7 @@ def _argumentify(functions: _Dict[str, Callable]) -> None:
|
|
|
456
723
|
break
|
|
457
724
|
else:
|
|
458
725
|
raise RuntimeError('No function found for the given arguments.')
|
|
726
|
+
_apply_repeatable_defaults(cli_args, info)
|
|
459
727
|
for argument in info.arguments.values():
|
|
460
728
|
if argument.kind in (_inspect._ParameterKind.POSITIONAL_ONLY,
|
|
461
729
|
_inspect._ParameterKind.POSITIONAL_OR_KEYWORD):
|
|
@@ -474,7 +742,8 @@ def _argumentify(functions: _Dict[str, Callable]) -> None:
|
|
|
474
742
|
|
|
475
743
|
|
|
476
744
|
def argumentify(
|
|
477
|
-
entry_point: _Union[Callable, _List[Callable], _Dict[str, Callable]]
|
|
745
|
+
entry_point: _Union[Callable, _List[Callable], _Dict[str, Callable]],
|
|
746
|
+
mutually_exclusive: _Optional[_Any] = None
|
|
478
747
|
) -> _Union[Callable, _List[Callable], _Dict[str, Callable]]:
|
|
479
748
|
"""This function argumentifies one or more functions as the entry point of the
|
|
480
749
|
script.
|
|
@@ -498,15 +767,48 @@ def argumentify(
|
|
|
498
767
|
$ python argumentified.py Mehrad Pooryoussof
|
|
499
768
|
Mehrad Pooryoussof is not yet born.
|
|
500
769
|
|
|
770
|
+
Parameters annotated with `list[T]` accept multiple values:
|
|
771
|
+
positional-only ones take several values (`items: list[str], /` called as
|
|
772
|
+
`prog a b c`), while other parameters become repeatable `--flag` options
|
|
773
|
+
(`defines: list[str]` called as `prog --defines a=1 --defines b=2`, each
|
|
774
|
+
occurrence converted to `T`). A declared default (e.g. `= []`) is used
|
|
775
|
+
when the argument is omitted. As with other parameters, if the function
|
|
776
|
+
also defines keyword-only parameters, positional-or-keyword `list[T]`
|
|
777
|
+
parameters become required flags; declare them keyword-only (after `*`)
|
|
778
|
+
to keep them optional.
|
|
779
|
+
|
|
780
|
+
Mutually exclusive parameters can be declared with `mutually_exclusive`.
|
|
781
|
+
Each group is a list of parameter names or a
|
|
782
|
+
`{"names": [...], "required": bool}` mapping (`required=True` means
|
|
783
|
+
exactly one of them must be given):
|
|
784
|
+
|
|
785
|
+
def run(recipe_path: str = None, /, *, builtin: str = None) -> None: ...
|
|
786
|
+
argumentify(run, mutually_exclusive=[["recipe_path", "builtin"]])
|
|
787
|
+
|
|
788
|
+
For multiple entry points, pass a `{command_name: groups}` mapping, or a
|
|
789
|
+
plain list of groups which applies to every command defining all named
|
|
790
|
+
parameters. Positional parameters in a group accept zero values (single
|
|
791
|
+
ones behave like `nargs='?'`, multi-value ones like `nargs='*'`) and
|
|
792
|
+
receive their default (or `None`) when omitted; pass
|
|
793
|
+
`{"names": [...], "required": True}` when exactly one of them must be
|
|
794
|
+
given:
|
|
795
|
+
|
|
796
|
+
argumentify(
|
|
797
|
+
{"run-recipe": run_recipe},
|
|
798
|
+
mutually_exclusive={"run-recipe": [["recipe_path", "builtin"]]},
|
|
799
|
+
)
|
|
800
|
+
|
|
501
801
|
:param entry_point: The function(s) to argumentify.
|
|
802
|
+
:param mutually_exclusive: Mutually exclusive parameter groups.
|
|
502
803
|
:raises TypeError: A function must be a function or a list of functions or a
|
|
503
804
|
dictionary of functions.
|
|
805
|
+
:raises ValueError: A group is malformed or references unknown parameters.
|
|
504
806
|
"""
|
|
505
807
|
|
|
506
808
|
functions = {}
|
|
507
809
|
# Check the types
|
|
508
810
|
if callable(entry_point):
|
|
509
|
-
_argumentify_one(entry_point)
|
|
811
|
+
_argumentify_one(entry_point, mutually_exclusive)
|
|
510
812
|
return entry_point
|
|
511
813
|
if isinstance(entry_point, _List):
|
|
512
814
|
for func in entry_point:
|
|
@@ -530,5 +832,5 @@ def argumentify(
|
|
|
530
832
|
"dictionary of functions."
|
|
531
833
|
)
|
|
532
834
|
|
|
533
|
-
_argumentify(functions)
|
|
835
|
+
_argumentify(functions, mutually_exclusive)
|
|
534
836
|
return entry_point
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|