pluggable-namespace 2.0.0__py3-none-any.whl
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.
- _config/config.yaml +41 -0
- _config/plugin/choices.py +37 -0
- _config/plugin/contract/init.py +1 -0
- _config/plugin/default.py +30 -0
- _config/plugin/display_priority.py +34 -0
- _config/plugin/group.py +31 -0
- _config/plugin/init.py +318 -0
- _config/plugin/options.py +14 -0
- _config/plugin/os.py +10 -0
- _config/plugin/positional.py +21 -0
- _config/plugin/source.py +53 -0
- _config/plugin/subcommands.py +82 -0
- _config/plugin/type.py +15 -0
- _log/config.yaml +76 -0
- _log/plugin/console.py +6 -0
- _log/plugin/contract/init.py +1 -0
- _log/plugin/file.py +11 -0
- _log/plugin/init.py +121 -0
- _log/plugin/noop.py +4 -0
- _log/plugin/test.py +9 -0
- _patt/config.yaml +10 -0
- _patt/plugin/inst.py +286 -0
- _pop/file.py +16 -0
- _pop/sub.py +132 -0
- _pop/test.py +121 -0
- _rend/README.rst +17 -0
- _rend/config.yaml +55 -0
- _rend/exc/rend.py +16 -0
- _rend/output/contracts/init.py +1 -0
- _rend/output/json_out.py +19 -0
- _rend/output/nested.py +191 -0
- _rend/output/pretty.py +5 -0
- _rend/output/raw.py +5 -0
- _rend/output/yaml_out.py +37 -0
- _rend/rend/contracts/init.py +1 -0
- _rend/rend/init.py +136 -0
- _rend/rend/jinja.py +85 -0
- _rend/rend/json_file.py +20 -0
- _rend/rend/toml_file.py +19 -0
- _rend/rend/yaml_file.py +179 -0
- _rend/rend/yaml_template.py +25 -0
- _seed/config.yaml +32 -0
- _seed/seed/init.py +27 -0
- _seed/template/plugin/.github/workflows/ci.yaml +70 -0
- _seed/template/plugin/.gitignore +165 -0
- _seed/template/plugin/.gitlab-ci.yml +42 -0
- _seed/template/plugin/.pre-commit-config.yaml +76 -0
- _seed/template/plugin/README.rst.jinja +4 -0
- _seed/template/plugin/copier.yml +6 -0
- _seed/template/plugin/pyproject.toml.jinja +46 -0
- _seed/template/plugin/src/{{name}}/__init__.py.jinja +0 -0
- _seed/template/plugin/src/{{name}}/config.yaml.jinja +19 -0
- _seed/template/plugin/src/{{name}}/plugin/init.py.jinja +9 -0
- _seed/template/plugin/test/conftest.py.jinja +23 -0
- _seed/template/plugin/test/test_init.py.jinja +25 -0
- _seed/template/plugin/test/tpath/README.rst +7 -0
- _seed/template/plugin/test/tpath/mods/config.yaml +9 -0
- _seed/template/plugin/test/tpath/mods/src/init.py +6 -0
- hub/README.rst +55 -0
- hub/__main__.py +74 -0
- hub/config.yaml +72 -0
- hub/plugin/cli.py +125 -0
- hub/plugin/completer.py +143 -0
- hub/plugin/config.py +35 -0
- hub/plugin/console.py +78 -0
- hub/plugin/init.py +88 -0
- hub/plugin/ref.py +55 -0
- pluggable_namespace-2.0.0.dist-info/METADATA +210 -0
- pluggable_namespace-2.0.0.dist-info/RECORD +91 -0
- pluggable_namespace-2.0.0.dist-info/WHEEL +5 -0
- pluggable_namespace-2.0.0.dist-info/entry_points.txt +2 -0
- pluggable_namespace-2.0.0.dist-info/licenses/LICENSE.txt +28 -0
- pluggable_namespace-2.0.0.dist-info/top_level.txt +8 -0
- pns/__init__.py +11 -0
- pns/_contract.c +25287 -0
- pns/_contract.py +379 -0
- pns/_data.c +17441 -0
- pns/_data.py +381 -0
- pns/_debug.py +24 -0
- pns/_hub.c +14921 -0
- pns/_hub.py +187 -0
- pns/contract.py +159 -0
- pns/data.py +27 -0
- pns/dir.py +219 -0
- pns/hub.py +217 -0
- pns/loop.py +79 -0
- pns/mod.py +270 -0
- pns/ref.py +100 -0
- pns/shell.py +230 -0
- pns/shim.py +119 -0
- pns/verify.py +129 -0
_config/config.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
cli_config:
|
|
3
|
+
pns:
|
|
4
|
+
config:
|
|
5
|
+
default: ~/.pns/config.yaml
|
|
6
|
+
os: PNS_CONFIG
|
|
7
|
+
help: The config file used for PNS
|
|
8
|
+
group: Config Options
|
|
9
|
+
options:
|
|
10
|
+
- -c
|
|
11
|
+
subcommands:
|
|
12
|
+
- __global__
|
|
13
|
+
|
|
14
|
+
dyne:
|
|
15
|
+
config:
|
|
16
|
+
- plugin
|
|
17
|
+
|
|
18
|
+
import:
|
|
19
|
+
- aiofiles
|
|
20
|
+
- aiofiles.os
|
|
21
|
+
- aiologger.handlers.files
|
|
22
|
+
- argparse
|
|
23
|
+
- asyncio
|
|
24
|
+
- ast
|
|
25
|
+
- collections
|
|
26
|
+
- pns.contract
|
|
27
|
+
- pns.data
|
|
28
|
+
- pns.exc
|
|
29
|
+
- pns.hub
|
|
30
|
+
- importlib
|
|
31
|
+
- importlib.resources
|
|
32
|
+
- logging
|
|
33
|
+
- msgpack
|
|
34
|
+
- os
|
|
35
|
+
- pathlib
|
|
36
|
+
- pickle
|
|
37
|
+
- signal
|
|
38
|
+
- sys
|
|
39
|
+
- traceback
|
|
40
|
+
- yaml
|
|
41
|
+
- typing
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
"""
|
|
3
|
+
Handle choices that may come from a loaded mod.
|
|
4
|
+
|
|
5
|
+
You specify a sub on the hub for "choices" to dynamically use the loaded mods of that sub as the choices
|
|
6
|
+
|
|
7
|
+
I.e.
|
|
8
|
+
|
|
9
|
+
config:
|
|
10
|
+
my_app:
|
|
11
|
+
my_opt:
|
|
12
|
+
choices:
|
|
13
|
+
my_sub
|
|
14
|
+
|
|
15
|
+
Otherwise, you can specify a list of static choices that may be used
|
|
16
|
+
|
|
17
|
+
I.e.
|
|
18
|
+
|
|
19
|
+
config:
|
|
20
|
+
my_app:
|
|
21
|
+
my_opt:
|
|
22
|
+
choices:
|
|
23
|
+
- choice_1
|
|
24
|
+
- choice_2
|
|
25
|
+
"""
|
|
26
|
+
choices = opts.pop("choices", ())
|
|
27
|
+
if isinstance(choices, str):
|
|
28
|
+
finder = hub
|
|
29
|
+
for part in choices.split("."):
|
|
30
|
+
try:
|
|
31
|
+
finder = getattr(finder, part)
|
|
32
|
+
except AttributeError:
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
opts["choices"] = sorted(finder)
|
|
36
|
+
|
|
37
|
+
return {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
async def sig_parse_opt(hub, opts: dict[str, object]) -> dict[str, object]: ...
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
async def __init__(hub):
|
|
2
|
+
hub._.REF_PATTERN = hub.lib.re.compile(r"^hub\.(\w+(\.\w+)+)\(\)$")
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
6
|
+
"""
|
|
7
|
+
Remove 'default' from the argument opts, it will be handled by the config prioritizer, not argparse.
|
|
8
|
+
If the "default" is a function that exists on the hub, then call it to get the default value.
|
|
9
|
+
This allows you to call a function on the hub to do more processing on the default.
|
|
10
|
+
This could be useful for using a different value for the default based on OS.
|
|
11
|
+
|
|
12
|
+
I.e.
|
|
13
|
+
|
|
14
|
+
config:
|
|
15
|
+
my_app:
|
|
16
|
+
my_opt:
|
|
17
|
+
default: hub.my_sub.mod.func()
|
|
18
|
+
"""
|
|
19
|
+
default = opts.pop("default", None)
|
|
20
|
+
|
|
21
|
+
if default and isinstance(default, str):
|
|
22
|
+
match = hub._.REF_PATTERN.match(default)
|
|
23
|
+
if match:
|
|
24
|
+
ref = match.group(1)
|
|
25
|
+
func = hub.lib.pns.ref.find(hub, ref)
|
|
26
|
+
default = func()
|
|
27
|
+
if hub.lib.asyncio.iscoroutine(default):
|
|
28
|
+
default = await default
|
|
29
|
+
|
|
30
|
+
return {"default": default}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
"""
|
|
3
|
+
Handle the display priority of positional arguments.
|
|
4
|
+
This ensures that positional arguments appear in the defined order
|
|
5
|
+
|
|
6
|
+
I.e.
|
|
7
|
+
|
|
8
|
+
config:
|
|
9
|
+
my_app:
|
|
10
|
+
my_opt_1:
|
|
11
|
+
positional: True
|
|
12
|
+
display_priority: 1
|
|
13
|
+
my_opt_2:
|
|
14
|
+
positional: True
|
|
15
|
+
display_priority: 2
|
|
16
|
+
"""
|
|
17
|
+
display_priority = opts.pop("display_priority", None)
|
|
18
|
+
return {"display_priority": display_priority}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def sort(hub, cli_args: list[dict[str, object]]) -> list[dict[str, object]]:
|
|
22
|
+
"""
|
|
23
|
+
Sort the CLI arguments by display_priority.
|
|
24
|
+
The negative display_priorities were applied to args with no display priority,
|
|
25
|
+
They will come in the order they were defined
|
|
26
|
+
"""
|
|
27
|
+
# Sort arguments by display_priority
|
|
28
|
+
return sorted(
|
|
29
|
+
cli_args,
|
|
30
|
+
key=lambda opt: (
|
|
31
|
+
opt["extra"]["display_priority"] is None,
|
|
32
|
+
opt["extra"]["display_priority"],
|
|
33
|
+
),
|
|
34
|
+
)
|
_config/plugin/group.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
"""
|
|
3
|
+
This config value groups arguments together in the --help text.
|
|
4
|
+
|
|
5
|
+
config:
|
|
6
|
+
my_app:
|
|
7
|
+
my_opt:
|
|
8
|
+
group: my_group
|
|
9
|
+
other_opt:
|
|
10
|
+
group: my_group
|
|
11
|
+
"""
|
|
12
|
+
group = opts.pop("group", False)
|
|
13
|
+
return {"group": group}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def merge(hub, name: str, groups: dict[str, object], subcmd: str, subparser):
|
|
17
|
+
"""
|
|
18
|
+
Merge the group into the subparser if a group name is provided.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
name (str): The name of the group.
|
|
22
|
+
groups (dict): The existing groups dictionary.
|
|
23
|
+
subcmd (str): The subcommand name.
|
|
24
|
+
subparser (ArgumentParser): The subparser instance.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
ArgumentParser: The argument group added to the subparser.
|
|
28
|
+
"""
|
|
29
|
+
if name:
|
|
30
|
+
return groups[subcmd].setdefault(name, subparser.add_argument_group(name))
|
|
31
|
+
return subparser
|
_config/plugin/init.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
DEFAULT_GLOBAL_CLIS = ("pns", "log")
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
async def load(
|
|
5
|
+
hub,
|
|
6
|
+
cli: str = None,
|
|
7
|
+
cli_config: dict[str, object] = None,
|
|
8
|
+
config: dict[str, object] = None,
|
|
9
|
+
subcommands: dict[str, object] = None,
|
|
10
|
+
global_clis: list[str] = None,
|
|
11
|
+
parser_args: tuple = None,
|
|
12
|
+
parser_init_kwargs: dict[str, object] = None,
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Use the pns-config system to load up a fresh configuration for this project
|
|
16
|
+
from the included conf.py file.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
cli (str): The name of the authoritative CLI to parse.
|
|
20
|
+
cli_config (dict): The cli_config section of the plugin's config.yaml
|
|
21
|
+
config (dict): The config section of the plugin's config.yaml
|
|
22
|
+
subcommands (dict): The subcommands section of the plugin's config.yaml
|
|
23
|
+
global_clis (list): The namespaces that should be implicitly added to any cli parser or subparser
|
|
24
|
+
parser_args (tuple): Arguments for the parser.
|
|
25
|
+
parser_init_kwargs (dict): Keyword arguments for initializing the parser or subparsers.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
dict: The parsed CLI options.
|
|
29
|
+
"""
|
|
30
|
+
if parser_init_kwargs is None:
|
|
31
|
+
parser_init_kwargs = {}
|
|
32
|
+
if cli_config is None:
|
|
33
|
+
cli_config = hub._dynamic.config.cli_config
|
|
34
|
+
|
|
35
|
+
# Get the plain config data that will tell us about OS vars and defaults
|
|
36
|
+
if config is None:
|
|
37
|
+
config = hub._dynamic.config.get("config") or {}
|
|
38
|
+
|
|
39
|
+
# Merge config and cli_config
|
|
40
|
+
full_config = hub.lib.pns.data.update(cli_config, config, merge_lists=True)
|
|
41
|
+
|
|
42
|
+
# These CLI namespaces will be added on top of any cli
|
|
43
|
+
if global_clis is None:
|
|
44
|
+
global_clis = DEFAULT_GLOBAL_CLIS
|
|
45
|
+
|
|
46
|
+
# Initialize the active cli, this is what will go into argparse
|
|
47
|
+
active_cli = {}
|
|
48
|
+
|
|
49
|
+
# Logging options and config file are part of the global namespace
|
|
50
|
+
for gn in global_clis:
|
|
51
|
+
active_cli.update(full_config.get(gn, {}).copy())
|
|
52
|
+
|
|
53
|
+
if subcommands is None:
|
|
54
|
+
subcommands = hub._dynamic.config.subcommands
|
|
55
|
+
else:
|
|
56
|
+
active_subcommands = subcommands
|
|
57
|
+
if cli:
|
|
58
|
+
active_subcommands = subcommands.get(cli, {})
|
|
59
|
+
|
|
60
|
+
# Grab the named cli last so that it can override globals
|
|
61
|
+
active_cli.update(full_config.get(cli, {}))
|
|
62
|
+
# Handle options that are sourced from other apps
|
|
63
|
+
await hub.config.source.resolve(cli, active_cli, full_config)
|
|
64
|
+
|
|
65
|
+
main_parser = await hub.config.init.parser(cli, **parser_init_kwargs)
|
|
66
|
+
|
|
67
|
+
# Process config/cli_config values
|
|
68
|
+
subparsers, arguments = await hub.config.init.parse_cli(
|
|
69
|
+
main_parser,
|
|
70
|
+
active_cli=active_cli,
|
|
71
|
+
subcommands=active_subcommands,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Add all the cli options to argparse and call the parser
|
|
75
|
+
main_parser = await hub.config.subcommands.create_parsers(
|
|
76
|
+
main_parser, arguments, subparsers
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
cli_opts = await hub.config.init.parse(main_parser, parser_args)
|
|
80
|
+
else:
|
|
81
|
+
cli_opts = {}
|
|
82
|
+
|
|
83
|
+
# Load the config file parameter in the proper order
|
|
84
|
+
pns_config = full_config.get("pns", {}).get("config") or {}
|
|
85
|
+
config_file = (
|
|
86
|
+
cli_opts.get("config")
|
|
87
|
+
or hub.lib.os.environ.get("PNS_CONFIG", pns_config.get("os"))
|
|
88
|
+
or pns_config.get("default")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
config_data = {}
|
|
92
|
+
if config_file:
|
|
93
|
+
config_file = hub.lib.pathlib.Path(config_file)
|
|
94
|
+
if config_file.exists():
|
|
95
|
+
with config_file.open("r") as fh:
|
|
96
|
+
config_data = hub.lib.yaml.safe_load(fh.read())
|
|
97
|
+
|
|
98
|
+
if not isinstance(config_data, dict):
|
|
99
|
+
if not config_data: # The config is just empty, just let the user know
|
|
100
|
+
hub.lib.warnings.warn(
|
|
101
|
+
f"The configuration file {config_file} "
|
|
102
|
+
"retunred no data or failed to load, no configuration "
|
|
103
|
+
"file data will be used"
|
|
104
|
+
)
|
|
105
|
+
else: # The config is invalid, warn the user
|
|
106
|
+
hub.lib.warnings.warn(
|
|
107
|
+
f"Configuration file must contain "
|
|
108
|
+
f"key/value pairs, not {type(config_data)}, the supplied "
|
|
109
|
+
f"configuration file {config_file} is not returning valid "
|
|
110
|
+
f"data. No configuration file data will be used"
|
|
111
|
+
)
|
|
112
|
+
config_data = {}
|
|
113
|
+
|
|
114
|
+
opt = await hub.config.init.prioritize(
|
|
115
|
+
cli=cli,
|
|
116
|
+
cli_opts=cli_opts,
|
|
117
|
+
config=full_config,
|
|
118
|
+
config_file_data=config_data,
|
|
119
|
+
global_clis=global_clis,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return hub.lib.pns.data.NamespaceDict(opt)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def parse(
|
|
126
|
+
hub,
|
|
127
|
+
main_parser: object,
|
|
128
|
+
parser_args: tuple[dict[str, object]],
|
|
129
|
+
) -> dict[str, object]:
|
|
130
|
+
# Actually call the main parser
|
|
131
|
+
parsed_args = main_parser.parse_args(args=parser_args)
|
|
132
|
+
return hub.lib.pns.data.NamespaceDict(parsed_args.__dict__)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def parser(hub, cli: str, parser: object = None, **kwargs) -> object:
|
|
136
|
+
"""
|
|
137
|
+
Create a new ArgumentParser or add a subparser to an existing parser.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
cli (str): The name of the CLI being parsed.
|
|
141
|
+
parser (ArgumentParser, optional): An existing ArgumentParser instance.
|
|
142
|
+
**kwargs: Additional keyword arguments for ArgumentParser.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
ArgumentParser: The created or modified ArgumentParser instance.
|
|
146
|
+
"""
|
|
147
|
+
if cli and "prog" not in kwargs:
|
|
148
|
+
kwargs["prog"] = cli.title()
|
|
149
|
+
if cli and "description" not in kwargs:
|
|
150
|
+
kwargs["description"] = f"{cli.title().replace('_', ' ')} CLI Parser"
|
|
151
|
+
if "conflict_handler" not in kwargs:
|
|
152
|
+
kwargs["conflict_handler"] = "resolve"
|
|
153
|
+
if parser is None:
|
|
154
|
+
return hub.lib.argparse.ArgumentParser(**kwargs)
|
|
155
|
+
else:
|
|
156
|
+
return parser.add_parser(cli, **kwargs)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
160
|
+
"""
|
|
161
|
+
Parse and process CLI options, handling custom config values.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
opts (dict): The options to be parsed.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
dict: The processed options.
|
|
168
|
+
"""
|
|
169
|
+
extra = {}
|
|
170
|
+
for parser_mod in sorted(hub.config):
|
|
171
|
+
if parser_mod == "init":
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if "parse_opt" not in hub.config[parser_mod]:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
new_extras = await hub.config[parser_mod].parse_opt(opts)
|
|
178
|
+
extra.update(new_extras)
|
|
179
|
+
return hub.lib.pns.data.NamespaceDict(extra)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def parse_cli(
|
|
183
|
+
hub,
|
|
184
|
+
main_parser,
|
|
185
|
+
active_cli: dict[str, object],
|
|
186
|
+
subcommands: dict[str, object] = None,
|
|
187
|
+
) -> dict[str, object]:
|
|
188
|
+
"""
|
|
189
|
+
Create a parser and parse all the CLI options.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
main_parser (ArgumentParser): The main parser instance.
|
|
193
|
+
active_cli (dict): The active CLI configuration.
|
|
194
|
+
subcommands (dict): The subcommands configuration.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
tuple: A tuple containing the subparsers dictionary and the list of arguments.
|
|
198
|
+
"""
|
|
199
|
+
if subcommands is None:
|
|
200
|
+
subcommands = {}
|
|
201
|
+
# Create the main parser for the CLI
|
|
202
|
+
if subcommands:
|
|
203
|
+
sparser = main_parser.add_subparsers(dest="SUBPARSER")
|
|
204
|
+
subparsers = {}
|
|
205
|
+
|
|
206
|
+
for subcommand, opts in subcommands.items():
|
|
207
|
+
subparsers[subcommand] = await hub.config.init.parser(
|
|
208
|
+
subcommand, sparser, **opts
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Collect all arguments and their metadata
|
|
212
|
+
arguments = []
|
|
213
|
+
|
|
214
|
+
for name, namespace_opts in active_cli.items():
|
|
215
|
+
opts = namespace_opts.copy()
|
|
216
|
+
opts["__name__"] = name
|
|
217
|
+
# Separate/process our custom config values from those consumed by argparse
|
|
218
|
+
extra = await hub.config.init.parse_opt(opts)
|
|
219
|
+
options = extra.options
|
|
220
|
+
group_name = extra.group
|
|
221
|
+
cli_name = opts.pop("__name__")
|
|
222
|
+
|
|
223
|
+
argument_meta = {
|
|
224
|
+
"cli_name": cli_name,
|
|
225
|
+
"options": options,
|
|
226
|
+
"opts": opts,
|
|
227
|
+
"group_name": group_name,
|
|
228
|
+
"extra": extra,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
arguments.append(argument_meta)
|
|
232
|
+
|
|
233
|
+
return subparsers, arguments
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
PLACEHOLDER = object()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def prioritize(
|
|
240
|
+
hub,
|
|
241
|
+
cli: str,
|
|
242
|
+
cli_opts: dict[str, any],
|
|
243
|
+
config: dict[str, any],
|
|
244
|
+
config_file_data: dict[str, any],
|
|
245
|
+
global_clis: list[str],
|
|
246
|
+
*,
|
|
247
|
+
document_parameters: bool = False,
|
|
248
|
+
):
|
|
249
|
+
"""
|
|
250
|
+
Prioritize configuration data from various sources.
|
|
251
|
+
|
|
252
|
+
The order of priority is:
|
|
253
|
+
1. CLI options (highest priority)
|
|
254
|
+
2. Configuration file data
|
|
255
|
+
3. OS environment variables
|
|
256
|
+
4. Default values (lowest priority)
|
|
257
|
+
5. Rewrite the root_dir option so running apps automatically changes dirs to user preferences
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
cli (str): The name of the CLI being prioritized.
|
|
261
|
+
cli_opts (dict): The parsed CLI options.
|
|
262
|
+
config (dict): The configuration dictionary.
|
|
263
|
+
config_file_data (dict): The data from the configuration file.
|
|
264
|
+
Document_parameters: If True, hub.OPT will contain docstrings for leaf nodes
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
pns.data.NamespaceDict: The prioritized configuration options.
|
|
268
|
+
"""
|
|
269
|
+
opt = hub.lib.collections.defaultdict(dict)
|
|
270
|
+
for namespace, args in config.items():
|
|
271
|
+
# Boolean to determine if the given option is part of the active cli
|
|
272
|
+
is_active_namespace = namespace == cli or namespace in global_clis
|
|
273
|
+
for arg, data in args.items():
|
|
274
|
+
is_active_cli = is_active_namespace
|
|
275
|
+
# This option belongs to a different part of the namespace
|
|
276
|
+
if data.get("source"):
|
|
277
|
+
# If the source is not the current namespace, skip it
|
|
278
|
+
if data["source"] != namespace:
|
|
279
|
+
continue
|
|
280
|
+
is_active_cli = True
|
|
281
|
+
# Initialize value to None
|
|
282
|
+
value = None
|
|
283
|
+
|
|
284
|
+
# 1. Check CLI options first
|
|
285
|
+
if is_active_cli and arg in cli_opts:
|
|
286
|
+
value = cli_opts.get(arg)
|
|
287
|
+
|
|
288
|
+
# 2. Check config file data if CLI option is not set
|
|
289
|
+
if value is None:
|
|
290
|
+
value = config_file_data.get(namespace, {}).get(arg, PLACEHOLDER)
|
|
291
|
+
|
|
292
|
+
# Skip malformed config
|
|
293
|
+
if not isinstance(data, dict):
|
|
294
|
+
msg = f"Invalid data from config.yaml: {data}"
|
|
295
|
+
raise TypeError(msg)
|
|
296
|
+
|
|
297
|
+
# 3. Check OS environment variables if config file data is not set
|
|
298
|
+
if value is PLACEHOLDER and "os" in data:
|
|
299
|
+
value = hub.lib.os.environ.get(data["os"], PLACEHOLDER)
|
|
300
|
+
|
|
301
|
+
# 4. Use default value if none of the above are set
|
|
302
|
+
if value is PLACEHOLDER:
|
|
303
|
+
if "default" not in data:
|
|
304
|
+
msg = f"Option '{namespace}.{arg}' has no value from config, os, or defaults"
|
|
305
|
+
if is_active_cli:
|
|
306
|
+
raise ValueError(msg)
|
|
307
|
+
value = data.get("default")
|
|
308
|
+
if document_parameters:
|
|
309
|
+
# Wrap the value in a class that gives it a docstring
|
|
310
|
+
value = hub.lib.pns.data.wrap_value(arg, value, data.get("help", ""))
|
|
311
|
+
|
|
312
|
+
# Set the value in the OPT dictionary
|
|
313
|
+
opt[namespace][arg] = value
|
|
314
|
+
|
|
315
|
+
opt["pns"]["subparser"] = cli_opts.get("SUBPARSER", "")
|
|
316
|
+
opt["pns"]["global_clis"] = global_clis
|
|
317
|
+
|
|
318
|
+
return opt
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
"""
|
|
3
|
+
Alternate flags that can be used for this config option
|
|
4
|
+
|
|
5
|
+
config:
|
|
6
|
+
my_app:
|
|
7
|
+
my_opt:
|
|
8
|
+
options:
|
|
9
|
+
- -o
|
|
10
|
+
- --opt
|
|
11
|
+
- --opt1
|
|
12
|
+
"""
|
|
13
|
+
options = opts.pop("options", ())
|
|
14
|
+
return {"options": options}
|
_config/plugin/os.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
"""
|
|
3
|
+
Mark an argument as a positional
|
|
4
|
+
|
|
5
|
+
I.e.
|
|
6
|
+
|
|
7
|
+
config:
|
|
8
|
+
my_app:
|
|
9
|
+
my_opt_1:
|
|
10
|
+
positional: True
|
|
11
|
+
"""
|
|
12
|
+
positional = opts.pop("positional", False)
|
|
13
|
+
|
|
14
|
+
if positional:
|
|
15
|
+
# A positional argument cannot have flag options
|
|
16
|
+
opts.pop("options", None)
|
|
17
|
+
else:
|
|
18
|
+
# For non-positional args, create an inituitive cli name
|
|
19
|
+
opts["__name__"] = f"--{opts['__name__'].lower().replace('_', '-')}"
|
|
20
|
+
|
|
21
|
+
return {}
|
_config/plugin/source.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
"""
|
|
3
|
+
Include an option defined in another app's config in this app's CLI
|
|
4
|
+
Merge over the source app's config for the option.
|
|
5
|
+
|
|
6
|
+
It will still show up under the namespace of the other app.
|
|
7
|
+
|
|
8
|
+
I.e.
|
|
9
|
+
|
|
10
|
+
config:
|
|
11
|
+
my_app:
|
|
12
|
+
my_opt:
|
|
13
|
+
source: other_app
|
|
14
|
+
default: override
|
|
15
|
+
|
|
16
|
+
other_app:
|
|
17
|
+
my_opt:
|
|
18
|
+
default: value
|
|
19
|
+
"""
|
|
20
|
+
opts.pop("source", None)
|
|
21
|
+
|
|
22
|
+
# This is already handled in hub.config.init.load since it needs to happen before everything else.
|
|
23
|
+
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def resolve(
|
|
28
|
+
hub, cli: str, active_cli: dict[str, object], full_config: dict[str, object]
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Add an option from another app's cli_config to the active cli
|
|
32
|
+
"""
|
|
33
|
+
new_stuff = {}
|
|
34
|
+
for name, opt in active_cli.items():
|
|
35
|
+
source = opt.pop("source", None)
|
|
36
|
+
if not source:
|
|
37
|
+
continue
|
|
38
|
+
# Get the config for the opt from the source
|
|
39
|
+
if not full_config.get(source):
|
|
40
|
+
full_config[source] = {}
|
|
41
|
+
if not full_config[source].get(name):
|
|
42
|
+
full_config[source][name] = {}
|
|
43
|
+
|
|
44
|
+
# Override the sourced option config with the values from the active cli
|
|
45
|
+
full_config[source][name].update(opt)
|
|
46
|
+
new_stuff[name] = full_config[source][name]
|
|
47
|
+
new_stuff[name]["source"] = source
|
|
48
|
+
|
|
49
|
+
for key in new_stuff:
|
|
50
|
+
# Ensure that the key only shows up under the source's namespace
|
|
51
|
+
full_config[cli].pop(key, None)
|
|
52
|
+
|
|
53
|
+
active_cli.update(new_stuff)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
"""
|
|
3
|
+
Specify that a given option belongs to a certain subcommand
|
|
4
|
+
|
|
5
|
+
I.e.
|
|
6
|
+
|
|
7
|
+
config:
|
|
8
|
+
my_app:
|
|
9
|
+
my_opt:
|
|
10
|
+
subcommands:
|
|
11
|
+
- my_subcommand
|
|
12
|
+
my_global_opt:
|
|
13
|
+
subcommands:
|
|
14
|
+
- __global__
|
|
15
|
+
|
|
16
|
+
subcommands:
|
|
17
|
+
my_app:
|
|
18
|
+
my_subcommand: {}
|
|
19
|
+
"""
|
|
20
|
+
subcommands = opts.pop("subcommands", ())
|
|
21
|
+
return {"subcommands": subcommands}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
GLOBAL = "__global__"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def create_parsers(
|
|
28
|
+
hub,
|
|
29
|
+
main_parser,
|
|
30
|
+
cli_args: list[object],
|
|
31
|
+
subparsers: dict[str, object],
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Create and add CLI parsers and arguments.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
hub: The PNS hub instance.
|
|
38
|
+
main_parser (ArgumentParser): The main parser instance.
|
|
39
|
+
parser_args (tuple): Arguments for the parser.
|
|
40
|
+
cli_args (list): List of CLI arguments.
|
|
41
|
+
subparsers (dict): Dictionary of subparsers.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
dict: The parsed CLI options.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Add CLI options to the parser
|
|
48
|
+
groups = {}
|
|
49
|
+
subparser_groups = {subcommand: {} for subcommand in subparsers}
|
|
50
|
+
sorted_arguments = await hub.config.display_priority.sort(cli_args)
|
|
51
|
+
|
|
52
|
+
for arg_meta in sorted_arguments:
|
|
53
|
+
cli_name = arg_meta["cli_name"]
|
|
54
|
+
options = arg_meta["options"]
|
|
55
|
+
opts = arg_meta["opts"]
|
|
56
|
+
group_name = arg_meta["group_name"]
|
|
57
|
+
extra_subcommands = arg_meta["extra"].subcommands
|
|
58
|
+
|
|
59
|
+
# Handle argument groups for top-level parser
|
|
60
|
+
target_group = await hub.config.group.merge(
|
|
61
|
+
group_name, {None: groups}, None, main_parser
|
|
62
|
+
)
|
|
63
|
+
if GLOBAL in extra_subcommands or not extra_subcommands:
|
|
64
|
+
target_group.add_argument(cli_name, *options, **opts)
|
|
65
|
+
|
|
66
|
+
# Handle argument groups for subparsers
|
|
67
|
+
for subcommand in extra_subcommands:
|
|
68
|
+
if subcommand == GLOBAL:
|
|
69
|
+
for subcmd, sparser in subparsers.items():
|
|
70
|
+
subparser_group = await hub.config.group.merge(
|
|
71
|
+
group_name, subparser_groups, subcmd, sparser
|
|
72
|
+
)
|
|
73
|
+
subparser_group.add_argument(cli_name, *options, **opts)
|
|
74
|
+
elif subcommand in subparsers:
|
|
75
|
+
subcmd = subcommand
|
|
76
|
+
sparser = subparsers[subcommand]
|
|
77
|
+
subparser_group = await hub.config.group.merge(
|
|
78
|
+
group_name, subparser_groups, subcmd, sparser
|
|
79
|
+
)
|
|
80
|
+
subparser_group.add_argument(cli_name, *options, **opts)
|
|
81
|
+
|
|
82
|
+
return main_parser
|
_config/plugin/type.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
"""
|
|
3
|
+
Evaluate the "type" string to be a constructor for the actual type
|
|
4
|
+
I.e.
|
|
5
|
+
|
|
6
|
+
config:
|
|
7
|
+
my_app:
|
|
8
|
+
my_opt:
|
|
9
|
+
type: str
|
|
10
|
+
"""
|
|
11
|
+
type_ = opts.pop("type", None)
|
|
12
|
+
if type_:
|
|
13
|
+
opts["type"] = eval(type_)
|
|
14
|
+
|
|
15
|
+
return {}
|