moat-lib-run 0.1.1__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.
- moat_lib_run-0.1.1/LICENSE.txt +14 -0
- moat_lib_run-0.1.1/Makefile +14 -0
- moat_lib_run-0.1.1/PKG-INFO +97 -0
- moat_lib_run-0.1.1/README.md +72 -0
- moat_lib_run-0.1.1/debian/.gitignore +7 -0
- moat_lib_run-0.1.1/debian/changelog +5 -0
- moat_lib_run-0.1.1/debian/control +22 -0
- moat_lib_run-0.1.1/debian/rules +5 -0
- moat_lib_run-0.1.1/pyproject.toml +39 -0
- moat_lib_run-0.1.1/setup.cfg +4 -0
- moat_lib_run-0.1.1/src/moat/lib/run/__init__.py +947 -0
- moat_lib_run-0.1.1/src/moat_lib_run.egg-info/PKG-INFO +97 -0
- moat_lib_run-0.1.1/src/moat_lib_run.egg-info/SOURCES.txt +14 -0
- moat_lib_run-0.1.1/src/moat_lib_run.egg-info/dependency_links.txt +1 -0
- moat_lib_run-0.1.1/src/moat_lib_run.egg-info/requires.txt +6 -0
- moat_lib_run-0.1.1/src/moat_lib_run.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
The code in this repository, and all MoaT submodules it refers to,
|
|
2
|
+
is part of the MoaT project.
|
|
3
|
+
|
|
4
|
+
Unless a submodule's LICENSE.txt states otherwise, all included files are
|
|
5
|
+
licensed under the LGPL V3, as published by the FSF at
|
|
6
|
+
https://www.gnu.org/licenses/lgpl-3.0.html .
|
|
7
|
+
|
|
8
|
+
In addition to the LGPL's terms, the author(s) respectfully ask all users of
|
|
9
|
+
this code to contribute any bug fixes or enhancements. Also, please link back to
|
|
10
|
+
https://M-o-a-T.org.
|
|
11
|
+
|
|
12
|
+
Thank you.
|
|
13
|
+
|
|
14
|
+
Copyright © 2021 ff.: the MoaT contributor(s), as per the git changelog(s).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/make -f
|
|
2
|
+
|
|
3
|
+
PACKAGE = moat-lib-run
|
|
4
|
+
MAKEINCL ?= $(shell python3 -mmoat src path)/make/py
|
|
5
|
+
|
|
6
|
+
ifneq ($(wildcard $(MAKEINCL)),)
|
|
7
|
+
include $(MAKEINCL)
|
|
8
|
+
# availabe via http://github.com/smurfix/sourcemgr
|
|
9
|
+
|
|
10
|
+
else
|
|
11
|
+
%:
|
|
12
|
+
@echo "Please fix 'python3 -mmoat src path'."
|
|
13
|
+
@exit 1
|
|
14
|
+
endif
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moat-lib-run
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Main command entry point infrastructure for MoaT applications
|
|
5
|
+
Maintainer-email: Matthias Urlichs <matthias@urlichs.de>
|
|
6
|
+
Project-URL: homepage, https://m-o-a-t.org
|
|
7
|
+
Project-URL: repository, https://github.com/M-o-a-T/moat
|
|
8
|
+
Keywords: MoaT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Framework :: AnyIO
|
|
11
|
+
Classifier: Framework :: Trio
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: anyio~=4.0
|
|
19
|
+
Requires-Dist: moat-util~=0.61.1
|
|
20
|
+
Requires-Dist: asyncclick~=8.0
|
|
21
|
+
Requires-Dist: simpleeval~=1.0
|
|
22
|
+
Requires-Dist: moat-lib-config~=0.1.0
|
|
23
|
+
Requires-Dist: moat-lib-proxy~=0.1.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# moat-lib-run
|
|
27
|
+
|
|
28
|
+
% start main
|
|
29
|
+
% start synopsis
|
|
30
|
+
|
|
31
|
+
Main command entry point infrastructure for MoaT applications.
|
|
32
|
+
|
|
33
|
+
% end synopsis
|
|
34
|
+
|
|
35
|
+
This module provides the infrastructure for building command-line interfaces
|
|
36
|
+
for MoaT applications. It includes:
|
|
37
|
+
|
|
38
|
+
- Command-line argument parsing with Click integration
|
|
39
|
+
- Subcommand loading from internal modules and extensions
|
|
40
|
+
- Configuration file handling
|
|
41
|
+
- Logging setup
|
|
42
|
+
- Main entry point wrappers for testing
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Basic command setup
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from moat.lib.run import main_, wrap_main
|
|
50
|
+
|
|
51
|
+
@main_.command()
|
|
52
|
+
async def my_command(ctx):
|
|
53
|
+
"""A simple command"""
|
|
54
|
+
print("Hello from my command!")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Loading subcommands
|
|
58
|
+
|
|
59
|
+
Use `load_subgroup` to create command groups that automatically load subcommands:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from moat.lib.run import load_subgroup
|
|
63
|
+
import asyncclick as click
|
|
64
|
+
|
|
65
|
+
@load_subgroup(prefix="myapp.commands")
|
|
66
|
+
@click.pass_context
|
|
67
|
+
async def cli(ctx):
|
|
68
|
+
"""Main command group"""
|
|
69
|
+
pass
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Processing command-line arguments
|
|
73
|
+
|
|
74
|
+
The `attr_args` decorator and `process_args` function provide flexible
|
|
75
|
+
argument handling:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from moat.lib.run import attr_args, process_args
|
|
79
|
+
|
|
80
|
+
@main_.command()
|
|
81
|
+
@attr_args(with_path=True)
|
|
82
|
+
async def configure(**kw):
|
|
83
|
+
"""Configure the application"""
|
|
84
|
+
config = process_args({}, **kw)
|
|
85
|
+
# config now contains parsed arguments
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Key Functions
|
|
89
|
+
|
|
90
|
+
- `main_`: The default main command handler
|
|
91
|
+
- `wrap_main`: Wrapper for the main command, useful for testing
|
|
92
|
+
- `load_subgroup`: Decorator to create command groups with automatic subcommand loading
|
|
93
|
+
- `attr_args`: Decorator for adding flexible argument handling to commands
|
|
94
|
+
- `process_args`: Function to process command-line arguments into configuration
|
|
95
|
+
- `Loader`: Click group class that loads commands from submodules and extensions
|
|
96
|
+
|
|
97
|
+
% end main
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# moat-lib-run
|
|
2
|
+
|
|
3
|
+
% start main
|
|
4
|
+
% start synopsis
|
|
5
|
+
|
|
6
|
+
Main command entry point infrastructure for MoaT applications.
|
|
7
|
+
|
|
8
|
+
% end synopsis
|
|
9
|
+
|
|
10
|
+
This module provides the infrastructure for building command-line interfaces
|
|
11
|
+
for MoaT applications. It includes:
|
|
12
|
+
|
|
13
|
+
- Command-line argument parsing with Click integration
|
|
14
|
+
- Subcommand loading from internal modules and extensions
|
|
15
|
+
- Configuration file handling
|
|
16
|
+
- Logging setup
|
|
17
|
+
- Main entry point wrappers for testing
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Basic command setup
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from moat.lib.run import main_, wrap_main
|
|
25
|
+
|
|
26
|
+
@main_.command()
|
|
27
|
+
async def my_command(ctx):
|
|
28
|
+
"""A simple command"""
|
|
29
|
+
print("Hello from my command!")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Loading subcommands
|
|
33
|
+
|
|
34
|
+
Use `load_subgroup` to create command groups that automatically load subcommands:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from moat.lib.run import load_subgroup
|
|
38
|
+
import asyncclick as click
|
|
39
|
+
|
|
40
|
+
@load_subgroup(prefix="myapp.commands")
|
|
41
|
+
@click.pass_context
|
|
42
|
+
async def cli(ctx):
|
|
43
|
+
"""Main command group"""
|
|
44
|
+
pass
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Processing command-line arguments
|
|
48
|
+
|
|
49
|
+
The `attr_args` decorator and `process_args` function provide flexible
|
|
50
|
+
argument handling:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from moat.lib.run import attr_args, process_args
|
|
54
|
+
|
|
55
|
+
@main_.command()
|
|
56
|
+
@attr_args(with_path=True)
|
|
57
|
+
async def configure(**kw):
|
|
58
|
+
"""Configure the application"""
|
|
59
|
+
config = process_args({}, **kw)
|
|
60
|
+
# config now contains parsed arguments
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Key Functions
|
|
64
|
+
|
|
65
|
+
- `main_`: The default main command handler
|
|
66
|
+
- `wrap_main`: Wrapper for the main command, useful for testing
|
|
67
|
+
- `load_subgroup`: Decorator to create command groups with automatic subcommand loading
|
|
68
|
+
- `attr_args`: Decorator for adding flexible argument handling to commands
|
|
69
|
+
- `process_args`: Function to process command-line arguments into configuration
|
|
70
|
+
- `Loader`: Click group class that loads commands from submodules and extensions
|
|
71
|
+
|
|
72
|
+
% end main
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Source: moat-lib-run
|
|
2
|
+
Maintainer: "Matthias Urlichs" <matthias@urlichs.de>
|
|
3
|
+
Section: python
|
|
4
|
+
Priority: optional
|
|
5
|
+
Build-Depends: dh-python, python3-all, debhelper (>= 13),
|
|
6
|
+
python3-setuptools,
|
|
7
|
+
python3-wheel,
|
|
8
|
+
Standards-Version: 3.9.6
|
|
9
|
+
Homepage: https://m-o-a-t.org
|
|
10
|
+
X-DH-Compat: 13
|
|
11
|
+
|
|
12
|
+
Package: python3-moat-lib-run
|
|
13
|
+
Architecture: all
|
|
14
|
+
Depends: ${misc:Depends}, ${python3:Depends},
|
|
15
|
+
python3-anyio (>= 4.0),
|
|
16
|
+
python3-moat-util (>= 0.6),
|
|
17
|
+
python3-asyncclick (>= 8.0),
|
|
18
|
+
python3-simpleeval (>= 1.0),
|
|
19
|
+
Description: Main command entry point infrastructure for MoaT applications
|
|
20
|
+
This module provides the infrastructure for building command-line interfaces
|
|
21
|
+
for MoaT applications, including command-line argument parsing, subcommand
|
|
22
|
+
loading, configuration file handling, and logging setup.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = ["wheel","setuptools"]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
classifiers = [
|
|
7
|
+
"Development Status :: 4 - Beta",
|
|
8
|
+
"Framework :: AnyIO",
|
|
9
|
+
"Framework :: Trio",
|
|
10
|
+
"Framework :: AsyncIO",
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"anyio ~= 4.0",
|
|
16
|
+
"moat-util ~= 0.61.1",
|
|
17
|
+
"asyncclick ~= 8.0",
|
|
18
|
+
"simpleeval ~= 1.0",
|
|
19
|
+
"moat-lib-config ~= 0.1.0",
|
|
20
|
+
"moat-lib-proxy ~= 0.1.0",
|
|
21
|
+
]
|
|
22
|
+
keywords = ["MoaT"]
|
|
23
|
+
requires-python = ">=3.8"
|
|
24
|
+
name = "moat-lib-run"
|
|
25
|
+
maintainers = [{email = "matthias@urlichs.de",name = "Matthias Urlichs"}]
|
|
26
|
+
description='Main command entry point infrastructure for MoaT applications'
|
|
27
|
+
readme = "README.md"
|
|
28
|
+
version = "0.1.1"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
homepage = "https://m-o-a-t.org"
|
|
32
|
+
repository = "https://github.com/M-o-a-T/moat"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
"*" = ["*.yaml"]
|
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Support for main program, argv, etc.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import importlib
|
|
9
|
+
import logging
|
|
10
|
+
import logging.config
|
|
11
|
+
import sys
|
|
12
|
+
from contextlib import suppress
|
|
13
|
+
from contextvars import ContextVar
|
|
14
|
+
from functools import partial, wraps
|
|
15
|
+
from pathlib import Path as FSPath
|
|
16
|
+
|
|
17
|
+
import asyncclick as click
|
|
18
|
+
import simpleeval
|
|
19
|
+
|
|
20
|
+
from moat.util import NotGiven, P, Path, attrdict, ungroup
|
|
21
|
+
from moat.lib.config import CFG, CfgStore, current_cfg
|
|
22
|
+
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
from collections.abc import Mapping
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from moat.lib.proxy import Proxy
|
|
28
|
+
except ImportError:
|
|
29
|
+
Proxy = None
|
|
30
|
+
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from typing import Awaitable, Literal, Sequence # noqa:UP035
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("_loader")
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Loader",
|
|
40
|
+
"attr_args",
|
|
41
|
+
"list_ext",
|
|
42
|
+
"load_ext",
|
|
43
|
+
"load_subgroup",
|
|
44
|
+
"main_",
|
|
45
|
+
"option_ng",
|
|
46
|
+
"process_args",
|
|
47
|
+
"wrap_main",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
this_load = ContextVar("this_load", default=None)
|
|
51
|
+
|
|
52
|
+
NoneType = type(None)
|
|
53
|
+
|
|
54
|
+
# cmd_eval is a simple and safe "eval" replacement.
|
|
55
|
+
_eval = simpleeval.SimpleEval(functions={})
|
|
56
|
+
_eval.nodes[ast.Tuple] = lambda node: tuple( # pyright: ignore[reportOptionalSubscript]
|
|
57
|
+
_eval._eval(x) for x in node.elts
|
|
58
|
+
)
|
|
59
|
+
_eval.nodes[ast.List] = lambda node: list( # pyright: ignore[reportOptionalSubscript]
|
|
60
|
+
_eval._eval(x) for x in node.elts
|
|
61
|
+
)
|
|
62
|
+
_eval.nodes[ast.Dict] = lambda node: attrdict( # pyright: ignore[reportOptionalSubscript]
|
|
63
|
+
(_eval._eval(x), _eval._eval(y)) for x, y in zip(node.keys, node.values, strict=False)
|
|
64
|
+
)
|
|
65
|
+
cmd_eval = _eval.eval
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _no_config(*a, **k): # noqa:ARG001
|
|
69
|
+
import warnings # noqa: PLC0415
|
|
70
|
+
|
|
71
|
+
warnings.warn("Call to logging config ignored", stacklevel=2)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def attr_args(
|
|
75
|
+
proc=None,
|
|
76
|
+
with_combined="s",
|
|
77
|
+
with_arglist=False,
|
|
78
|
+
with_path=True,
|
|
79
|
+
with_eval=True,
|
|
80
|
+
with_var=True,
|
|
81
|
+
with_proxy=False,
|
|
82
|
+
par_name="Parameter",
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Add an option for setting possibly-hierarchical values to `click.command`.
|
|
86
|
+
|
|
87
|
+
``-s``/``--set`` accepts these prefix-tagged values:
|
|
88
|
+
* ~str
|
|
89
|
+
* =value (``=-``/``=t``/``=f``/``=n`` for ``undefined``/`True`/`False`/`None`)
|
|
90
|
+
* .path (the leading dot is stripped)
|
|
91
|
+
* :path (the colon is not stripped)
|
|
92
|
+
* ^named_proxy
|
|
93
|
+
|
|
94
|
+
These are enabled (rather, displayed) by passing ``True`` in
|
|
95
|
+
``with_val``, ``with_eval``, ``with_path``, and ``with_proxy``,
|
|
96
|
+
respectively. The latter defaults to `False`, all others to `True`.
|
|
97
|
+
|
|
98
|
+
Legacy behavior (hidden unless `with_combined` is False): Adds separate
|
|
99
|
+
``-v``/``--var``, ``-e``/``--eval``, -p``/``--path, and
|
|
100
|
+
``-P``/``--proxy`` arguments.
|
|
101
|
+
|
|
102
|
+
Use ``with_combined=False`` to get legacy behavior.
|
|
103
|
+
Use ``with_combined=LETTER`` to change the default from ``-s``.
|
|
104
|
+
|
|
105
|
+
In new mode, Legacy short options are not availabile;
|
|
106
|
+
long options are available but hidden.
|
|
107
|
+
|
|
108
|
+
All arguments are of the form "-X path value". A path consisting of a
|
|
109
|
+
single ``+`` expands to ``:n:n`` for easy appending to argument lists.
|
|
110
|
+
Use ``:=`` if you ever need a path that consist of a single plus character.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def _proc(proc):
|
|
114
|
+
if with_combined:
|
|
115
|
+
ht = []
|
|
116
|
+
if with_var:
|
|
117
|
+
ht.append("~str")
|
|
118
|
+
if with_eval:
|
|
119
|
+
ht.append("=expr")
|
|
120
|
+
if with_path:
|
|
121
|
+
ht.append(".path, :path")
|
|
122
|
+
if with_proxy:
|
|
123
|
+
ht.append("^proxy")
|
|
124
|
+
ht = " | ".join(ht)
|
|
125
|
+
|
|
126
|
+
args = ("--set",) + (
|
|
127
|
+
("-" + ("s" if isinstance(with_combined, bool) else with_combined),)
|
|
128
|
+
if with_combined
|
|
129
|
+
else ()
|
|
130
|
+
)
|
|
131
|
+
proc = click.option(
|
|
132
|
+
*args,
|
|
133
|
+
"set_",
|
|
134
|
+
nargs=2,
|
|
135
|
+
type=(str, str),
|
|
136
|
+
multiple=True,
|
|
137
|
+
metavar="name|path value",
|
|
138
|
+
help=f"{par_name} (value: {ht})",
|
|
139
|
+
hidden=not with_eval or not with_combined,
|
|
140
|
+
)(proc)
|
|
141
|
+
|
|
142
|
+
if with_arglist:
|
|
143
|
+
proc = click.argument(
|
|
144
|
+
"args_",
|
|
145
|
+
nargs=-1,
|
|
146
|
+
type=str,
|
|
147
|
+
)(proc)
|
|
148
|
+
|
|
149
|
+
args = (f"--{with_path}" if isinstance(with_path, str) else "--path",) + (
|
|
150
|
+
("-p",) if with_path else ()
|
|
151
|
+
)
|
|
152
|
+
proc = click.option(
|
|
153
|
+
*args,
|
|
154
|
+
"path_",
|
|
155
|
+
nargs=2,
|
|
156
|
+
type=(P, P),
|
|
157
|
+
multiple=True,
|
|
158
|
+
help=f"{par_name} (name value), as path",
|
|
159
|
+
hidden=not with_path or with_combined,
|
|
160
|
+
)(proc)
|
|
161
|
+
|
|
162
|
+
args = (f"--{with_eval}" if isinstance(with_eval, str) else "--eval",) + (
|
|
163
|
+
("-e",) if with_eval is True else ()
|
|
164
|
+
)
|
|
165
|
+
proc = click.option(
|
|
166
|
+
*args,
|
|
167
|
+
"eval_",
|
|
168
|
+
nargs=2,
|
|
169
|
+
type=(P, str),
|
|
170
|
+
multiple=True,
|
|
171
|
+
help=f"{par_name} (name value), evaluated",
|
|
172
|
+
hidden=not with_eval or with_combined,
|
|
173
|
+
)(proc)
|
|
174
|
+
|
|
175
|
+
args = (f"--{with_var}" if isinstance(with_var, str) else "--var",) + (
|
|
176
|
+
("-v",) if with_var is True else ()
|
|
177
|
+
)
|
|
178
|
+
proc = click.option(
|
|
179
|
+
*args,
|
|
180
|
+
"vars_",
|
|
181
|
+
nargs=2,
|
|
182
|
+
type=(P, str),
|
|
183
|
+
multiple=True,
|
|
184
|
+
help=f"{par_name} (name value)",
|
|
185
|
+
hidden=not with_var or with_combined,
|
|
186
|
+
)(proc)
|
|
187
|
+
|
|
188
|
+
args = (f"--{with_proxy}" if isinstance(with_proxy, str) else "--proxy",) + (
|
|
189
|
+
("-P",) if with_proxy is True else ()
|
|
190
|
+
)
|
|
191
|
+
proc = click.option(
|
|
192
|
+
*args,
|
|
193
|
+
"proxy_",
|
|
194
|
+
nargs=2,
|
|
195
|
+
type=(P, str),
|
|
196
|
+
multiple=True,
|
|
197
|
+
help="Remote proxy (name value)",
|
|
198
|
+
hidden=not with_proxy or with_combined,
|
|
199
|
+
)(proc)
|
|
200
|
+
|
|
201
|
+
return proc
|
|
202
|
+
|
|
203
|
+
if proc is None:
|
|
204
|
+
return _proc
|
|
205
|
+
else:
|
|
206
|
+
return _proc(proc)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def process_args(
|
|
210
|
+
val: dict | None = None,
|
|
211
|
+
set_=(),
|
|
212
|
+
args_=(),
|
|
213
|
+
vars_=(),
|
|
214
|
+
eval_=(),
|
|
215
|
+
path_=(),
|
|
216
|
+
proxy_=(),
|
|
217
|
+
no_path=False,
|
|
218
|
+
vs=None,
|
|
219
|
+
):
|
|
220
|
+
"""
|
|
221
|
+
process ``set_``/``args_``/``vars_``/``eval_``/``path_``/``proxy_`` args.
|
|
222
|
+
|
|
223
|
+
Arguments:
|
|
224
|
+
val: dict to modify
|
|
225
|
+
set_, vars_, args_, eval_, path_, proxy_: via `attr_args`
|
|
226
|
+
vs: if given: set of vars
|
|
227
|
+
Returns:
|
|
228
|
+
the new value.
|
|
229
|
+
"""
|
|
230
|
+
# otherwise these are assumes to be empty tuples.
|
|
231
|
+
if isinstance(set_, Mapping):
|
|
232
|
+
set_ = set_.items()
|
|
233
|
+
if isinstance(vars_, Mapping):
|
|
234
|
+
vars_ = vars_.items()
|
|
235
|
+
if isinstance(eval_, Mapping):
|
|
236
|
+
eval_ = eval_.items()
|
|
237
|
+
if isinstance(path_, Mapping):
|
|
238
|
+
path_ = path_.items()
|
|
239
|
+
if isinstance(proxy_, Mapping):
|
|
240
|
+
proxy_ = proxy_.items()
|
|
241
|
+
|
|
242
|
+
def data():
|
|
243
|
+
def s_eval(v):
|
|
244
|
+
if v[0] == "~":
|
|
245
|
+
v = v[1:]
|
|
246
|
+
elif v == "=-":
|
|
247
|
+
v = NotGiven
|
|
248
|
+
elif v == "=t":
|
|
249
|
+
v = True
|
|
250
|
+
elif v == "=f":
|
|
251
|
+
v = False
|
|
252
|
+
elif v == "=n":
|
|
253
|
+
v = None
|
|
254
|
+
elif v[0] == "=":
|
|
255
|
+
v = cmd_eval(v[1:]) # pylint: disable=W0631
|
|
256
|
+
elif v[0] == ":":
|
|
257
|
+
v = P(v)
|
|
258
|
+
elif v[0] == ".":
|
|
259
|
+
v = P(v[1:])
|
|
260
|
+
elif v[0] == "^":
|
|
261
|
+
v = Proxy(v[1:])
|
|
262
|
+
else:
|
|
263
|
+
try:
|
|
264
|
+
v = int(v)
|
|
265
|
+
except ValueError:
|
|
266
|
+
try:
|
|
267
|
+
v = float(v)
|
|
268
|
+
except ValueError:
|
|
269
|
+
pass # leave it as a string
|
|
270
|
+
return v
|
|
271
|
+
|
|
272
|
+
for k, v in set_:
|
|
273
|
+
yield k, s_eval(v)
|
|
274
|
+
for k, v in vars_:
|
|
275
|
+
yield k, v
|
|
276
|
+
for k, v in eval_:
|
|
277
|
+
# ruff:noqa:PLW2901 # var overwritten
|
|
278
|
+
if v == "-":
|
|
279
|
+
v = NotGiven
|
|
280
|
+
elif v == "/": # pylint: disable=W0631
|
|
281
|
+
if vs is None:
|
|
282
|
+
raise click.BadOptionUsage(
|
|
283
|
+
option_name=k,
|
|
284
|
+
message="A slash value doesn't work here.",
|
|
285
|
+
)
|
|
286
|
+
v = NoneType
|
|
287
|
+
else:
|
|
288
|
+
v = eval(v) # pylint: disable=W0631
|
|
289
|
+
yield k, v
|
|
290
|
+
for k, v in path_:
|
|
291
|
+
v = P(v)
|
|
292
|
+
if no_path:
|
|
293
|
+
v = tuple(v)
|
|
294
|
+
yield k, v
|
|
295
|
+
if proxy_:
|
|
296
|
+
if Proxy is None:
|
|
297
|
+
raise ImportError("No Proxy")
|
|
298
|
+
for k, v in proxy_:
|
|
299
|
+
v = Proxy(v)
|
|
300
|
+
yield k, v
|
|
301
|
+
|
|
302
|
+
# Arguments are given last, thus they get processed last.
|
|
303
|
+
for v in args_:
|
|
304
|
+
yield ((None, None), s_eval(v))
|
|
305
|
+
|
|
306
|
+
if set_:
|
|
307
|
+
dd = data()
|
|
308
|
+
else:
|
|
309
|
+
dd = [(len(k), k, v) for k, v in data()]
|
|
310
|
+
dd.sort()
|
|
311
|
+
dd = [(k, v) for _l, k, v in dd]
|
|
312
|
+
|
|
313
|
+
for k, v in dd:
|
|
314
|
+
if isinstance(k, str):
|
|
315
|
+
if k == "+":
|
|
316
|
+
k = Path.build((None, None))
|
|
317
|
+
else:
|
|
318
|
+
k = P(k)
|
|
319
|
+
if val is None:
|
|
320
|
+
CFG.mod(k, v)
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
if not len(k):
|
|
324
|
+
if vs is not None:
|
|
325
|
+
raise click.BadOptionUsage(
|
|
326
|
+
option_name=k,
|
|
327
|
+
message="You can't use empty paths here.",
|
|
328
|
+
)
|
|
329
|
+
val = v
|
|
330
|
+
continue
|
|
331
|
+
if not isinstance(val, Mapping):
|
|
332
|
+
val = attrdict()
|
|
333
|
+
if vs is not None:
|
|
334
|
+
vs.add(str(k))
|
|
335
|
+
if v is NotGiven:
|
|
336
|
+
val = attrdict._delete(val, k) # pylint: disable=protected-access
|
|
337
|
+
elif v is NoneType:
|
|
338
|
+
val = attrdict._delete(val, k) # pylint: disable=protected-access
|
|
339
|
+
vs.discard(str(k))
|
|
340
|
+
else:
|
|
341
|
+
val = attrdict._update(val, k, v) # pylint: disable=protected-access
|
|
342
|
+
return val
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def load_ext(name, *attr, err=False):
|
|
346
|
+
"""
|
|
347
|
+
Load a module
|
|
348
|
+
"""
|
|
349
|
+
path = name.split(".")
|
|
350
|
+
path.extend(attr[:-1])
|
|
351
|
+
dp = ".".join(path)
|
|
352
|
+
".".join(path[:-1])
|
|
353
|
+
try:
|
|
354
|
+
mod = importlib.import_module(dp)
|
|
355
|
+
except ModuleNotFoundError as exc:
|
|
356
|
+
if err and not exc.name.endswith("._main"):
|
|
357
|
+
logger.debug("Err %s: %r", dp, exc, exc_info=exc)
|
|
358
|
+
return None
|
|
359
|
+
except FileNotFoundError:
|
|
360
|
+
if err:
|
|
361
|
+
raise
|
|
362
|
+
return None
|
|
363
|
+
else:
|
|
364
|
+
if attr:
|
|
365
|
+
try:
|
|
366
|
+
mod = getattr(mod, attr[-1])
|
|
367
|
+
except AttributeError:
|
|
368
|
+
logger.debug("Err %s.%s", dp, attr[-1])
|
|
369
|
+
return None
|
|
370
|
+
return mod
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _namespaces(name):
|
|
374
|
+
import pkgutil # pylint: disable=import-outside-toplevel # noqa: PLC0415
|
|
375
|
+
|
|
376
|
+
if name is NotGiven:
|
|
377
|
+
return ()
|
|
378
|
+
try:
|
|
379
|
+
ext = importlib.import_module(name)
|
|
380
|
+
except ModuleNotFoundError:
|
|
381
|
+
logger.debug("No NS: %s", name)
|
|
382
|
+
return ()
|
|
383
|
+
try:
|
|
384
|
+
p = ext.__path__
|
|
385
|
+
except AttributeError:
|
|
386
|
+
p = (str(FSPath(ext.__file__).parent),)
|
|
387
|
+
logger.debug("NS: %s %s", name, p)
|
|
388
|
+
return pkgutil.iter_modules(p, ext.__name__ + ".")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
_ext_cache = defaultdict(dict)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _cache_ext(ext_name, pkg_only):
|
|
395
|
+
"""List external modules
|
|
396
|
+
|
|
397
|
+
Yields (name,path) tuples.
|
|
398
|
+
|
|
399
|
+
TODO: This is not zip safe.
|
|
400
|
+
"""
|
|
401
|
+
for finder, name, ispkg in _namespaces(ext_name):
|
|
402
|
+
if pkg_only and not ispkg:
|
|
403
|
+
logger.debug("ExtNoC %s", name)
|
|
404
|
+
continue
|
|
405
|
+
logger.debug("ExtC %s", name)
|
|
406
|
+
x = name.rsplit(".", 1)[-1]
|
|
407
|
+
f = FSPath(finder.path) / x
|
|
408
|
+
_ext_cache[ext_name][x] = f
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def list_ext(name, func=None, pkg_only=True):
|
|
412
|
+
"""List external modules
|
|
413
|
+
|
|
414
|
+
Yields (name,path) tuples.
|
|
415
|
+
|
|
416
|
+
TODO: This is not zip safe.
|
|
417
|
+
"""
|
|
418
|
+
logger.debug("List Ext %s (%s)", name, func)
|
|
419
|
+
if name not in _ext_cache:
|
|
420
|
+
with suppress(ModuleNotFoundError):
|
|
421
|
+
_cache_ext(name, pkg_only)
|
|
422
|
+
if func is None:
|
|
423
|
+
for a, b in _ext_cache[name].items():
|
|
424
|
+
logger.debug("Found %s %s", a, b)
|
|
425
|
+
yield a, b
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
for x, f in _ext_cache[name].items():
|
|
429
|
+
if (f / ".no_load").is_file():
|
|
430
|
+
logger.debug("Skip %s", f)
|
|
431
|
+
continue
|
|
432
|
+
fn = f / (func + ".py")
|
|
433
|
+
if not fn.is_file():
|
|
434
|
+
fn = f / func / "__init__.py"
|
|
435
|
+
if not fn.is_file():
|
|
436
|
+
# XXX this might be a namespace
|
|
437
|
+
logger.debug("No file: %s/%s", f, func)
|
|
438
|
+
continue
|
|
439
|
+
logger.debug("Found2 %s %s", x, f)
|
|
440
|
+
yield (x, f)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def load_subgroup(
|
|
444
|
+
_fn=None,
|
|
445
|
+
prefix=None,
|
|
446
|
+
sub_pre=None,
|
|
447
|
+
sub_post=None,
|
|
448
|
+
ext_pre=None,
|
|
449
|
+
ext_post=None,
|
|
450
|
+
**kw,
|
|
451
|
+
) -> click.Command:
|
|
452
|
+
"""
|
|
453
|
+
A decorator like click.group, enabling loading of subcommands
|
|
454
|
+
|
|
455
|
+
Internal extensions are loaded as ``{sub_pre}.*.{sub_post}``.
|
|
456
|
+
External extensions are loaded as ``{ext_pre}.*.{ext_post}``.
|
|
457
|
+
|
|
458
|
+
All other arguments are forwarded to `click.command`.
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
def _ext(fn, **kw):
|
|
462
|
+
return click.command(**kw)(fn)
|
|
463
|
+
|
|
464
|
+
kw["cls"] = partial(
|
|
465
|
+
kw.get("cls", Loader),
|
|
466
|
+
_util_sub_pre=sub_pre or this_load.get() or prefix,
|
|
467
|
+
_util_sub_post=sub_post or (None if prefix is None else "cli"),
|
|
468
|
+
_util_ext_pre=ext_pre or prefix,
|
|
469
|
+
_util_ext_post=ext_post or (None if prefix is None else "_main.cli"),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if _fn is None:
|
|
473
|
+
return partial(_ext, **kw)
|
|
474
|
+
else:
|
|
475
|
+
return _ext(_fn, **kw)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class Loader(click.Group):
|
|
479
|
+
"""
|
|
480
|
+
A `click.group` that loads additional commands from subfolders and/or extensions.
|
|
481
|
+
|
|
482
|
+
Subfolders: set _util_sub_pre to your module's name.
|
|
483
|
+
This works with namespace packages.
|
|
484
|
+
E.g. "distkv.command" loads "distkv.command.*.cli".
|
|
485
|
+
|
|
486
|
+
Extensions: set _util_ext_pre to the extension basename.
|
|
487
|
+
Set _util_ext_post to the name of the extension.
|
|
488
|
+
|
|
489
|
+
E.g. "distkv_ext"+"client" loads "distkv_ext.*.client.cli".
|
|
490
|
+
|
|
491
|
+
Both work in parallel.
|
|
492
|
+
|
|
493
|
+
Caller:
|
|
494
|
+
|
|
495
|
+
from moat.util import Loader
|
|
496
|
+
from functools import partial
|
|
497
|
+
|
|
498
|
+
@click.command(cls=partial(Loader,_util_sub_post='command'))
|
|
499
|
+
async def cmd()
|
|
500
|
+
print("I am the main program")
|
|
501
|
+
|
|
502
|
+
Sub-Command Usage (``main`` is defined for you), e.g. in ``command/subcmd.py``::
|
|
503
|
+
|
|
504
|
+
from moat.util import Loader
|
|
505
|
+
from functools import partial
|
|
506
|
+
|
|
507
|
+
@main.command / group()
|
|
508
|
+
async def cmd(self):
|
|
509
|
+
print("I am", self.name) # prints "subcmd"
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
# ruff:noqa:SLF001
|
|
513
|
+
|
|
514
|
+
def __init__(
|
|
515
|
+
self,
|
|
516
|
+
*,
|
|
517
|
+
_util_sub_pre=None,
|
|
518
|
+
_util_sub_post=None,
|
|
519
|
+
_util_ext_pre=None,
|
|
520
|
+
_util_ext_post=None,
|
|
521
|
+
**kw,
|
|
522
|
+
):
|
|
523
|
+
logger.debug(
|
|
524
|
+
"* Load: %s.*.%s / %s.*.%s",
|
|
525
|
+
_util_sub_pre,
|
|
526
|
+
_util_sub_post,
|
|
527
|
+
_util_ext_pre,
|
|
528
|
+
_util_ext_post,
|
|
529
|
+
)
|
|
530
|
+
if _util_sub_pre is not None:
|
|
531
|
+
self._util_sub_pre = _util_sub_pre
|
|
532
|
+
if _util_sub_post is not None:
|
|
533
|
+
self._util_sub_post = _util_sub_post
|
|
534
|
+
if _util_ext_pre is not None:
|
|
535
|
+
self._util_ext_pre = _util_ext_pre
|
|
536
|
+
if _util_ext_post is not None:
|
|
537
|
+
self._util_ext_post = _util_ext_post
|
|
538
|
+
super().__init__(**kw)
|
|
539
|
+
|
|
540
|
+
def get_sub_ext(self, ctx):
|
|
541
|
+
"""Fetch extension variables"""
|
|
542
|
+
sub_pre = getattr(
|
|
543
|
+
# pylint: disable=protected-access
|
|
544
|
+
self,
|
|
545
|
+
"_util_sub_pre",
|
|
546
|
+
ctx.obj._util_sub_pre,
|
|
547
|
+
)
|
|
548
|
+
sub_post = getattr(
|
|
549
|
+
# pylint: disable=protected-access
|
|
550
|
+
self,
|
|
551
|
+
"_util_sub_post",
|
|
552
|
+
ctx.obj._util_sub_post,
|
|
553
|
+
)
|
|
554
|
+
ext_pre = getattr(
|
|
555
|
+
# pylint: disable=protected-access
|
|
556
|
+
self,
|
|
557
|
+
"_util_ext_pre",
|
|
558
|
+
ctx.obj._util_ext_pre,
|
|
559
|
+
)
|
|
560
|
+
ext_post = getattr(
|
|
561
|
+
# pylint: disable=protected-access
|
|
562
|
+
self,
|
|
563
|
+
"_util_ext_post",
|
|
564
|
+
ctx.obj._util_ext_post,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if sub_pre is None:
|
|
568
|
+
sub_post = None
|
|
569
|
+
elif sub_post is None:
|
|
570
|
+
sub_pre = ("cli",)
|
|
571
|
+
elif isinstance(sub_post, str):
|
|
572
|
+
sub_post = sub_post.split(".")
|
|
573
|
+
|
|
574
|
+
if ext_pre is None:
|
|
575
|
+
ext_post = None
|
|
576
|
+
elif ext_post is None:
|
|
577
|
+
ext_pre = None
|
|
578
|
+
elif isinstance(ext_post, str):
|
|
579
|
+
ext_post = ext_post.split(".")
|
|
580
|
+
if len(ext_post) == 1:
|
|
581
|
+
ext_post.append("cli")
|
|
582
|
+
|
|
583
|
+
return sub_pre, sub_post, ext_pre, ext_post
|
|
584
|
+
|
|
585
|
+
def list_commands(self, ctx):
|
|
586
|
+
"show subpackages"
|
|
587
|
+
rv = super().list_commands(ctx)
|
|
588
|
+
sub_pre, sub_post, ext_pre, ext_post = self.get_sub_ext(ctx)
|
|
589
|
+
logger.debug("* List: %s.*.%s / %s.*.%s", sub_pre, sub_post, ext_pre, ext_post)
|
|
590
|
+
|
|
591
|
+
if sub_pre:
|
|
592
|
+
logger.debug("Adding sub %s", sub_pre)
|
|
593
|
+
for _finder, name, _ispkg in _namespaces(sub_pre):
|
|
594
|
+
# ruff:noqa:PLW2901 # var overwritten
|
|
595
|
+
name = name.rsplit(".", 1)[1]
|
|
596
|
+
if name[0] == "_":
|
|
597
|
+
continue
|
|
598
|
+
if load_ext(sub_pre, name, *sub_post, err=ctx.obj.debug_loader):
|
|
599
|
+
rv.append(name)
|
|
600
|
+
|
|
601
|
+
if ext_pre:
|
|
602
|
+
logger.debug("Adding ext %s", ext_pre)
|
|
603
|
+
for n, _ in list_ext(ext_pre):
|
|
604
|
+
if load_ext(ext_pre, n, *ext_post, err=ctx.obj.debug_loader):
|
|
605
|
+
rv.append(n)
|
|
606
|
+
rv.sort()
|
|
607
|
+
logger.debug("List: %r", rv)
|
|
608
|
+
return rv
|
|
609
|
+
|
|
610
|
+
def get_command(self, ctx, cmd_name):
|
|
611
|
+
"add subpackages"
|
|
612
|
+
command = super().get_command(ctx, cmd_name)
|
|
613
|
+
|
|
614
|
+
sub_pre, sub_post, ext_pre, ext_post = self.get_sub_ext(ctx)
|
|
615
|
+
|
|
616
|
+
if command is None and ext_pre is not None:
|
|
617
|
+
command = load_ext(ext_pre, cmd_name, *ext_post)
|
|
618
|
+
if command is not None:
|
|
619
|
+
CFG.with_(f"{ext_pre}.{cmd_name}")
|
|
620
|
+
|
|
621
|
+
if command is None:
|
|
622
|
+
if sub_pre is None or sub_pre is NotGiven:
|
|
623
|
+
return None
|
|
624
|
+
if sub_post is None or sub_post is NotGiven:
|
|
625
|
+
pass
|
|
626
|
+
else:
|
|
627
|
+
command = load_ext(sub_pre, cmd_name, *sub_post)
|
|
628
|
+
if command is not None:
|
|
629
|
+
CFG.with_(f"{sub_pre}.{cmd_name}")
|
|
630
|
+
|
|
631
|
+
if command is None:
|
|
632
|
+
# raise click.UsageError(f"No such subcommand: {cmd_name}")
|
|
633
|
+
return None
|
|
634
|
+
command.__name__ = command.name = cmd_name
|
|
635
|
+
return command
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
class MainLoader(Loader):
|
|
639
|
+
"""
|
|
640
|
+
A special loader that runs the main setup code even if there's a
|
|
641
|
+
subcommand with "--help".
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
async def invoke(self, ctx):
|
|
645
|
+
if not getattr(ctx, "_moat_invoked", False):
|
|
646
|
+
await ctx.invoke(self.callback, **ctx.params)
|
|
647
|
+
return await super().invoke(ctx)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
#
|
|
651
|
+
# There are two ways this can start up.
|
|
652
|
+
# (a) `main_` is the "real" main function. It sets up the Click environment and then
|
|
653
|
+
# starts anyio and runs the function body, which calls `wrap_main`
|
|
654
|
+
# synchronously to set up our object.
|
|
655
|
+
#
|
|
656
|
+
# (b) `wrap_main` is used as a wrapper, used mainly for testing. It sets up the context
|
|
657
|
+
# and then returns "main_.main()", which is an awaitable, thus
|
|
658
|
+
# `wrap_main` acts as an async function.
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@load_subgroup(
|
|
662
|
+
cls=MainLoader,
|
|
663
|
+
add_help_option=False,
|
|
664
|
+
invoke_without_command=True,
|
|
665
|
+
) # , __file__, "command"))
|
|
666
|
+
@click.option("-V", "--verbose", count=True, help="Be more verbose. Can be used multiple times.")
|
|
667
|
+
@click.option("-L", "--debug-loader", is_flag=True, help="Debug submodule loading.")
|
|
668
|
+
@click.option("-Q", "--quiet", count=True, help="Be less verbose. Opposite of '--verbose'.")
|
|
669
|
+
@click.option("-D", "--debug", count=True, help="Enable debug speed-ups (smaller keys etc).")
|
|
670
|
+
@click.option(
|
|
671
|
+
"-l",
|
|
672
|
+
"--log",
|
|
673
|
+
multiple=True,
|
|
674
|
+
help="Adjust log level. Example: '--log asyncactor=DEBUG'.",
|
|
675
|
+
)
|
|
676
|
+
@click.option(
|
|
677
|
+
"-c",
|
|
678
|
+
"--cfg",
|
|
679
|
+
"cfg_files",
|
|
680
|
+
type=click.Path("r"),
|
|
681
|
+
default=None,
|
|
682
|
+
help="Configuration file (YAML).",
|
|
683
|
+
multiple=True,
|
|
684
|
+
)
|
|
685
|
+
@click.option(
|
|
686
|
+
"-h",
|
|
687
|
+
"-?",
|
|
688
|
+
"--help",
|
|
689
|
+
is_flag=True,
|
|
690
|
+
help="Show help. Subcommands only understand '--help'.",
|
|
691
|
+
)
|
|
692
|
+
@attr_args(par_name="Config item")
|
|
693
|
+
@click.pass_context
|
|
694
|
+
async def main_(ctx, verbose, quiet, help=False, **kv): # pylint: disable=redefined-builtin
|
|
695
|
+
"""
|
|
696
|
+
This is the main command. (You might want to override this text.)
|
|
697
|
+
|
|
698
|
+
You need to add a subcommand for this to do anything.
|
|
699
|
+
"""
|
|
700
|
+
ctx.allow_interspersed_args = True
|
|
701
|
+
|
|
702
|
+
# The above `MainLoader.invoke` call causes this code to be called
|
|
703
|
+
# twice instead of never.
|
|
704
|
+
if hasattr(ctx, "_moat_invoked"):
|
|
705
|
+
return
|
|
706
|
+
ctx._moat_invoked = True # pylint: disable=protected-access
|
|
707
|
+
cfg = current_cfg.get()
|
|
708
|
+
if cfg is not None:
|
|
709
|
+
kv["cfg"] = cfg
|
|
710
|
+
wrap_main(ctx=ctx, verbose=max(0, 1 + verbose - quiet), **kv)
|
|
711
|
+
try:
|
|
712
|
+
main = ctx.obj.moat.main_cmd
|
|
713
|
+
except AttributeError:
|
|
714
|
+
main = None
|
|
715
|
+
if help or (
|
|
716
|
+
main is None
|
|
717
|
+
and ctx.invoked_subcommand is None
|
|
718
|
+
and not ctx.args
|
|
719
|
+
and not ctx._protected_args
|
|
720
|
+
):
|
|
721
|
+
print(ctx.get_help())
|
|
722
|
+
await ctx.aexit()
|
|
723
|
+
elif main is not None:
|
|
724
|
+
await main(ctx)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def wrap_main( # pylint: disable=redefined-builtin,inconsistent-return-statements
|
|
728
|
+
main=main_,
|
|
729
|
+
*,
|
|
730
|
+
set_=(),
|
|
731
|
+
vars_=(),
|
|
732
|
+
eval_=(),
|
|
733
|
+
path_=(),
|
|
734
|
+
proxy_=(),
|
|
735
|
+
name=None,
|
|
736
|
+
sub_pre=None,
|
|
737
|
+
sub_post=None,
|
|
738
|
+
ext_pre=None,
|
|
739
|
+
ext_post=None,
|
|
740
|
+
ext_name: str | None = None,
|
|
741
|
+
cfg: attrdict | None | Literal[False] = None,
|
|
742
|
+
cfg_files: str | Sequence[str] = (),
|
|
743
|
+
cfg_load_all: bool | None = True,
|
|
744
|
+
args=None,
|
|
745
|
+
wrap=False,
|
|
746
|
+
verbose=1,
|
|
747
|
+
debug=0,
|
|
748
|
+
debug_loader=False,
|
|
749
|
+
log=(),
|
|
750
|
+
ctx=None,
|
|
751
|
+
help=None,
|
|
752
|
+
) -> Awaitable:
|
|
753
|
+
"""
|
|
754
|
+
The main command entry point, when testing.
|
|
755
|
+
|
|
756
|
+
main: special main function, defaults to moat.run.main_
|
|
757
|
+
name: command name, defaults to {main}'s toplevel module name.
|
|
758
|
+
{sub,ext}_{pre,post}: commands to load in submodules or extensions.
|
|
759
|
+
|
|
760
|
+
cfg: additional configuration to preconfigure
|
|
761
|
+
cfg_files: additional configuration file(s) to load
|
|
762
|
+
cfg_load_all: Flag whether to load the default config file(s): True=all,
|
|
763
|
+
False=first found, None=No.
|
|
764
|
+
|
|
765
|
+
wrap: Flag: this is a subcommand. Don't set up logging, return the awaitable.
|
|
766
|
+
args: Argument list if called from a test, `None` otherwise.
|
|
767
|
+
help: Help text of your code.
|
|
768
|
+
|
|
769
|
+
Internal extensions are loaded as ``{sub_pre}.*.{sub_post}``.
|
|
770
|
+
External extensions are loaded as ``{ext_pre}.*.{ext_post}``.
|
|
771
|
+
|
|
772
|
+
cfg.moat may contain values for {sub,ext}_{pre,post}.
|
|
773
|
+
"""
|
|
774
|
+
|
|
775
|
+
obj = getattr(ctx, "obj", None)
|
|
776
|
+
if obj is None:
|
|
777
|
+
obj = attrdict()
|
|
778
|
+
|
|
779
|
+
opts = obj.get("moat", None)
|
|
780
|
+
if opts is None:
|
|
781
|
+
obj.moat = opts = attrdict()
|
|
782
|
+
|
|
783
|
+
if sub_pre is None:
|
|
784
|
+
sub_pre = opts.get("sub_pre", None)
|
|
785
|
+
else:
|
|
786
|
+
opts["sub_pre"] = sub_pre
|
|
787
|
+
|
|
788
|
+
if sub_post is None:
|
|
789
|
+
sub_post = opts.get("sub_post", None)
|
|
790
|
+
else:
|
|
791
|
+
opts["sub_post"] = sub_post
|
|
792
|
+
|
|
793
|
+
if ext_pre is None:
|
|
794
|
+
ext_pre = opts.get("ext_pre", None)
|
|
795
|
+
else:
|
|
796
|
+
opts["ext_pre"] = ext_pre
|
|
797
|
+
|
|
798
|
+
if ext_post is None:
|
|
799
|
+
ext_post = opts.get("ext_post", None)
|
|
800
|
+
else:
|
|
801
|
+
opts["ext_post"] = ext_post
|
|
802
|
+
|
|
803
|
+
# Name defaults to "moat", of course ;-)
|
|
804
|
+
if name is None:
|
|
805
|
+
name = opts.setdefault("name", "moat")
|
|
806
|
+
else:
|
|
807
|
+
opts["name"] = name
|
|
808
|
+
|
|
809
|
+
if sub_pre is True:
|
|
810
|
+
"discover from caller"
|
|
811
|
+
import inspect # pylint: disable=import-outside-toplevel # noqa: PLC0415
|
|
812
|
+
|
|
813
|
+
sub_pre = inspect.currentframe().f_back.f_globals["__package__"]
|
|
814
|
+
elif sub_pre is None:
|
|
815
|
+
sub_pre = name
|
|
816
|
+
if sub_post is None:
|
|
817
|
+
sub_post = "_main.cli"
|
|
818
|
+
|
|
819
|
+
if main is None:
|
|
820
|
+
if help is not None:
|
|
821
|
+
raise RuntimeError("You can't set the help text this way")
|
|
822
|
+
else:
|
|
823
|
+
main.context_settings["obj"] = obj
|
|
824
|
+
if help is not None:
|
|
825
|
+
main.help = help
|
|
826
|
+
|
|
827
|
+
obj._util_sub_pre = sub_pre # pylint: disable=protected-access
|
|
828
|
+
obj._util_sub_post = sub_post # pylint: disable=protected-access
|
|
829
|
+
obj._util_ext_pre = ext_pre # pylint: disable=protected-access
|
|
830
|
+
obj._util_ext_post = ext_post # pylint: disable=protected-access
|
|
831
|
+
|
|
832
|
+
if not isinstance(cfg, CfgStore):
|
|
833
|
+
cfg = CfgStore(name, preload=cfg, load_all=cfg_load_all, ext=ext_name)
|
|
834
|
+
CFG.set_real_cfg(cfg)
|
|
835
|
+
|
|
836
|
+
if isinstance(cfg_files, str):
|
|
837
|
+
cfg_files = (cfg_files,)
|
|
838
|
+
for fn in cfg_files:
|
|
839
|
+
cfg.add(fn)
|
|
840
|
+
|
|
841
|
+
# our toplevel config file(s)
|
|
842
|
+
CFG.with_("moat")
|
|
843
|
+
if name != "moat":
|
|
844
|
+
CFG.with_(name)
|
|
845
|
+
|
|
846
|
+
obj.debug = verbose
|
|
847
|
+
obj.DEBUG = debug
|
|
848
|
+
|
|
849
|
+
if wrap:
|
|
850
|
+
pass
|
|
851
|
+
elif hasattr(logging.root, "_MoaT"):
|
|
852
|
+
logging.debug("Logging already set up") # noqa:LOG015
|
|
853
|
+
else:
|
|
854
|
+
# Configure logging. This is a somewhat arcane art.
|
|
855
|
+
cfg.mod(
|
|
856
|
+
P("logging.root.level"),
|
|
857
|
+
"DEBUG"
|
|
858
|
+
if verbose > 2
|
|
859
|
+
else "INFO"
|
|
860
|
+
if verbose > 1
|
|
861
|
+
else "WARNING"
|
|
862
|
+
if verbose
|
|
863
|
+
else "ERROR",
|
|
864
|
+
)
|
|
865
|
+
for k in log:
|
|
866
|
+
k, v = k.split("=")
|
|
867
|
+
cfg.mod(P("logging.loggers") / k / "level", v)
|
|
868
|
+
logging.config.dictConfig(cfg.logging)
|
|
869
|
+
|
|
870
|
+
process_args(
|
|
871
|
+
set_=set_,
|
|
872
|
+
vars_=vars_,
|
|
873
|
+
eval_=eval_,
|
|
874
|
+
path_=path_,
|
|
875
|
+
proxy_=proxy_,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
try:
|
|
879
|
+
in_test = cfg.env.in_test
|
|
880
|
+
except AttributeError:
|
|
881
|
+
pass
|
|
882
|
+
else:
|
|
883
|
+
in_test(cfg)
|
|
884
|
+
|
|
885
|
+
if not wrap and not hasattr(logging.root, "_MoaT"):
|
|
886
|
+
logging.basicConfig = _no_config
|
|
887
|
+
logging.config.dictConfig = _no_config
|
|
888
|
+
logging.config.fileConfig = _no_config
|
|
889
|
+
|
|
890
|
+
logging.captureWarnings(verbose > 0)
|
|
891
|
+
logger.disabled = False
|
|
892
|
+
if debug_loader:
|
|
893
|
+
logger.level = logging.DEBUG
|
|
894
|
+
for p in sys.path:
|
|
895
|
+
logger.debug("Path: %s", p)
|
|
896
|
+
logging.root._MoaT = True
|
|
897
|
+
|
|
898
|
+
obj.logger = logging.getLogger(name)
|
|
899
|
+
obj.debug_loader = debug_loader
|
|
900
|
+
obj.cfg = cfg.result[name]
|
|
901
|
+
try:
|
|
902
|
+
obj.stdout = cfg.result.env.stdout
|
|
903
|
+
except AttributeError:
|
|
904
|
+
obj.stdout = sys.stdout
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
# pylint: disable=no-value-for-parameter,unexpected-keyword-arg
|
|
908
|
+
# NOTE this return an awaitable
|
|
909
|
+
if ctx is not None:
|
|
910
|
+
ctx.obj = obj
|
|
911
|
+
elif main is not None:
|
|
912
|
+
if wrap:
|
|
913
|
+
main = main.main
|
|
914
|
+
with ungroup():
|
|
915
|
+
return main(args=args, standalone_mode=False, obj=obj)
|
|
916
|
+
|
|
917
|
+
except click.exceptions.MissingParameter as exc:
|
|
918
|
+
print(
|
|
919
|
+
f"You need to provide an argument {exc.param.name.upper()!r}.\n",
|
|
920
|
+
file=sys.stderr,
|
|
921
|
+
)
|
|
922
|
+
print(exc.cmd.get_help(exc.ctx), file=sys.stderr)
|
|
923
|
+
sys.exit(2)
|
|
924
|
+
except click.exceptions.UsageError as exc:
|
|
925
|
+
try:
|
|
926
|
+
s = str(exc)
|
|
927
|
+
except TypeError:
|
|
928
|
+
logger.exception("??", exc_info=exc)
|
|
929
|
+
else:
|
|
930
|
+
print(s, file=sys.stderr)
|
|
931
|
+
sys.exit(2)
|
|
932
|
+
except click.exceptions.Abort:
|
|
933
|
+
print("Aborted.", file=sys.stderr)
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _ng(type_):
|
|
937
|
+
@wraps(type_)
|
|
938
|
+
def gen(data):
|
|
939
|
+
if data is NotGiven:
|
|
940
|
+
return data
|
|
941
|
+
return type_(data)
|
|
942
|
+
|
|
943
|
+
return gen
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def option_ng(*a, type=str, **kw): # noqa: A002, D103
|
|
947
|
+
return click.option(*a, **kw, type=_ng(type), default=NotGiven)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moat-lib-run
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Main command entry point infrastructure for MoaT applications
|
|
5
|
+
Maintainer-email: Matthias Urlichs <matthias@urlichs.de>
|
|
6
|
+
Project-URL: homepage, https://m-o-a-t.org
|
|
7
|
+
Project-URL: repository, https://github.com/M-o-a-T/moat
|
|
8
|
+
Keywords: MoaT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Framework :: AnyIO
|
|
11
|
+
Classifier: Framework :: Trio
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: anyio~=4.0
|
|
19
|
+
Requires-Dist: moat-util~=0.61.1
|
|
20
|
+
Requires-Dist: asyncclick~=8.0
|
|
21
|
+
Requires-Dist: simpleeval~=1.0
|
|
22
|
+
Requires-Dist: moat-lib-config~=0.1.0
|
|
23
|
+
Requires-Dist: moat-lib-proxy~=0.1.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# moat-lib-run
|
|
27
|
+
|
|
28
|
+
% start main
|
|
29
|
+
% start synopsis
|
|
30
|
+
|
|
31
|
+
Main command entry point infrastructure for MoaT applications.
|
|
32
|
+
|
|
33
|
+
% end synopsis
|
|
34
|
+
|
|
35
|
+
This module provides the infrastructure for building command-line interfaces
|
|
36
|
+
for MoaT applications. It includes:
|
|
37
|
+
|
|
38
|
+
- Command-line argument parsing with Click integration
|
|
39
|
+
- Subcommand loading from internal modules and extensions
|
|
40
|
+
- Configuration file handling
|
|
41
|
+
- Logging setup
|
|
42
|
+
- Main entry point wrappers for testing
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Basic command setup
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from moat.lib.run import main_, wrap_main
|
|
50
|
+
|
|
51
|
+
@main_.command()
|
|
52
|
+
async def my_command(ctx):
|
|
53
|
+
"""A simple command"""
|
|
54
|
+
print("Hello from my command!")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Loading subcommands
|
|
58
|
+
|
|
59
|
+
Use `load_subgroup` to create command groups that automatically load subcommands:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from moat.lib.run import load_subgroup
|
|
63
|
+
import asyncclick as click
|
|
64
|
+
|
|
65
|
+
@load_subgroup(prefix="myapp.commands")
|
|
66
|
+
@click.pass_context
|
|
67
|
+
async def cli(ctx):
|
|
68
|
+
"""Main command group"""
|
|
69
|
+
pass
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Processing command-line arguments
|
|
73
|
+
|
|
74
|
+
The `attr_args` decorator and `process_args` function provide flexible
|
|
75
|
+
argument handling:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from moat.lib.run import attr_args, process_args
|
|
79
|
+
|
|
80
|
+
@main_.command()
|
|
81
|
+
@attr_args(with_path=True)
|
|
82
|
+
async def configure(**kw):
|
|
83
|
+
"""Configure the application"""
|
|
84
|
+
config = process_args({}, **kw)
|
|
85
|
+
# config now contains parsed arguments
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Key Functions
|
|
89
|
+
|
|
90
|
+
- `main_`: The default main command handler
|
|
91
|
+
- `wrap_main`: Wrapper for the main command, useful for testing
|
|
92
|
+
- `load_subgroup`: Decorator to create command groups with automatic subcommand loading
|
|
93
|
+
- `attr_args`: Decorator for adding flexible argument handling to commands
|
|
94
|
+
- `process_args`: Function to process command-line arguments into configuration
|
|
95
|
+
- `Loader`: Click group class that loads commands from submodules and extensions
|
|
96
|
+
|
|
97
|
+
% end main
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
Makefile
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
debian/.gitignore
|
|
6
|
+
debian/changelog
|
|
7
|
+
debian/control
|
|
8
|
+
debian/rules
|
|
9
|
+
src/moat/lib/run/__init__.py
|
|
10
|
+
src/moat_lib_run.egg-info/PKG-INFO
|
|
11
|
+
src/moat_lib_run.egg-info/SOURCES.txt
|
|
12
|
+
src/moat_lib_run.egg-info/dependency_links.txt
|
|
13
|
+
src/moat_lib_run.egg-info/requires.txt
|
|
14
|
+
src/moat_lib_run.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
moat
|