hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a190__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.
- hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
- hpcflow/_version.py +1 -1
- hpcflow/app.py +1 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
- hpcflow/sdk/__init__.py +21 -15
- hpcflow/sdk/app.py +2133 -770
- hpcflow/sdk/cli.py +281 -250
- hpcflow/sdk/cli_common.py +6 -2
- hpcflow/sdk/config/__init__.py +1 -1
- hpcflow/sdk/config/callbacks.py +77 -42
- hpcflow/sdk/config/cli.py +126 -103
- hpcflow/sdk/config/config.py +578 -311
- hpcflow/sdk/config/config_file.py +131 -95
- hpcflow/sdk/config/errors.py +112 -85
- hpcflow/sdk/config/types.py +145 -0
- hpcflow/sdk/core/actions.py +1054 -994
- hpcflow/sdk/core/app_aware.py +24 -0
- hpcflow/sdk/core/cache.py +81 -63
- hpcflow/sdk/core/command_files.py +275 -185
- hpcflow/sdk/core/commands.py +111 -107
- hpcflow/sdk/core/element.py +724 -503
- hpcflow/sdk/core/enums.py +192 -0
- hpcflow/sdk/core/environment.py +74 -93
- hpcflow/sdk/core/errors.py +398 -51
- hpcflow/sdk/core/json_like.py +540 -272
- hpcflow/sdk/core/loop.py +380 -334
- hpcflow/sdk/core/loop_cache.py +160 -43
- hpcflow/sdk/core/object_list.py +370 -207
- hpcflow/sdk/core/parameters.py +728 -600
- hpcflow/sdk/core/rule.py +59 -41
- hpcflow/sdk/core/run_dir_files.py +33 -22
- hpcflow/sdk/core/task.py +1546 -1325
- hpcflow/sdk/core/task_schema.py +240 -196
- hpcflow/sdk/core/test_utils.py +126 -88
- hpcflow/sdk/core/types.py +387 -0
- hpcflow/sdk/core/utils.py +410 -305
- hpcflow/sdk/core/validation.py +82 -9
- hpcflow/sdk/core/workflow.py +1192 -1028
- hpcflow/sdk/core/zarr_io.py +98 -137
- hpcflow/sdk/demo/cli.py +46 -33
- hpcflow/sdk/helper/cli.py +18 -16
- hpcflow/sdk/helper/helper.py +75 -63
- hpcflow/sdk/helper/watcher.py +61 -28
- hpcflow/sdk/log.py +83 -59
- hpcflow/sdk/persistence/__init__.py +8 -31
- hpcflow/sdk/persistence/base.py +988 -586
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +38 -0
- hpcflow/sdk/persistence/json.py +408 -153
- hpcflow/sdk/persistence/pending.py +158 -123
- hpcflow/sdk/persistence/store_resource.py +37 -22
- hpcflow/sdk/persistence/types.py +307 -0
- hpcflow/sdk/persistence/utils.py +14 -11
- hpcflow/sdk/persistence/zarr.py +477 -420
- hpcflow/sdk/runtime.py +44 -41
- hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
- hpcflow/sdk/submission/jobscript.py +444 -404
- hpcflow/sdk/submission/schedulers/__init__.py +133 -40
- hpcflow/sdk/submission/schedulers/direct.py +97 -71
- hpcflow/sdk/submission/schedulers/sge.py +132 -126
- hpcflow/sdk/submission/schedulers/slurm.py +263 -268
- hpcflow/sdk/submission/schedulers/utils.py +7 -2
- hpcflow/sdk/submission/shells/__init__.py +14 -15
- hpcflow/sdk/submission/shells/base.py +102 -29
- hpcflow/sdk/submission/shells/bash.py +72 -55
- hpcflow/sdk/submission/shells/os_version.py +31 -30
- hpcflow/sdk/submission/shells/powershell.py +37 -29
- hpcflow/sdk/submission/submission.py +203 -257
- hpcflow/sdk/submission/types.py +143 -0
- hpcflow/sdk/typing.py +163 -12
- hpcflow/tests/conftest.py +8 -6
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
- hpcflow/tests/scripts/test_main_scripts.py +60 -30
- hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
- hpcflow/tests/unit/test_action.py +86 -75
- hpcflow/tests/unit/test_action_rule.py +9 -4
- hpcflow/tests/unit/test_app.py +13 -6
- hpcflow/tests/unit/test_cli.py +1 -1
- hpcflow/tests/unit/test_command.py +71 -54
- hpcflow/tests/unit/test_config.py +20 -15
- hpcflow/tests/unit/test_config_file.py +21 -18
- hpcflow/tests/unit/test_element.py +58 -62
- hpcflow/tests/unit/test_element_iteration.py +3 -1
- hpcflow/tests/unit/test_element_set.py +29 -19
- hpcflow/tests/unit/test_group.py +4 -2
- hpcflow/tests/unit/test_input_source.py +116 -93
- hpcflow/tests/unit/test_input_value.py +29 -24
- hpcflow/tests/unit/test_json_like.py +44 -35
- hpcflow/tests/unit/test_loop.py +65 -58
- hpcflow/tests/unit/test_object_list.py +17 -12
- hpcflow/tests/unit/test_parameter.py +16 -7
- hpcflow/tests/unit/test_persistence.py +48 -35
- hpcflow/tests/unit/test_resources.py +20 -18
- hpcflow/tests/unit/test_run.py +8 -3
- hpcflow/tests/unit/test_runtime.py +2 -1
- hpcflow/tests/unit/test_schema_input.py +23 -15
- hpcflow/tests/unit/test_shell.py +3 -2
- hpcflow/tests/unit/test_slurm.py +8 -7
- hpcflow/tests/unit/test_submission.py +39 -19
- hpcflow/tests/unit/test_task.py +352 -247
- hpcflow/tests/unit/test_task_schema.py +33 -20
- hpcflow/tests/unit/test_utils.py +9 -11
- hpcflow/tests/unit/test_value_sequence.py +15 -12
- hpcflow/tests/unit/test_workflow.py +114 -83
- hpcflow/tests/unit/test_workflow_template.py +0 -1
- hpcflow/tests/workflows/test_jobscript.py +2 -1
- hpcflow/tests/workflows/test_workflows.py +18 -13
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
- hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
- hpcflow/sdk/core/parallel.py +0 -21
- hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/config/config.py
CHANGED
@@ -14,27 +14,24 @@ import os
|
|
14
14
|
import socket
|
15
15
|
import uuid
|
16
16
|
from dataclasses import dataclass, field
|
17
|
-
from hashlib import new
|
18
17
|
from pathlib import Path
|
19
|
-
from typing import
|
20
|
-
import fsspec
|
18
|
+
from typing import cast, overload, TYPE_CHECKING
|
19
|
+
import fsspec # type: ignore
|
21
20
|
|
22
21
|
from rich.console import Console, Group
|
23
22
|
from rich.table import Table
|
24
23
|
from rich.pretty import Pretty
|
25
24
|
from rich.panel import Panel
|
26
25
|
from rich import print as rich_print
|
27
|
-
from fsspec.registry import known_implementations as fsspec_protocols
|
28
|
-
from fsspec.implementations.local import LocalFileSystem
|
29
|
-
from platformdirs import user_data_dir
|
30
|
-
from valida.schema import Schema
|
26
|
+
from fsspec.registry import known_implementations as fsspec_protocols # type: ignore
|
27
|
+
from fsspec.implementations.local import LocalFileSystem # type: ignore
|
31
28
|
from hpcflow.sdk.core.utils import get_in_container, read_YAML_file, set_in_container
|
32
29
|
|
33
|
-
from hpcflow.sdk.core.validation import get_schema
|
30
|
+
from hpcflow.sdk.core.validation import get_schema, Schema
|
34
31
|
from hpcflow.sdk.submission.shells import DEFAULT_SHELL_NAMES
|
35
32
|
from hpcflow.sdk.typing import PathLike
|
36
33
|
|
37
|
-
from .callbacks import (
|
34
|
+
from hpcflow.sdk.config.callbacks import (
|
38
35
|
callback_bool,
|
39
36
|
callback_lowercase,
|
40
37
|
callback_scheduler_set_up,
|
@@ -48,8 +45,8 @@ from .callbacks import (
|
|
48
45
|
check_load_data_files,
|
49
46
|
set_scheduler_invocation_match,
|
50
47
|
)
|
51
|
-
from .config_file import ConfigFile
|
52
|
-
from .errors import (
|
48
|
+
from hpcflow.sdk.config.config_file import ConfigFile
|
49
|
+
from hpcflow.sdk.config.errors import (
|
53
50
|
ConfigChangeInvalidJSONError,
|
54
51
|
ConfigChangePopIndexError,
|
55
52
|
ConfigChangeTypeInvalidError,
|
@@ -62,11 +59,28 @@ from .errors import (
|
|
62
59
|
ConfigValidationError,
|
63
60
|
)
|
64
61
|
|
62
|
+
if TYPE_CHECKING:
|
63
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
64
|
+
from typing import Any, Literal
|
65
|
+
from .types import (
|
66
|
+
ConfigDescriptor,
|
67
|
+
ConfigMetadata,
|
68
|
+
DefaultConfiguration,
|
69
|
+
SchedulerConfigDescriptor,
|
70
|
+
ShellConfigDescriptor,
|
71
|
+
GetterCallback,
|
72
|
+
SetterCallback,
|
73
|
+
T,
|
74
|
+
)
|
75
|
+
from ..app import BaseApp
|
76
|
+
from ..core.types import AbstractFileSystem
|
77
|
+
|
78
|
+
|
65
79
|
logger = logging.getLogger(__name__)
|
66
80
|
|
67
81
|
_DEFAULT_SHELL = DEFAULT_SHELL_NAMES[os.name]
|
68
82
|
#: The default configuration descriptor.
|
69
|
-
DEFAULT_CONFIG = {
|
83
|
+
DEFAULT_CONFIG: DefaultConfiguration = {
|
70
84
|
"invocation": {"environment_setup": None, "match": {}},
|
71
85
|
"config": {
|
72
86
|
"machine": socket.gethostname(),
|
@@ -88,29 +102,29 @@ class ConfigOptions:
|
|
88
102
|
"""Application-level options for configuration"""
|
89
103
|
|
90
104
|
#: The default directory.
|
91
|
-
default_directory:
|
105
|
+
default_directory: Path | str
|
92
106
|
#: The environment variable containing the directory name.
|
93
107
|
directory_env_var: str
|
94
108
|
#: The default configuration.
|
95
|
-
default_config:
|
109
|
+
default_config: DefaultConfiguration = field(
|
96
110
|
default_factory=lambda: deepcopy(DEFAULT_CONFIG)
|
97
111
|
)
|
98
112
|
#: Any extra schemas to apply.
|
99
|
-
extra_schemas:
|
113
|
+
extra_schemas: Sequence[Schema] = field(default_factory=list)
|
100
114
|
#: Default directory of known configurations.
|
101
|
-
default_known_configs_dir:
|
115
|
+
default_known_configs_dir: str | None = None
|
116
|
+
_schemas: Sequence[Schema] = field(init=False)
|
117
|
+
_configurable_keys: Sequence[str] = field(init=False)
|
102
118
|
|
103
|
-
def __post_init__(self):
|
104
|
-
|
105
|
-
self._schemas = cfg_schemas
|
106
|
-
self._configurable_keys = cfg_keys
|
119
|
+
def __post_init__(self) -> None:
|
120
|
+
self._schemas, self._configurable_keys = self.init_schemas()
|
107
121
|
|
108
|
-
def init_schemas(self):
|
122
|
+
def init_schemas(self) -> tuple[Sequence[Schema], Sequence[str]]:
|
109
123
|
"""
|
110
124
|
Get allowed configurable keys from config schemas.
|
111
125
|
"""
|
112
|
-
cfg_schemas = [get_schema("config_schema.yaml")
|
113
|
-
cfg_keys = []
|
126
|
+
cfg_schemas = [get_schema("config_schema.yaml"), *self.extra_schemas]
|
127
|
+
cfg_keys: list[str] = []
|
114
128
|
for cfg_schema in cfg_schemas:
|
115
129
|
for rule in cfg_schema.rules:
|
116
130
|
if not rule.path and rule.condition.callable.name == "allowed_keys":
|
@@ -118,7 +132,13 @@ class ConfigOptions:
|
|
118
132
|
|
119
133
|
return (cfg_schemas, cfg_keys)
|
120
134
|
|
121
|
-
def validate(
|
135
|
+
def validate(
|
136
|
+
self,
|
137
|
+
data: T,
|
138
|
+
logger: logging.Logger,
|
139
|
+
metadata: ConfigMetadata | None = None,
|
140
|
+
raise_with_metadata: bool = True,
|
141
|
+
) -> T:
|
122
142
|
"""Validate configuration items of the loaded invocation."""
|
123
143
|
|
124
144
|
logger.debug("Validating configuration...")
|
@@ -140,7 +160,8 @@ class ConfigOptions:
|
|
140
160
|
|
141
161
|
|
142
162
|
class Config:
|
143
|
-
"""
|
163
|
+
"""
|
164
|
+
Application configuration as defined in one or more config files.
|
144
165
|
|
145
166
|
This class supports indexing into the collection of properties via Python dot notation.
|
146
167
|
|
@@ -178,76 +199,25 @@ class Config:
|
|
178
199
|
|
179
200
|
Attributes
|
180
201
|
----------
|
181
|
-
|
182
|
-
The directory containing the configuration file.
|
183
|
-
config_file_name:
|
184
|
-
The name of the configuration file.
|
185
|
-
config_file_path:
|
186
|
-
The full path to the configuration file.
|
187
|
-
config_file_contents:
|
188
|
-
The cached contents of the configuration file.
|
189
|
-
config_key:
|
190
|
-
The primary key to select the configuration within the configuration file.
|
191
|
-
config_schemas:
|
192
|
-
The schemas that apply to the configuration file.
|
193
|
-
host_user_id:
|
194
|
-
User ID as understood by the script.
|
195
|
-
host_user_id_file_path:
|
196
|
-
Where user ID information is stored.
|
197
|
-
invoking_user_id:
|
198
|
-
User ID that created the workflow.
|
199
|
-
machine:
|
200
|
-
Machine to submit to.
|
201
|
-
Mapped to a field in the configuration file.
|
202
|
-
user_name:
|
202
|
+
user_name: str
|
203
203
|
User to submit as.
|
204
204
|
Mapped to a field in the configuration file.
|
205
|
-
user_orcid:
|
205
|
+
user_orcid: str
|
206
206
|
User's ORCID.
|
207
207
|
Mapped to a field in the configuration file.
|
208
|
-
user_affiliation:
|
208
|
+
user_affiliation: str
|
209
209
|
User's institutional affiliation.
|
210
210
|
Mapped to a field in the configuration file.
|
211
|
-
linux_release_file:
|
211
|
+
linux_release_file: str
|
212
212
|
Where to get the description of the Linux release version data.
|
213
213
|
Mapped to a field in the configuration file.
|
214
|
-
|
215
|
-
Where to log to.
|
216
|
-
Mapped to a field in the configuration file.
|
217
|
-
log_file_level:
|
214
|
+
log_file_level: str
|
218
215
|
At what level to do logging to the file.
|
219
216
|
Mapped to a field in the configuration file.
|
220
|
-
log_console_level:
|
217
|
+
log_console_level: str
|
221
218
|
At what level to do logging to the console. Usually coarser than to a file.
|
222
219
|
Mapped to a field in the configuration file.
|
223
|
-
|
224
|
-
Where to get task schemas.
|
225
|
-
Mapped to a field in the configuration file.
|
226
|
-
parameter_sources:
|
227
|
-
Where to get parameter descriptors.
|
228
|
-
Mapped to a field in the configuration file.
|
229
|
-
command_file_sources:
|
230
|
-
Where to get command files.
|
231
|
-
Mapped to a field in the configuration file.
|
232
|
-
environment_sources:
|
233
|
-
Where to get execution environment descriptors.
|
234
|
-
Mapped to a field in the configuration file.
|
235
|
-
default_scheduler:
|
236
|
-
The name of the default scheduler.
|
237
|
-
Mapped to a field in the configuration file.
|
238
|
-
default_shell:
|
239
|
-
The name of the default shell.
|
240
|
-
Mapped to a field in the configuration file.
|
241
|
-
schedulers:
|
242
|
-
Settings for supported scheduler(s).
|
243
|
-
Mapped to a field in the configuration file.
|
244
|
-
shells:
|
245
|
-
Settings for supported shell(s).
|
246
|
-
Mapped to a field in the configuration file.
|
247
|
-
demo_data_dir:
|
248
|
-
Location of demo data.
|
249
|
-
Mapped to a field in the configuration file.
|
250
|
-
demo_data_manifest_file:
|
220
|
+
demo_data_manifest_file: str
|
251
221
|
Where the manifest describing the demo data is.
|
252
222
|
Mapped to a field in the configuration file.
|
253
223
|
"""
|
@@ -258,10 +228,10 @@ class Config:
|
|
258
228
|
config_file: ConfigFile,
|
259
229
|
options: ConfigOptions,
|
260
230
|
logger: logging.Logger,
|
261
|
-
config_key:
|
262
|
-
uid=None,
|
263
|
-
callbacks=None,
|
264
|
-
variables=None,
|
231
|
+
config_key: str | None,
|
232
|
+
uid: str | None = None,
|
233
|
+
callbacks: dict[str, tuple[GetterCallback, ...]] | None = None,
|
234
|
+
variables: dict[str, str] | None = None,
|
265
235
|
**overrides,
|
266
236
|
):
|
267
237
|
self._app = app
|
@@ -281,7 +251,7 @@ class Config:
|
|
281
251
|
)
|
282
252
|
|
283
253
|
# Callbacks are run on get:
|
284
|
-
self._get_callbacks = {
|
254
|
+
self._get_callbacks: dict[str, tuple[GetterCallback, ...]] = {
|
285
255
|
"task_schema_sources": (callback_file_paths,),
|
286
256
|
"environment_sources": (callback_file_paths,),
|
287
257
|
"parameter_sources": (callback_file_paths,),
|
@@ -297,7 +267,7 @@ class Config:
|
|
297
267
|
}
|
298
268
|
|
299
269
|
# Set callbacks are run on set:
|
300
|
-
self._set_callbacks = {
|
270
|
+
self._set_callbacks: dict[str, tuple[SetterCallback, ...]] = {
|
301
271
|
"task_schema_sources": (set_callback_file_paths, check_load_data_files),
|
302
272
|
"environment_sources": (set_callback_file_paths, check_load_data_files),
|
303
273
|
"parameter_sources": (set_callback_file_paths, check_load_data_files),
|
@@ -311,16 +281,15 @@ class Config:
|
|
311
281
|
}
|
312
282
|
|
313
283
|
self._configurable_keys = self._options._configurable_keys
|
314
|
-
self._modified_keys = {}
|
315
|
-
self._unset_keys =
|
284
|
+
self._modified_keys: ConfigDescriptor = {}
|
285
|
+
self._unset_keys: set[str] = set()
|
316
286
|
|
317
|
-
for name in overrides:
|
318
|
-
|
319
|
-
raise ConfigUnknownOverrideError(name=name)
|
287
|
+
if any((unknown := name) not in self._configurable_keys for name in overrides):
|
288
|
+
raise ConfigUnknownOverrideError(name=unknown)
|
320
289
|
|
321
290
|
host_uid, host_uid_file_path = self._get_user_id()
|
322
291
|
|
323
|
-
metadata = {
|
292
|
+
metadata: ConfigMetadata = {
|
324
293
|
"config_directory": self._file.directory,
|
325
294
|
"config_file_name": self._file.path.name,
|
326
295
|
"config_file_path": self._file.path,
|
@@ -339,16 +308,211 @@ class Config:
|
|
339
308
|
metadata=metadata,
|
340
309
|
)
|
341
310
|
|
342
|
-
def __dir__(self):
|
343
|
-
|
311
|
+
def __dir__(self) -> Iterator[str]:
|
312
|
+
yield from super().__dir__()
|
313
|
+
yield from self._all_keys
|
344
314
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
315
|
+
@property
|
316
|
+
def config_directory(self) -> Path:
|
317
|
+
"""
|
318
|
+
The directory containing the configuration file.
|
319
|
+
"""
|
320
|
+
return self._get("config_directory")
|
321
|
+
|
322
|
+
@property
|
323
|
+
def config_file_name(self) -> str:
|
324
|
+
"""
|
325
|
+
The name of the configuration file.
|
326
|
+
"""
|
327
|
+
return self._get("config_file_name")
|
328
|
+
|
329
|
+
@property
|
330
|
+
def config_file_path(self) -> Path:
|
331
|
+
"""
|
332
|
+
The full path to the configuration file.
|
333
|
+
"""
|
334
|
+
return self._get("config_file_path")
|
335
|
+
|
336
|
+
@property
|
337
|
+
def config_file_contents(self) -> str:
|
338
|
+
"""
|
339
|
+
The cached contents of the configuration file.
|
340
|
+
"""
|
341
|
+
return self._get("config_file_contents")
|
342
|
+
|
343
|
+
@property
|
344
|
+
def config_key(self) -> str:
|
345
|
+
"""
|
346
|
+
The primary key to select the configuration within the configuration file.
|
347
|
+
"""
|
348
|
+
return self._get("config_key")
|
349
|
+
|
350
|
+
@property
|
351
|
+
def config_schemas(self) -> Sequence[Schema]:
|
352
|
+
"""
|
353
|
+
The schemas that apply to the configuration file.
|
354
|
+
"""
|
355
|
+
return self._get("config_schemas")
|
356
|
+
|
357
|
+
@property
|
358
|
+
def invoking_user_id(self) -> str:
|
359
|
+
"""
|
360
|
+
User ID that created the workflow.
|
361
|
+
"""
|
362
|
+
return self._get("invoking_user_id")
|
363
|
+
|
364
|
+
@property
|
365
|
+
def host_user_id(self) -> str:
|
366
|
+
"""
|
367
|
+
User ID as understood by the script.
|
368
|
+
"""
|
369
|
+
return self._get("host_user_id")
|
370
|
+
|
371
|
+
@property
|
372
|
+
def host_user_id_file_path(self) -> Path:
|
373
|
+
"""
|
374
|
+
Where user ID information is stored.
|
375
|
+
"""
|
376
|
+
return self._get("host_user_id_file_path")
|
377
|
+
|
378
|
+
@property
|
379
|
+
def machine(self) -> str:
|
380
|
+
"""
|
381
|
+
Machine to submit to.
|
382
|
+
Mapped to a field in the configuration file.
|
383
|
+
"""
|
384
|
+
return self._get("machine")
|
385
|
+
|
386
|
+
@machine.setter
|
387
|
+
def machine(self, value: str):
|
388
|
+
self._set("machine", value)
|
389
|
+
|
390
|
+
@property
|
391
|
+
def log_file_path(self) -> str:
|
392
|
+
"""
|
393
|
+
Where to log to.
|
394
|
+
Mapped to a field in the configuration file.
|
395
|
+
"""
|
396
|
+
return self._get("log_file_path")
|
397
|
+
|
398
|
+
@log_file_path.setter
|
399
|
+
def log_file_path(self, value: str):
|
400
|
+
self._set("log_file_path", value)
|
401
|
+
|
402
|
+
@property
|
403
|
+
def environment_sources(self) -> Sequence[Path]:
|
404
|
+
"""
|
405
|
+
Where to get execution environment descriptors.
|
406
|
+
Mapped to a field in the configuration file.
|
407
|
+
"""
|
408
|
+
return self._get("environment_sources")
|
409
|
+
|
410
|
+
@environment_sources.setter
|
411
|
+
def environment_sources(self, value: Sequence[Path]):
|
412
|
+
self._set("environment_sources", value)
|
413
|
+
|
414
|
+
@property
|
415
|
+
def task_schema_sources(self) -> Sequence[str]:
|
416
|
+
"""
|
417
|
+
Where to get task schemas.
|
418
|
+
Mapped to a field in the configuration file.
|
419
|
+
"""
|
420
|
+
return self._get("task_schema_sources")
|
421
|
+
|
422
|
+
@task_schema_sources.setter
|
423
|
+
def task_schema_sources(self, value: Sequence[str]):
|
424
|
+
self._set("task_schema_sources", value)
|
425
|
+
|
426
|
+
@property
|
427
|
+
def command_file_sources(self) -> Sequence[str]:
|
428
|
+
"""
|
429
|
+
Where to get command files.
|
430
|
+
Mapped to a field in the configuration file.
|
431
|
+
"""
|
432
|
+
return self._get("command_file_sources")
|
433
|
+
|
434
|
+
@command_file_sources.setter
|
435
|
+
def command_file_sources(self, value: Sequence[str]):
|
436
|
+
self._set("command_file_sources", value)
|
437
|
+
|
438
|
+
@property
|
439
|
+
def parameter_sources(self) -> Sequence[str]:
|
440
|
+
"""
|
441
|
+
Where to get parameter descriptors.
|
442
|
+
Mapped to a field in the configuration file.
|
443
|
+
"""
|
444
|
+
return self._get("parameter_sources")
|
445
|
+
|
446
|
+
@parameter_sources.setter
|
447
|
+
def parameter_sources(self, value: Sequence[str]):
|
448
|
+
self._set("parameter_sources", value)
|
449
|
+
|
450
|
+
@property
|
451
|
+
def default_scheduler(self) -> str:
|
452
|
+
"""
|
453
|
+
The name of the default scheduler.
|
454
|
+
Mapped to a field in the configuration file.
|
455
|
+
"""
|
456
|
+
return self._get("default_scheduler")
|
457
|
+
|
458
|
+
@default_scheduler.setter
|
459
|
+
def default_scheduler(self, value: str):
|
460
|
+
self._set("default_scheduler", value)
|
461
|
+
|
462
|
+
@property
|
463
|
+
def default_shell(self) -> str:
|
464
|
+
"""
|
465
|
+
The name of the default shell.
|
466
|
+
Mapped to a field in the configuration file.
|
467
|
+
"""
|
468
|
+
return self._get("default_shell")
|
469
|
+
|
470
|
+
@default_shell.setter
|
471
|
+
def default_shell(self, value: str):
|
472
|
+
self._set("default_shell", value)
|
473
|
+
|
474
|
+
@property
|
475
|
+
def schedulers(self) -> Mapping[str, SchedulerConfigDescriptor]:
|
476
|
+
"""
|
477
|
+
Settings for supported scheduler(s).
|
478
|
+
Mapped to a field in the configuration file.
|
479
|
+
"""
|
480
|
+
return self._get("schedulers")
|
481
|
+
|
482
|
+
@schedulers.setter
|
483
|
+
def schedulers(self, value: Mapping[str, SchedulerConfigDescriptor]):
|
484
|
+
self._set("schedulers", value)
|
485
|
+
|
486
|
+
@property
|
487
|
+
def shells(self) -> Mapping[str, ShellConfigDescriptor]:
|
488
|
+
"""
|
489
|
+
Settings for supported shell(s).
|
490
|
+
Mapped to a field in the configuration file.
|
491
|
+
"""
|
492
|
+
return self._get("shells")
|
493
|
+
|
494
|
+
@shells.setter
|
495
|
+
def shells(self, value: Mapping[str, ShellConfigDescriptor]):
|
496
|
+
self._set("shells", value)
|
497
|
+
|
498
|
+
@property
|
499
|
+
def demo_data_dir(self) -> str | None:
|
500
|
+
"""
|
501
|
+
Location of demo data.
|
502
|
+
Mapped to a field in the configuration file.
|
503
|
+
"""
|
504
|
+
return self._get("demo_data_dir")
|
505
|
+
|
506
|
+
@demo_data_dir.setter
|
507
|
+
def demo_data_dir(self, value: str | None):
|
508
|
+
self._set("demo_data_dir", value)
|
509
|
+
|
510
|
+
def __getattr__(self, name: str):
|
511
|
+
if name.startswith("__"):
|
349
512
|
raise AttributeError(f"Attribute not known: {name!r}.")
|
513
|
+
return self._get(name)
|
350
514
|
|
351
|
-
def __setattr__(self, name, value):
|
515
|
+
def __setattr__(self, name: str, value):
|
352
516
|
if (
|
353
517
|
"_configurable_keys" in self.__dict__
|
354
518
|
and name in self.__dict__["_configurable_keys"]
|
@@ -357,20 +521,25 @@ class Config:
|
|
357
521
|
else:
|
358
522
|
super().__setattr__(name, value)
|
359
523
|
|
360
|
-
def _disable_callbacks(
|
361
|
-
|
524
|
+
def _disable_callbacks(
|
525
|
+
self, callbacks: Sequence[str]
|
526
|
+
) -> tuple[
|
527
|
+
dict[str, tuple[GetterCallback, ...]], dict[str, tuple[SetterCallback, ...]]
|
528
|
+
]:
|
529
|
+
"""
|
530
|
+
Disable named get and set callbacks.
|
362
531
|
|
363
532
|
Returns
|
364
533
|
-------
|
365
534
|
The original get and set callback dictionaries.
|
366
535
|
"""
|
367
536
|
self._logger.info(f"disabling config callbacks: {callbacks!r}")
|
368
|
-
get_callbacks_tmp = {
|
369
|
-
k: tuple(
|
537
|
+
get_callbacks_tmp: dict[str, tuple[GetterCallback, ...]] = {
|
538
|
+
k: tuple(cb for cb in v if cb.__name__ not in callbacks)
|
370
539
|
for k, v in self._get_callbacks.items()
|
371
540
|
}
|
372
|
-
set_callbacks_tmp = {
|
373
|
-
k: tuple(
|
541
|
+
set_callbacks_tmp: dict[str, tuple[SetterCallback, ...]] = {
|
542
|
+
k: tuple(cb for cb in v if cb.__name__ not in callbacks)
|
374
543
|
for k, v in self._set_callbacks.items()
|
375
544
|
}
|
376
545
|
get_callbacks = copy.deepcopy(self._get_callbacks)
|
@@ -380,14 +549,14 @@ class Config:
|
|
380
549
|
return (get_callbacks, set_callbacks)
|
381
550
|
|
382
551
|
@contextlib.contextmanager
|
383
|
-
def _without_callbacks(self, *callbacks):
|
552
|
+
def _without_callbacks(self, *callbacks: str) -> Iterator[None]:
|
384
553
|
"""Context manager to temporarily exclude named get and set callbacks."""
|
385
554
|
get_callbacks, set_callbacks = self._disable_callbacks(*callbacks)
|
386
555
|
yield
|
387
556
|
self._get_callbacks = get_callbacks
|
388
557
|
self._set_callbacks = set_callbacks
|
389
558
|
|
390
|
-
def _validate(self):
|
559
|
+
def _validate(self) -> None:
|
391
560
|
data = self.get_all(include_overrides=True)
|
392
561
|
self._options.validate(
|
393
562
|
data=data,
|
@@ -396,95 +565,117 @@ class Config:
|
|
396
565
|
raise_with_metadata=True,
|
397
566
|
)
|
398
567
|
|
399
|
-
def _resolve_path(self, path):
|
568
|
+
def _resolve_path(self, path: PathLike) -> PathLike:
|
400
569
|
"""Resolve a file path, but leave fsspec protocols alone."""
|
401
|
-
if
|
402
|
-
|
403
|
-
|
404
|
-
if not path.is_absolute():
|
405
|
-
path = self._meta_data["config_directory"].joinpath(path)
|
406
|
-
else:
|
570
|
+
if path is None:
|
571
|
+
return None
|
572
|
+
if any(str(path).startswith(i + ":") for i in fsspec_protocols):
|
407
573
|
self._logger.debug(
|
408
574
|
f"Not resolving path {path!r} because it looks like an `fsspec` URL."
|
409
575
|
)
|
576
|
+
return path
|
577
|
+
real_path = Path(path).expanduser()
|
578
|
+
if real_path.is_absolute():
|
579
|
+
return real_path
|
580
|
+
return self._meta_data["config_directory"].joinpath(real_path)
|
581
|
+
|
582
|
+
def register_config_get_callback(
|
583
|
+
self, name: str
|
584
|
+
) -> Callable[[GetterCallback], GetterCallback]:
|
585
|
+
"""
|
586
|
+
Decorator to register a function as a configuration callback for a specified
|
587
|
+
configuration item name, to be invoked on `get` of the item.
|
588
|
+
"""
|
410
589
|
|
411
|
-
|
412
|
-
|
413
|
-
def register_config_get_callback(self, name):
|
414
|
-
"""Decorator to register a function as a configuration callback for a specified
|
415
|
-
configuration item name, to be invoked on `get` of the item."""
|
416
|
-
|
417
|
-
def decorator(func):
|
590
|
+
def decorator(func: GetterCallback) -> GetterCallback:
|
418
591
|
if name in self._get_callbacks:
|
419
|
-
self._get_callbacks[name] =
|
420
|
-
list(self._get_callbacks[name]) + [func]
|
421
|
-
)
|
592
|
+
self._get_callbacks[name] = self._get_callbacks[name] + (func,)
|
422
593
|
else:
|
423
594
|
self._get_callbacks[name] = (func,)
|
424
595
|
|
425
596
|
@functools.wraps(func)
|
426
|
-
def wrap(value):
|
427
|
-
return func(value)
|
597
|
+
def wrap(config: Config, value: T) -> T:
|
598
|
+
return func(config, value)
|
428
599
|
|
429
600
|
return wrap
|
430
601
|
|
431
602
|
return decorator
|
432
603
|
|
433
|
-
def register_config_set_callback(
|
434
|
-
|
435
|
-
|
604
|
+
def register_config_set_callback(
|
605
|
+
self, name: str
|
606
|
+
) -> Callable[[SetterCallback], SetterCallback]:
|
607
|
+
"""
|
608
|
+
Decorator to register a function as a configuration callback for a specified
|
609
|
+
configuration item name, to be invoked on `set` of the item.
|
610
|
+
"""
|
436
611
|
|
437
|
-
def decorator(func):
|
612
|
+
def decorator(func: SetterCallback) -> SetterCallback:
|
438
613
|
if name in self._set_callbacks:
|
439
|
-
self._set_callbacks[name] =
|
440
|
-
list(self._set_callbacks[name]) + [func]
|
441
|
-
)
|
614
|
+
self._set_callbacks[name] = self._set_callbacks[name] + (func,)
|
442
615
|
else:
|
443
616
|
self._set_callbacks[name] = (func,)
|
444
617
|
|
445
618
|
@functools.wraps(func)
|
446
|
-
def wrap(value):
|
447
|
-
return func(value)
|
619
|
+
def wrap(config: Config, value: T) -> Any:
|
620
|
+
return func(config, value)
|
448
621
|
|
449
622
|
return wrap
|
450
623
|
|
451
624
|
return decorator
|
452
625
|
|
453
626
|
@property
|
454
|
-
def _all_keys(self):
|
455
|
-
return self._configurable_keys
|
456
|
-
|
457
|
-
|
627
|
+
def _all_keys(self) -> list[str]:
|
628
|
+
return [*self._configurable_keys, *self._meta_data]
|
629
|
+
|
630
|
+
@overload
|
631
|
+
def get_all(
|
632
|
+
self, *, include_overrides: bool = True, as_str: Literal[True]
|
633
|
+
) -> Mapping[str, str]:
|
634
|
+
...
|
635
|
+
|
636
|
+
@overload
|
637
|
+
def get_all(
|
638
|
+
self, *, include_overrides: bool = True, as_str: Literal[False] = False
|
639
|
+
) -> Mapping[str, Any]:
|
640
|
+
...
|
641
|
+
|
642
|
+
def get_all(
|
643
|
+
self, *, include_overrides: bool = True, as_str: bool = False
|
644
|
+
) -> Mapping[str, Any]:
|
458
645
|
"""Get all configurable items."""
|
459
|
-
items = {}
|
646
|
+
items: dict[str, Any] = {}
|
460
647
|
for key in self._configurable_keys:
|
461
648
|
if key in self._unset_keys:
|
462
649
|
continue
|
463
|
-
|
464
|
-
|
465
|
-
|
650
|
+
try:
|
651
|
+
if as_str:
|
652
|
+
items[key] = self._get(
|
466
653
|
name=key,
|
467
654
|
include_overrides=include_overrides,
|
468
655
|
raise_on_missing=True,
|
469
|
-
as_str=
|
656
|
+
as_str=True,
|
470
657
|
)
|
471
|
-
|
472
|
-
|
473
|
-
|
658
|
+
else:
|
659
|
+
items[key] = self._get(
|
660
|
+
name=key,
|
661
|
+
include_overrides=include_overrides,
|
662
|
+
raise_on_missing=True,
|
663
|
+
)
|
664
|
+
except ValueError:
|
665
|
+
continue
|
474
666
|
return items
|
475
667
|
|
476
|
-
def _show(self, config=True, metadata=False):
|
477
|
-
group_args = []
|
668
|
+
def _show(self, config: bool = True, metadata: bool = False):
|
669
|
+
group_args: list[Panel] = []
|
478
670
|
if metadata:
|
479
|
-
|
480
|
-
|
481
|
-
|
671
|
+
tab = Table(show_header=False, box=None)
|
672
|
+
tab.add_column()
|
673
|
+
tab.add_column()
|
482
674
|
for k, v in self._meta_data.items():
|
483
675
|
if k == "config_file_contents":
|
484
676
|
continue
|
485
|
-
|
486
|
-
|
487
|
-
group_args.append(panel_md)
|
677
|
+
tab.add_row(k, Pretty(v))
|
678
|
+
group_args.append(Panel(tab, title="Config metadata"))
|
488
679
|
|
489
680
|
if config:
|
490
681
|
tab = Table(show_header=False, box=None)
|
@@ -492,15 +683,13 @@ class Config:
|
|
492
683
|
tab.add_column()
|
493
684
|
for k, v in self.get_all().items():
|
494
685
|
tab.add_row(k, Pretty(v))
|
495
|
-
|
496
|
-
group_args.append(panel)
|
686
|
+
group_args.append(Panel(tab, title=f"Config {self._config_key!r}"))
|
497
687
|
|
498
|
-
|
499
|
-
rich_print(group)
|
688
|
+
rich_print(Group(*group_args))
|
500
689
|
|
501
|
-
def _get_callback_value(self, name, value):
|
690
|
+
def _get_callback_value(self, name: str, value):
|
502
691
|
if name in self._get_callbacks and value is not None:
|
503
|
-
for cb in self._get_callbacks.get(name,
|
692
|
+
for cb in self._get_callbacks.get(name, ()):
|
504
693
|
self._logger.debug(
|
505
694
|
f"Invoking `config.get` callback ({cb.__name__!r}) for item {name!r}={value!r}"
|
506
695
|
)
|
@@ -510,9 +699,36 @@ class Config:
|
|
510
699
|
raise ConfigItemCallbackError(name, cb, err) from None
|
511
700
|
return value
|
512
701
|
|
702
|
+
@overload
|
703
|
+
def _get(
|
704
|
+
self,
|
705
|
+
name: str,
|
706
|
+
*,
|
707
|
+
include_overrides=True,
|
708
|
+
raise_on_missing=False,
|
709
|
+
as_str: Literal[False] = False,
|
710
|
+
callback=True,
|
711
|
+
default_value=None,
|
712
|
+
) -> Any:
|
713
|
+
...
|
714
|
+
|
715
|
+
@overload
|
716
|
+
def _get(
|
717
|
+
self,
|
718
|
+
name: str,
|
719
|
+
*,
|
720
|
+
include_overrides=True,
|
721
|
+
raise_on_missing=False,
|
722
|
+
as_str: Literal[True],
|
723
|
+
callback=True,
|
724
|
+
default_value=None,
|
725
|
+
) -> list[str] | str:
|
726
|
+
...
|
727
|
+
|
513
728
|
def _get(
|
514
729
|
self,
|
515
|
-
name,
|
730
|
+
name: str,
|
731
|
+
*,
|
516
732
|
include_overrides=True,
|
517
733
|
raise_on_missing=False,
|
518
734
|
as_str=False,
|
@@ -525,7 +741,7 @@ class Config:
|
|
525
741
|
raise ConfigUnknownItemError(name=name)
|
526
742
|
|
527
743
|
elif name in self._meta_data:
|
528
|
-
val = self._meta_data[name]
|
744
|
+
val = cast("dict", self._meta_data)[name]
|
529
745
|
|
530
746
|
elif include_overrides and name in self._overrides:
|
531
747
|
val = self._overrides[name]
|
@@ -538,7 +754,7 @@ class Config:
|
|
538
754
|
val = default_value
|
539
755
|
|
540
756
|
elif name in self._modified_keys:
|
541
|
-
val = self._modified_keys[name]
|
757
|
+
val = cast("dict", self._modified_keys)[name]
|
542
758
|
|
543
759
|
elif name in self._configurable_keys:
|
544
760
|
val = self._file.get_config_item(
|
@@ -553,78 +769,119 @@ class Config:
|
|
553
769
|
|
554
770
|
if as_str:
|
555
771
|
if isinstance(val, (list, tuple, set)):
|
556
|
-
|
772
|
+
return [str(i) for i in val]
|
557
773
|
else:
|
558
|
-
|
774
|
+
return str(val)
|
559
775
|
|
560
776
|
return val
|
561
777
|
|
562
|
-
def _parse_JSON(self, name, value):
|
778
|
+
def _parse_JSON(self, name: str, value: str) -> Any:
|
563
779
|
try:
|
564
|
-
|
780
|
+
return json.loads(value)
|
565
781
|
except json.decoder.JSONDecodeError as err:
|
566
782
|
raise ConfigChangeInvalidJSONError(name=name, json_str=value, err=err)
|
567
|
-
return value
|
568
783
|
|
569
|
-
|
784
|
+
@overload
|
785
|
+
def _set(
|
786
|
+
self, name: str, value: str, *, is_json: Literal[True], callback=True, quiet=False
|
787
|
+
) -> None:
|
788
|
+
...
|
789
|
+
|
790
|
+
@overload
|
791
|
+
def _set(
|
792
|
+
self,
|
793
|
+
name: str,
|
794
|
+
value: Any,
|
795
|
+
*,
|
796
|
+
is_json: Literal[False] = False,
|
797
|
+
callback=True,
|
798
|
+
quiet=False,
|
799
|
+
) -> None:
|
800
|
+
...
|
801
|
+
|
802
|
+
def _set(
|
803
|
+
self, name: str, value, *, is_json=False, callback=True, quiet=False
|
804
|
+
) -> None:
|
805
|
+
"""
|
806
|
+
Set a configuration item.
|
807
|
+
"""
|
570
808
|
if name not in self._configurable_keys:
|
571
809
|
raise ConfigNonConfigurableError(name=name)
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
if callback_val != current_val:
|
581
|
-
was_in_modified = False
|
582
|
-
was_in_unset = False
|
583
|
-
prev_modified_val = None
|
584
|
-
modified_updated = False
|
585
|
-
|
586
|
-
if name in self._modified_keys:
|
587
|
-
was_in_modified = True
|
588
|
-
prev_modified_val = self._modified_keys[name]
|
589
|
-
|
590
|
-
if name in self._unset_keys:
|
591
|
-
was_in_unset = True
|
592
|
-
idx = self._unset_keys.index(name)
|
593
|
-
self._unset_keys.pop(idx)
|
594
|
-
|
595
|
-
if callback_val != file_val:
|
596
|
-
self._modified_keys[name] = value
|
597
|
-
modified_updated = True
|
810
|
+
if is_json:
|
811
|
+
value = self._parse_JSON(name, cast("str", value))
|
812
|
+
current_val = self._get(name)
|
813
|
+
callback_val = self._get_callback_value(name, value)
|
814
|
+
file_val = self._get_callback_value(
|
815
|
+
name, self._file.get_config_item(self._config_key, name)
|
816
|
+
)
|
598
817
|
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
818
|
+
if callback_val != current_val:
|
819
|
+
was_in_modified = False
|
820
|
+
was_in_unset = False
|
821
|
+
prev_modified_val = None
|
822
|
+
modified_updated = False
|
823
|
+
mk = cast("dict", self._modified_keys)
|
824
|
+
|
825
|
+
if name in self._modified_keys:
|
826
|
+
was_in_modified = True
|
827
|
+
prev_modified_val = mk[name]
|
828
|
+
|
829
|
+
if name in self._unset_keys:
|
830
|
+
was_in_unset = True
|
831
|
+
self._unset_keys.remove(name)
|
832
|
+
|
833
|
+
if callback_val != file_val:
|
834
|
+
mk[name] = value
|
835
|
+
modified_updated = True
|
836
|
+
|
837
|
+
try:
|
838
|
+
self._validate()
|
839
|
+
|
840
|
+
if callback:
|
841
|
+
for cb in self._set_callbacks.get(name, ()):
|
842
|
+
self._logger.debug(
|
843
|
+
f"Invoking `config.set` callback for item {name!r}: {cb.__name__!r}"
|
844
|
+
)
|
845
|
+
cb(self, callback_val)
|
846
|
+
|
847
|
+
except ConfigValidationError as err:
|
848
|
+
# revert:
|
849
|
+
if modified_updated:
|
850
|
+
if was_in_modified:
|
851
|
+
mk[name] = prev_modified_val
|
852
|
+
else:
|
853
|
+
del mk[name]
|
854
|
+
if was_in_unset:
|
855
|
+
self._unset_keys.add(name)
|
856
|
+
|
857
|
+
raise ConfigChangeValidationError(name, validation_err=err) from None
|
620
858
|
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
859
|
+
self._logger.debug(
|
860
|
+
f"Successfully set config item {name!r} to {callback_val!r}."
|
861
|
+
)
|
862
|
+
elif not quiet:
|
863
|
+
print(f"value is already: {callback_val!r}")
|
626
864
|
|
627
|
-
|
865
|
+
@overload
|
866
|
+
def set(
|
867
|
+
self,
|
868
|
+
path: str,
|
869
|
+
value: Any,
|
870
|
+
*,
|
871
|
+
is_json: Literal[False] = False,
|
872
|
+
quiet: bool = False,
|
873
|
+
) -> None:
|
874
|
+
...
|
875
|
+
|
876
|
+
@overload
|
877
|
+
def set(
|
878
|
+
self, path: str, value: str, *, is_json: Literal[True], quiet: bool = False
|
879
|
+
) -> None:
|
880
|
+
...
|
881
|
+
|
882
|
+
def set(
|
883
|
+
self, path: str, value: Any, *, is_json: bool = False, quiet: bool = False
|
884
|
+
) -> None:
|
628
885
|
"""
|
629
886
|
Set the value of a configuration item.
|
630
887
|
|
@@ -640,16 +897,15 @@ class Config:
|
|
640
897
|
if is_json:
|
641
898
|
value = self._parse_JSON(path, value)
|
642
899
|
|
643
|
-
|
644
|
-
name = parts[0]
|
900
|
+
name, *path_suffix = path.split(".")
|
645
901
|
root = deepcopy(self._get(name, callback=False))
|
646
|
-
if
|
902
|
+
if path_suffix:
|
647
903
|
if root is None:
|
648
904
|
root = {}
|
649
|
-
self.set(path=
|
905
|
+
self.set(path=name, value={}, quiet=True)
|
650
906
|
set_in_container(
|
651
907
|
root,
|
652
|
-
path=
|
908
|
+
path=path_suffix,
|
653
909
|
value=value,
|
654
910
|
ensure_path=True,
|
655
911
|
cast_indices=True,
|
@@ -658,13 +914,13 @@ class Config:
|
|
658
914
|
root = value
|
659
915
|
self._set(name, root, quiet=quiet)
|
660
916
|
|
661
|
-
def unset(self, name):
|
917
|
+
def unset(self, name: str) -> None:
|
662
918
|
"""
|
663
919
|
Unset the value of a configuration item.
|
664
920
|
|
665
921
|
Parameters
|
666
922
|
----------
|
667
|
-
name:
|
923
|
+
name:
|
668
924
|
The name of the configuration item.
|
669
925
|
|
670
926
|
Notes
|
@@ -676,48 +932,67 @@ class Config:
|
|
676
932
|
if name in self._unset_keys or not self._file.is_item_set(self._config_key, name):
|
677
933
|
raise ConfigItemAlreadyUnsetError(name=name)
|
678
934
|
|
679
|
-
self._unset_keys.
|
935
|
+
self._unset_keys.add(name)
|
680
936
|
try:
|
681
937
|
self._validate()
|
682
938
|
except ConfigValidationError as err:
|
683
|
-
self._unset_keys.
|
939
|
+
self._unset_keys.remove(name)
|
684
940
|
raise ConfigChangeValidationError(name, validation_err=err) from None
|
685
941
|
|
942
|
+
@overload
|
686
943
|
def get(
|
687
944
|
self,
|
688
|
-
path,
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
default=None,
|
694
|
-
):
|
945
|
+
path: str,
|
946
|
+
*,
|
947
|
+
callback: bool = True,
|
948
|
+
copy: bool = False,
|
949
|
+
ret_root_and_parts: Literal[False] = False,
|
950
|
+
default: Any | None = None,
|
951
|
+
) -> Any:
|
952
|
+
...
|
953
|
+
|
954
|
+
@overload
|
955
|
+
def get(
|
956
|
+
self,
|
957
|
+
path: str,
|
958
|
+
*,
|
959
|
+
callback: bool = True,
|
960
|
+
copy: bool = False,
|
961
|
+
ret_root_and_parts: Literal[True],
|
962
|
+
default: Any | None = None,
|
963
|
+
) -> tuple[Any, Any, list[str]]:
|
964
|
+
...
|
965
|
+
|
966
|
+
def get(
|
967
|
+
self,
|
968
|
+
path: str,
|
969
|
+
*,
|
970
|
+
callback: bool = True,
|
971
|
+
copy: bool = False,
|
972
|
+
ret_root_and_parts: bool = False,
|
973
|
+
default: Any | None = None,
|
974
|
+
) -> Any:
|
695
975
|
"""
|
696
976
|
Get the value of a configuration item.
|
697
977
|
|
698
978
|
Parameters
|
699
979
|
----------
|
700
|
-
path:
|
980
|
+
path:
|
701
981
|
The name of or path to the configuration item.
|
702
982
|
"""
|
703
|
-
parts = path.split(".")
|
704
|
-
root = deepcopy(self._get(
|
983
|
+
name, *suffix = parts = path.split(".")
|
984
|
+
root = deepcopy(self._get(name, callback=callback))
|
705
985
|
try:
|
706
|
-
out = get_in_container(root,
|
986
|
+
out = get_in_container(root, suffix, cast_indices=True)
|
707
987
|
except KeyError:
|
708
988
|
out = default
|
709
989
|
if copy:
|
710
990
|
out = deepcopy(out)
|
711
|
-
if not
|
991
|
+
if not ret_root_and_parts:
|
712
992
|
return out
|
713
|
-
|
714
|
-
if ret_root:
|
715
|
-
ret += [root]
|
716
|
-
if ret_parts:
|
717
|
-
ret += [parts]
|
718
|
-
return tuple(ret)
|
993
|
+
return out, root, parts
|
719
994
|
|
720
|
-
def append(self, path, value, is_json=False):
|
995
|
+
def append(self, path: str, value, *, is_json: bool = False) -> None:
|
721
996
|
"""
|
722
997
|
Append a value to a list-like configuration item.
|
723
998
|
|
@@ -733,8 +1008,7 @@ class Config:
|
|
733
1008
|
|
734
1009
|
existing, root, parts = self.get(
|
735
1010
|
path,
|
736
|
-
|
737
|
-
ret_parts=True,
|
1011
|
+
ret_root_and_parts=True,
|
738
1012
|
callback=False,
|
739
1013
|
default=[],
|
740
1014
|
)
|
@@ -756,7 +1030,7 @@ class Config:
|
|
756
1030
|
root = new
|
757
1031
|
self._set(parts[0], root)
|
758
1032
|
|
759
|
-
def prepend(self, path, value, is_json=False):
|
1033
|
+
def prepend(self, path: str, value, *, is_json: bool = False) -> None:
|
760
1034
|
"""
|
761
1035
|
Prepend a value to a list-like configuration item.
|
762
1036
|
|
@@ -771,7 +1045,7 @@ class Config:
|
|
771
1045
|
value = self._parse_JSON(path, value)
|
772
1046
|
|
773
1047
|
existing, root, parts = self.get(
|
774
|
-
path,
|
1048
|
+
path, ret_root_and_parts=True, callback=False, default=[]
|
775
1049
|
)
|
776
1050
|
|
777
1051
|
try:
|
@@ -791,7 +1065,7 @@ class Config:
|
|
791
1065
|
root = new
|
792
1066
|
self._set(parts[0], root)
|
793
1067
|
|
794
|
-
def pop(self, path, index):
|
1068
|
+
def pop(self, path: str, index) -> None:
|
795
1069
|
"""
|
796
1070
|
Remove a value from a specified index of a list-like configuration item.
|
797
1071
|
|
@@ -802,11 +1076,9 @@ class Config:
|
|
802
1076
|
index: int
|
803
1077
|
Where to remove the value from. 0 for the first item, -1 for the last.
|
804
1078
|
"""
|
805
|
-
|
806
1079
|
existing, root, parts = self.get(
|
807
1080
|
path,
|
808
|
-
|
809
|
-
ret_parts=True,
|
1081
|
+
ret_root_and_parts=True,
|
810
1082
|
callback=False,
|
811
1083
|
default=[],
|
812
1084
|
)
|
@@ -832,8 +1104,9 @@ class Config:
|
|
832
1104
|
root = new
|
833
1105
|
self._set(parts[0], root)
|
834
1106
|
|
835
|
-
def update(self, path: str, value, is_json=False):
|
836
|
-
"""
|
1107
|
+
def update(self, path: str, value, *, is_json: bool = False) -> None:
|
1108
|
+
"""
|
1109
|
+
Update a map-like configuration item.
|
837
1110
|
|
838
1111
|
Parameters
|
839
1112
|
----------
|
@@ -842,15 +1115,13 @@ class Config:
|
|
842
1115
|
value: dict
|
843
1116
|
A dictionary to merge in.
|
844
1117
|
"""
|
845
|
-
|
846
1118
|
if is_json:
|
847
1119
|
value = self._parse_JSON(path, value)
|
848
1120
|
|
849
1121
|
val_mod, root, parts = self.get(
|
850
1122
|
path,
|
851
1123
|
copy=True,
|
852
|
-
|
853
|
-
ret_parts=True,
|
1124
|
+
ret_root_and_parts=True,
|
854
1125
|
callback=False,
|
855
1126
|
default={},
|
856
1127
|
)
|
@@ -872,20 +1143,22 @@ class Config:
|
|
872
1143
|
root = val_mod
|
873
1144
|
self._set(parts[0], root)
|
874
1145
|
|
875
|
-
def save(self):
|
1146
|
+
def save(self) -> None:
|
876
1147
|
"""Save any modified/unset configuration items into the file."""
|
877
1148
|
if not self._modified_keys and not self._unset_keys:
|
878
1149
|
print("No modifications to save!")
|
879
1150
|
else:
|
880
1151
|
self._file.save()
|
881
1152
|
|
882
|
-
def get_configurable(self):
|
1153
|
+
def get_configurable(self) -> Sequence[str]:
|
883
1154
|
"""Get a list of all configurable keys."""
|
884
1155
|
return self._configurable_keys
|
885
1156
|
|
886
|
-
def _get_user_id(self):
|
887
|
-
"""
|
888
|
-
|
1157
|
+
def _get_user_id(self) -> tuple[str, Path]:
|
1158
|
+
"""
|
1159
|
+
Retrieve (and set if non-existent) a unique user ID that is independent of the
|
1160
|
+
config directory.
|
1161
|
+
"""
|
889
1162
|
|
890
1163
|
uid_file_path = self._app.user_data_dir.joinpath("user_id.txt")
|
891
1164
|
if not uid_file_path.exists():
|
@@ -898,12 +1171,12 @@ class Config:
|
|
898
1171
|
|
899
1172
|
return uid, uid_file_path
|
900
1173
|
|
901
|
-
def reset(self):
|
1174
|
+
def reset(self) -> None:
|
902
1175
|
"""Reset to the default configuration."""
|
903
|
-
self._logger.info(
|
1176
|
+
self._logger.info("Resetting config file to defaults.")
|
904
1177
|
self._app.reset_config()
|
905
1178
|
|
906
|
-
def add_scheduler(self, scheduler, **defaults):
|
1179
|
+
def add_scheduler(self, scheduler: str, **defaults) -> None:
|
907
1180
|
"""
|
908
1181
|
Add a scheduler.
|
909
1182
|
"""
|
@@ -912,7 +1185,7 @@ class Config:
|
|
912
1185
|
return
|
913
1186
|
self.update(f"schedulers.{scheduler}.defaults", defaults)
|
914
1187
|
|
915
|
-
def add_shell(self, shell, **defaults):
|
1188
|
+
def add_shell(self, shell: str, **defaults) -> None:
|
916
1189
|
"""
|
917
1190
|
Add a shell.
|
918
1191
|
"""
|
@@ -923,7 +1196,7 @@ class Config:
|
|
923
1196
|
self.add_scheduler("direct_posix")
|
924
1197
|
self.update(f"shells.{shell}.defaults", defaults)
|
925
1198
|
|
926
|
-
def add_shell_WSL(self, **defaults):
|
1199
|
+
def add_shell_WSL(self, **defaults) -> None:
|
927
1200
|
"""
|
928
1201
|
Add shell with WSL prefix.
|
929
1202
|
"""
|
@@ -931,8 +1204,11 @@ class Config:
|
|
931
1204
|
defaults["WSL_executable"] = "wsl.exe"
|
932
1205
|
self.add_shell("wsl", **defaults)
|
933
1206
|
|
934
|
-
def import_from_file(
|
935
|
-
|
1207
|
+
def import_from_file(
|
1208
|
+
self, file_path: Path | str, *, rename=True, make_new=False
|
1209
|
+
) -> None:
|
1210
|
+
"""
|
1211
|
+
Import config items from a (remote or local) YAML file. Existing config items
|
936
1212
|
of the same names will be overwritten.
|
937
1213
|
|
938
1214
|
Parameters
|
@@ -947,17 +1223,12 @@ class Config:
|
|
947
1223
|
If True, add the config items as a new config, rather than modifying the
|
948
1224
|
current config. The name of the new config will be the stem of the file
|
949
1225
|
specified in `file_path`.
|
950
|
-
|
951
1226
|
"""
|
952
|
-
|
953
1227
|
self._logger.debug(f"import from file: {file_path!r}")
|
954
1228
|
|
955
1229
|
console = Console()
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
try:
|
960
|
-
file_dat = read_YAML_file(file_path)
|
1230
|
+
with console.status(f"Importing config from file {file_path!r}...") as status:
|
1231
|
+
file_dat: DefaultConfiguration = read_YAML_file(file_path)
|
961
1232
|
if rename or make_new:
|
962
1233
|
file_stem = Path(file_path).stem
|
963
1234
|
name = file_stem
|
@@ -992,72 +1263,68 @@ class Config:
|
|
992
1263
|
)
|
993
1264
|
|
994
1265
|
new_invoc = file_dat.get("invocation")
|
995
|
-
new_config = file_dat.get("config")
|
1266
|
+
new_config = file_dat.get("config", {})
|
996
1267
|
|
997
|
-
if new_invoc:
|
1268
|
+
if new_invoc is not None:
|
998
1269
|
status.update("Updating invocation details...")
|
999
1270
|
config_key = file_stem if (make_new or rename) else self._config_key
|
1000
1271
|
obj._file.update_invocation(
|
1001
1272
|
config_key=config_key,
|
1002
1273
|
environment_setup=new_invoc.get("environment_setup"),
|
1003
|
-
match=new_invoc.get("match"),
|
1274
|
+
match=new_invoc.get("match", {}),
|
1004
1275
|
)
|
1005
1276
|
|
1006
1277
|
# sort in reverse so "schedulers" and "shells" are set before
|
1007
1278
|
# "default_scheduler" and "default_shell" which might reference the former:
|
1008
|
-
|
1009
|
-
for k, v in new_config.items():
|
1279
|
+
for k, v in sorted(new_config.items(), reverse=True):
|
1010
1280
|
status.update(f"Updating configurable item {k!r}")
|
1011
1281
|
obj.set(k, value=v, quiet=True)
|
1012
1282
|
|
1013
1283
|
obj.save()
|
1014
1284
|
|
1015
|
-
except Exception:
|
1016
|
-
status.stop()
|
1017
|
-
raise
|
1018
|
-
|
1019
|
-
status.stop()
|
1020
1285
|
print(f"Config {name!r} updated.")
|
1021
1286
|
|
1022
|
-
def init(self, known_name: str, path:
|
1287
|
+
def init(self, known_name: str, path: str | None = None) -> None:
|
1023
1288
|
"""Configure from a known importable config."""
|
1024
1289
|
if not path:
|
1025
|
-
path
|
1026
|
-
if not path:
|
1290
|
+
if not (path := self._options.default_known_configs_dir):
|
1027
1291
|
raise ValueError("Specify an `path` to search for known config files.")
|
1028
1292
|
elif path == ".":
|
1029
1293
|
path = str(Path(path).resolve())
|
1030
1294
|
|
1031
1295
|
self._logger.debug(f"init with `path` = {path!r}")
|
1032
1296
|
|
1033
|
-
fs = fsspec.open(path).fs
|
1297
|
+
fs: AbstractFileSystem = fsspec.open(path).fs
|
1034
1298
|
local_path = f"{path}/" if isinstance(fs, LocalFileSystem) else ""
|
1035
1299
|
files = fs.glob(f"{local_path}*.yaml") + fs.glob(f"{local_path}*.yml")
|
1036
1300
|
self._logger.debug(f"All YAML files found in file-system {fs!r}: {files}")
|
1037
1301
|
|
1038
|
-
files
|
1039
|
-
if not files:
|
1302
|
+
if not (files := [i for i in files if Path(i).stem.startswith(known_name)]):
|
1040
1303
|
print(f"No configuration-import files found matching name {known_name!r}.")
|
1041
1304
|
return
|
1042
1305
|
|
1043
1306
|
print(f"Found configuration-import files: {files!r}")
|
1044
|
-
for
|
1045
|
-
self.import_from_file(file_path=
|
1307
|
+
for file_i in files:
|
1308
|
+
self.import_from_file(file_path=file_i, make_new=True)
|
1046
1309
|
|
1047
|
-
print(
|
1310
|
+
print("imports complete")
|
1048
1311
|
# if current config is named "default", rename machine to DEFAULT_CONFIG:
|
1049
1312
|
if self._config_key == "default":
|
1050
1313
|
self.set("machine", "DEFAULT_MACHINE")
|
1051
1314
|
self.save()
|
1052
1315
|
|
1053
|
-
def set_github_demo_data_dir(self, sha):
|
1054
|
-
"""
|
1316
|
+
def set_github_demo_data_dir(self, sha: str) -> None:
|
1317
|
+
"""
|
1318
|
+
Set the `demo_data_dir` item, to an fsspec Github URL.
|
1055
1319
|
|
1056
1320
|
We use this (via the CLI) when testing the frozen app on Github, because, by
|
1057
1321
|
default, the SHA is set to the current version tag, which might not include recent
|
1058
1322
|
changes to the demo data.
|
1059
|
-
|
1060
1323
|
"""
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1324
|
+
assert self._app.demo_data_dir is not None
|
1325
|
+
self.set(
|
1326
|
+
"demo_data_dir",
|
1327
|
+
self._app._get_github_url(
|
1328
|
+
sha=sha, path=self._app.demo_data_dir.replace(".", "/")
|
1329
|
+
),
|
1330
|
+
)
|