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.
Files changed (91) hide show
  1. _config/config.yaml +41 -0
  2. _config/plugin/choices.py +37 -0
  3. _config/plugin/contract/init.py +1 -0
  4. _config/plugin/default.py +30 -0
  5. _config/plugin/display_priority.py +34 -0
  6. _config/plugin/group.py +31 -0
  7. _config/plugin/init.py +318 -0
  8. _config/plugin/options.py +14 -0
  9. _config/plugin/os.py +10 -0
  10. _config/plugin/positional.py +21 -0
  11. _config/plugin/source.py +53 -0
  12. _config/plugin/subcommands.py +82 -0
  13. _config/plugin/type.py +15 -0
  14. _log/config.yaml +76 -0
  15. _log/plugin/console.py +6 -0
  16. _log/plugin/contract/init.py +1 -0
  17. _log/plugin/file.py +11 -0
  18. _log/plugin/init.py +121 -0
  19. _log/plugin/noop.py +4 -0
  20. _log/plugin/test.py +9 -0
  21. _patt/config.yaml +10 -0
  22. _patt/plugin/inst.py +286 -0
  23. _pop/file.py +16 -0
  24. _pop/sub.py +132 -0
  25. _pop/test.py +121 -0
  26. _rend/README.rst +17 -0
  27. _rend/config.yaml +55 -0
  28. _rend/exc/rend.py +16 -0
  29. _rend/output/contracts/init.py +1 -0
  30. _rend/output/json_out.py +19 -0
  31. _rend/output/nested.py +191 -0
  32. _rend/output/pretty.py +5 -0
  33. _rend/output/raw.py +5 -0
  34. _rend/output/yaml_out.py +37 -0
  35. _rend/rend/contracts/init.py +1 -0
  36. _rend/rend/init.py +136 -0
  37. _rend/rend/jinja.py +85 -0
  38. _rend/rend/json_file.py +20 -0
  39. _rend/rend/toml_file.py +19 -0
  40. _rend/rend/yaml_file.py +179 -0
  41. _rend/rend/yaml_template.py +25 -0
  42. _seed/config.yaml +32 -0
  43. _seed/seed/init.py +27 -0
  44. _seed/template/plugin/.github/workflows/ci.yaml +70 -0
  45. _seed/template/plugin/.gitignore +165 -0
  46. _seed/template/plugin/.gitlab-ci.yml +42 -0
  47. _seed/template/plugin/.pre-commit-config.yaml +76 -0
  48. _seed/template/plugin/README.rst.jinja +4 -0
  49. _seed/template/plugin/copier.yml +6 -0
  50. _seed/template/plugin/pyproject.toml.jinja +46 -0
  51. _seed/template/plugin/src/{{name}}/__init__.py.jinja +0 -0
  52. _seed/template/plugin/src/{{name}}/config.yaml.jinja +19 -0
  53. _seed/template/plugin/src/{{name}}/plugin/init.py.jinja +9 -0
  54. _seed/template/plugin/test/conftest.py.jinja +23 -0
  55. _seed/template/plugin/test/test_init.py.jinja +25 -0
  56. _seed/template/plugin/test/tpath/README.rst +7 -0
  57. _seed/template/plugin/test/tpath/mods/config.yaml +9 -0
  58. _seed/template/plugin/test/tpath/mods/src/init.py +6 -0
  59. hub/README.rst +55 -0
  60. hub/__main__.py +74 -0
  61. hub/config.yaml +72 -0
  62. hub/plugin/cli.py +125 -0
  63. hub/plugin/completer.py +143 -0
  64. hub/plugin/config.py +35 -0
  65. hub/plugin/console.py +78 -0
  66. hub/plugin/init.py +88 -0
  67. hub/plugin/ref.py +55 -0
  68. pluggable_namespace-2.0.0.dist-info/METADATA +210 -0
  69. pluggable_namespace-2.0.0.dist-info/RECORD +91 -0
  70. pluggable_namespace-2.0.0.dist-info/WHEEL +5 -0
  71. pluggable_namespace-2.0.0.dist-info/entry_points.txt +2 -0
  72. pluggable_namespace-2.0.0.dist-info/licenses/LICENSE.txt +28 -0
  73. pluggable_namespace-2.0.0.dist-info/top_level.txt +8 -0
  74. pns/__init__.py +11 -0
  75. pns/_contract.c +25287 -0
  76. pns/_contract.py +379 -0
  77. pns/_data.c +17441 -0
  78. pns/_data.py +381 -0
  79. pns/_debug.py +24 -0
  80. pns/_hub.c +14921 -0
  81. pns/_hub.py +187 -0
  82. pns/contract.py +159 -0
  83. pns/data.py +27 -0
  84. pns/dir.py +219 -0
  85. pns/hub.py +217 -0
  86. pns/loop.py +79 -0
  87. pns/mod.py +270 -0
  88. pns/ref.py +100 -0
  89. pns/shell.py +230 -0
  90. pns/shim.py +119 -0
  91. 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
+ )
@@ -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,10 @@
1
+ async def parse_opt(hub, opts: dict[str, object]) -> dict[str, object]:
2
+ """
3
+ Remove 'os' from the argument kwargs, it will be handled by the config prioritizer
4
+
5
+ config:
6
+ my_app:
7
+ my_opt:
8
+ os: MY_ENV_VAR
9
+ """
10
+ return {"os": opts.pop("os", False)}
@@ -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 {}
@@ -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 {}