hpcflow-new2 0.2.0a188__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.
Files changed (115) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  5. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  6. hpcflow/sdk/__init__.py +21 -15
  7. hpcflow/sdk/app.py +2133 -770
  8. hpcflow/sdk/cli.py +281 -250
  9. hpcflow/sdk/cli_common.py +6 -2
  10. hpcflow/sdk/config/__init__.py +1 -1
  11. hpcflow/sdk/config/callbacks.py +77 -42
  12. hpcflow/sdk/config/cli.py +126 -103
  13. hpcflow/sdk/config/config.py +578 -311
  14. hpcflow/sdk/config/config_file.py +131 -95
  15. hpcflow/sdk/config/errors.py +112 -85
  16. hpcflow/sdk/config/types.py +145 -0
  17. hpcflow/sdk/core/actions.py +1054 -994
  18. hpcflow/sdk/core/app_aware.py +24 -0
  19. hpcflow/sdk/core/cache.py +81 -63
  20. hpcflow/sdk/core/command_files.py +275 -185
  21. hpcflow/sdk/core/commands.py +111 -107
  22. hpcflow/sdk/core/element.py +724 -503
  23. hpcflow/sdk/core/enums.py +192 -0
  24. hpcflow/sdk/core/environment.py +74 -93
  25. hpcflow/sdk/core/errors.py +398 -51
  26. hpcflow/sdk/core/json_like.py +540 -272
  27. hpcflow/sdk/core/loop.py +380 -334
  28. hpcflow/sdk/core/loop_cache.py +160 -43
  29. hpcflow/sdk/core/object_list.py +370 -207
  30. hpcflow/sdk/core/parameters.py +728 -600
  31. hpcflow/sdk/core/rule.py +59 -41
  32. hpcflow/sdk/core/run_dir_files.py +33 -22
  33. hpcflow/sdk/core/task.py +1546 -1325
  34. hpcflow/sdk/core/task_schema.py +240 -196
  35. hpcflow/sdk/core/test_utils.py +126 -88
  36. hpcflow/sdk/core/types.py +387 -0
  37. hpcflow/sdk/core/utils.py +410 -305
  38. hpcflow/sdk/core/validation.py +82 -9
  39. hpcflow/sdk/core/workflow.py +1192 -1028
  40. hpcflow/sdk/core/zarr_io.py +98 -137
  41. hpcflow/sdk/demo/cli.py +46 -33
  42. hpcflow/sdk/helper/cli.py +18 -16
  43. hpcflow/sdk/helper/helper.py +75 -63
  44. hpcflow/sdk/helper/watcher.py +61 -28
  45. hpcflow/sdk/log.py +83 -59
  46. hpcflow/sdk/persistence/__init__.py +8 -31
  47. hpcflow/sdk/persistence/base.py +988 -586
  48. hpcflow/sdk/persistence/defaults.py +6 -0
  49. hpcflow/sdk/persistence/discovery.py +38 -0
  50. hpcflow/sdk/persistence/json.py +408 -153
  51. hpcflow/sdk/persistence/pending.py +158 -123
  52. hpcflow/sdk/persistence/store_resource.py +37 -22
  53. hpcflow/sdk/persistence/types.py +307 -0
  54. hpcflow/sdk/persistence/utils.py +14 -11
  55. hpcflow/sdk/persistence/zarr.py +477 -420
  56. hpcflow/sdk/runtime.py +44 -41
  57. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  58. hpcflow/sdk/submission/jobscript.py +444 -404
  59. hpcflow/sdk/submission/schedulers/__init__.py +133 -40
  60. hpcflow/sdk/submission/schedulers/direct.py +97 -71
  61. hpcflow/sdk/submission/schedulers/sge.py +132 -126
  62. hpcflow/sdk/submission/schedulers/slurm.py +263 -268
  63. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  64. hpcflow/sdk/submission/shells/__init__.py +14 -15
  65. hpcflow/sdk/submission/shells/base.py +102 -29
  66. hpcflow/sdk/submission/shells/bash.py +72 -55
  67. hpcflow/sdk/submission/shells/os_version.py +31 -30
  68. hpcflow/sdk/submission/shells/powershell.py +37 -29
  69. hpcflow/sdk/submission/submission.py +203 -257
  70. hpcflow/sdk/submission/types.py +143 -0
  71. hpcflow/sdk/typing.py +163 -12
  72. hpcflow/tests/conftest.py +8 -6
  73. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  74. hpcflow/tests/scripts/test_main_scripts.py +60 -30
  75. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
  76. hpcflow/tests/unit/test_action.py +86 -75
  77. hpcflow/tests/unit/test_action_rule.py +9 -4
  78. hpcflow/tests/unit/test_app.py +13 -6
  79. hpcflow/tests/unit/test_cli.py +1 -1
  80. hpcflow/tests/unit/test_command.py +71 -54
  81. hpcflow/tests/unit/test_config.py +20 -15
  82. hpcflow/tests/unit/test_config_file.py +21 -18
  83. hpcflow/tests/unit/test_element.py +58 -62
  84. hpcflow/tests/unit/test_element_iteration.py +3 -1
  85. hpcflow/tests/unit/test_element_set.py +29 -19
  86. hpcflow/tests/unit/test_group.py +4 -2
  87. hpcflow/tests/unit/test_input_source.py +116 -93
  88. hpcflow/tests/unit/test_input_value.py +29 -24
  89. hpcflow/tests/unit/test_json_like.py +44 -35
  90. hpcflow/tests/unit/test_loop.py +65 -58
  91. hpcflow/tests/unit/test_object_list.py +17 -12
  92. hpcflow/tests/unit/test_parameter.py +16 -7
  93. hpcflow/tests/unit/test_persistence.py +48 -35
  94. hpcflow/tests/unit/test_resources.py +20 -18
  95. hpcflow/tests/unit/test_run.py +8 -3
  96. hpcflow/tests/unit/test_runtime.py +2 -1
  97. hpcflow/tests/unit/test_schema_input.py +23 -15
  98. hpcflow/tests/unit/test_shell.py +3 -2
  99. hpcflow/tests/unit/test_slurm.py +8 -7
  100. hpcflow/tests/unit/test_submission.py +39 -19
  101. hpcflow/tests/unit/test_task.py +352 -247
  102. hpcflow/tests/unit/test_task_schema.py +33 -20
  103. hpcflow/tests/unit/test_utils.py +9 -11
  104. hpcflow/tests/unit/test_value_sequence.py +15 -12
  105. hpcflow/tests/unit/test_workflow.py +114 -83
  106. hpcflow/tests/unit/test_workflow_template.py +0 -1
  107. hpcflow/tests/workflows/test_jobscript.py +2 -1
  108. hpcflow/tests/workflows/test_workflows.py +18 -13
  109. {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
  110. hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
  111. hpcflow/sdk/core/parallel.py +0 -21
  112. hpcflow_new2-0.2.0a188.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -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 Dict, List, Optional, Tuple, Union
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: Union[Path, str]
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: Optional[Dict] = field(
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: Optional[List[Schema]] = field(default_factory=lambda: [])
113
+ extra_schemas: Sequence[Schema] = field(default_factory=list)
100
114
  #: Default directory of known configurations.
101
- default_known_configs_dir: Optional[str] = None
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
- cfg_schemas, cfg_keys = self.init_schemas()
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")] + self.extra_schemas
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(self, data, logger, metadata=None, raise_with_metadata=True):
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
- """Application configuration as defined in one or more config files.
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
- config_directory:
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
- log_file_path:
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
- task_schema_sources:
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: Optional[str],
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
- if name not in self._configurable_keys:
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
- return super().__dir__() + self._all_keys
311
+ def __dir__(self) -> Iterator[str]:
312
+ yield from super().__dir__()
313
+ yield from self._all_keys
344
314
 
345
- def __getattr__(self, name):
346
- if not name.startswith("__"):
347
- return self._get(name)
348
- else:
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(self, callbacks) -> Tuple[Dict]:
361
- """Disable named get and set callbacks.
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(i for i in v if i.__name__ not in callbacks)
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(i for i in v if i.__name__ not in callbacks)
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 not any(str(path).startswith(i + ":") for i in fsspec_protocols):
402
- path = Path(path)
403
- path = path.expanduser()
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
- return path
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] = tuple(
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(self, name):
434
- """Decorator to register a function as a configuration callback for a specified
435
- configuration item name, to be invoked on `set` of the item."""
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] = tuple(
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 + list(self._meta_data.keys())
456
-
457
- def get_all(self, include_overrides=True, as_str=False):
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
- else:
464
- try:
465
- val = self._get(
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=as_str,
656
+ as_str=True,
470
657
  )
471
- except ValueError:
472
- continue
473
- items.update({key: val})
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
- tab_md = Table(show_header=False, box=None)
480
- tab_md.add_column()
481
- tab_md.add_column()
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
- tab_md.add_row(k, Pretty(v))
486
- panel_md = Panel(tab_md, title="Config metadata")
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
- panel = Panel(tab, title=f"Config {self._config_key!r}")
496
- group_args.append(panel)
686
+ group_args.append(Panel(tab, title=f"Config {self._config_key!r}"))
497
687
 
498
- group = Group(*group_args)
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
- val = [str(i) for i in val]
772
+ return [str(i) for i in val]
557
773
  else:
558
- val = str(val)
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
- value = json.loads(value)
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
- def _set(self, name, value, is_json=False, callback=True, quiet=False):
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
- else:
573
- if is_json:
574
- value = self._parse_JSON(name, value)
575
- current_val = self._get(name)
576
- callback_val = self._get_callback_value(name, value)
577
- file_val_raw = self._file.get_config_item(self._config_key, name)
578
- file_val = self._get_callback_value(name, file_val_raw)
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
- try:
600
- self._validate()
601
-
602
- if callback:
603
- for cb in self._set_callbacks.get(name, []):
604
- self._logger.debug(
605
- f"Invoking `config.set` callback for item {name!r}: {cb.__name__!r}"
606
- )
607
- cb(self, callback_val)
608
-
609
- except ConfigValidationError as err:
610
- # revert:
611
- if modified_updated:
612
- if was_in_modified:
613
- self._modified_keys[name] = prev_modified_val
614
- else:
615
- del self._modified_keys[name]
616
- if was_in_unset:
617
- self._unset_keys.append(name)
618
-
619
- raise ConfigChangeValidationError(name, validation_err=err) from None
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
- self._logger.debug(
622
- f"Successfully set config item {name!r} to {callback_val!r}."
623
- )
624
- elif not quiet:
625
- print(f"value is already: {callback_val!r}")
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
- def set(self, path: str, value, is_json=False, quiet=False):
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
- parts = path.split(".")
644
- name = parts[0]
900
+ name, *path_suffix = path.split(".")
645
901
  root = deepcopy(self._get(name, callback=False))
646
- if parts[1:]:
902
+ if path_suffix:
647
903
  if root is None:
648
904
  root = {}
649
- self.set(path=parts[0], value={}, quiet=True)
905
+ self.set(path=name, value={}, quiet=True)
650
906
  set_in_container(
651
907
  root,
652
- path=parts[1:],
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: str
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.append(name)
935
+ self._unset_keys.add(name)
680
936
  try:
681
937
  self._validate()
682
938
  except ConfigValidationError as err:
683
- self._unset_keys.pop()
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
- callback=True,
690
- copy=False,
691
- ret_root=False,
692
- ret_parts=False,
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: str
980
+ path:
701
981
  The name of or path to the configuration item.
702
982
  """
703
- parts = path.split(".")
704
- root = deepcopy(self._get(parts[0], callback=callback))
983
+ name, *suffix = parts = path.split(".")
984
+ root = deepcopy(self._get(name, callback=callback))
705
985
  try:
706
- out = get_in_container(root, parts[1:], cast_indices=True)
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 (ret_root or ret_parts):
991
+ if not ret_root_and_parts:
712
992
  return out
713
- ret = [out]
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
- ret_root=True,
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, ret_root=True, ret_parts=True, callback=False, default=[]
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
- ret_root=True,
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
- """Update a map-like configuration item.
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
- ret_root=True,
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
- """Retrieve (and set if non-existent) a unique user ID that is independent of the
888
- config directory."""
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(f"Resetting config file to defaults.")
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(self, file_path, rename=True, make_new=False):
935
- """Import config items from a (remote or local) YAML file. Existing config items
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
- status = console.status(f"Importing config from file {file_path!r}...")
957
- status.start()
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
- new_config = dict(sorted(new_config.items(), reverse=True))
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: Optional[str] = None):
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 = self._options.default_known_configs_dir
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 = [i for i in files if Path(i).stem.startswith(known_name)]
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 i in files:
1045
- self.import_from_file(file_path=i, make_new=True)
1307
+ for file_i in files:
1308
+ self.import_from_file(file_path=file_i, make_new=True)
1046
1309
 
1047
- print(f"imports complete")
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
- """Set the `demo_data_dir` item, to an fsspec Github URL.
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
- path = "/".join(self._app.demo_data_dir.split("."))
1062
- url = self._app._get_github_url(sha=sha, path=path)
1063
- self.set("demo_data_dir", url)
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
+ )