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.
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.0a189.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.0a189.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -2,11 +2,34 @@
2
2
  General model of a searchable serializable list.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+ from collections import defaultdict
7
+ from collections.abc import Mapping, Sequence
5
8
  import copy
9
+ import sys
6
10
  from types import SimpleNamespace
11
+ from typing import Generic, TypeVar, cast, overload, TYPE_CHECKING
12
+ from typing_extensions import override
7
13
 
8
14
  from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
9
15
 
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterable, Iterator
18
+ from typing import Any, ClassVar, Literal
19
+ from typing_extensions import Self, TypeIs
20
+ from zarr import Group # type: ignore
21
+ from .actions import ActionScope
22
+ from .command_files import FileSpec
23
+ from .environment import Environment, Executable
24
+ from .loop import WorkflowLoop
25
+ from .json_like import JSONable, JSONed
26
+ from .parameters import Parameter, ResourceSpec
27
+ from .task import Task, TaskTemplate, TaskSchema, WorkflowTask, ElementSet
28
+ from .types import Resources
29
+ from .workflow import WorkflowTemplate
30
+
31
+ T = TypeVar("T")
32
+
10
33
 
11
34
  class ObjectListMultipleMatchError(ValueError):
12
35
  """
@@ -15,26 +38,32 @@ class ObjectListMultipleMatchError(ValueError):
15
38
  """
16
39
 
17
40
 
18
- class ObjectList(JSONLike):
19
- """A list-like class that provides item access via a `get` method according to
41
+ class ObjectList(JSONLike, Generic[T]):
42
+ """
43
+ A list-like class that provides item access via a `get` method according to
20
44
  attributes or dict-keys.
21
45
 
22
46
  Parameters
23
47
  ----------
24
48
  objects : sequence
25
- List
49
+ List of values of some type.
26
50
  descriptor : str
27
51
  Descriptive name for objects in the list.
28
52
  """
29
53
 
30
- def __init__(self, objects, descriptor=None):
54
+ # This would be in the docstring except it renders really wrongly!
55
+ # Type Parameters
56
+ # ---------------
57
+ # T
58
+ # The type of elements of the list.
59
+
60
+ def __init__(self, objects: Iterable[T], descriptor: str | None = None):
31
61
  self._objects = list(objects)
32
62
  self._descriptor = descriptor or "object"
33
- self._object_is_dict = False
34
-
63
+ self._object_is_dict: bool = False
35
64
  self._validate()
36
65
 
37
- def __deepcopy__(self, memo):
66
+ def __deepcopy__(self, memo: dict[int, Any]) -> Self:
38
67
  obj = self.__class__(copy.deepcopy(self._objects, memo))
39
68
  obj._descriptor = self._descriptor
40
69
  obj._object_is_dict = self._object_is_dict
@@ -54,61 +83,62 @@ class ObjectList(JSONLike):
54
83
  return repr(self._objects)
55
84
 
56
85
  def __str__(self):
57
- return str([self._get_item(i) for i in self._objects])
86
+ return str([self._get_item(obj) for obj in self._objects])
58
87
 
59
- def __iter__(self):
88
+ def __iter__(self) -> Iterator[T]:
60
89
  if self._object_is_dict:
61
- return iter(self._get_item(i) for i in self._objects)
90
+ return iter(self._get_item(obj) for obj in self._objects)
62
91
  else:
63
92
  return self._objects.__iter__()
64
93
 
65
- def __getitem__(self, key):
94
+ @overload
95
+ def __getitem__(self, key: int) -> T:
96
+ ...
97
+
98
+ @overload
99
+ def __getitem__(self, key: slice) -> list[T]:
100
+ ...
101
+
102
+ def __getitem__(self, key: int | slice) -> T | list[T]:
66
103
  """Provide list-like index access."""
67
- return self._get_item(self._objects.__getitem__(key))
104
+ if isinstance(key, slice):
105
+ return list(map(self._get_item, self._objects.__getitem__(key)))
106
+ else:
107
+ return self._get_item(self._objects.__getitem__(key))
68
108
 
69
- def __contains__(self, item):
109
+ def __contains__(self, item: T) -> bool:
70
110
  if self._objects:
71
- if type(item) == type(self._get_item(self._objects[0])):
111
+ if type(item) is type(self._get_item(self._objects[0])):
72
112
  return self._objects.__contains__(item)
73
113
  return False
74
114
 
75
- def __eq__(self, other):
76
- return self._objects == other
77
-
78
- def list_attrs(self):
79
- """Get a tuple of the unique access-attribute values of the constituent objects."""
80
- return tuple(self._index.keys())
115
+ def __eq__(self, other: Any) -> bool:
116
+ return isinstance(other, self.__class__) and self._objects == other._objects
81
117
 
82
- def _get_item(self, obj):
118
+ def _get_item(self, obj: T):
83
119
  if self._object_is_dict:
84
120
  return obj.__dict__
85
121
  else:
86
122
  return obj
87
123
 
88
- def _get_obj_attr(self, obj, attr):
124
+ def _get_obj_attr(self, obj: T, attr: str):
89
125
  """Overriding this function allows control over how the `get` functions behave."""
90
126
  return getattr(obj, attr)
91
127
 
92
- def _get_all_from_objs(self, objs, **kwargs):
93
- # narrow down according to kwargs:
94
- specified_objs = []
128
+ def __specified_objs(self, objs: Iterable[T], kwargs: dict[str, Any]) -> Iterator[T]:
95
129
  for obj in objs:
96
- skip_obj = False
97
130
  for k, v in kwargs.items():
98
131
  try:
99
- obj_key_val = self._get_obj_attr(obj, k)
132
+ if self._get_obj_attr(obj, k) != v:
133
+ break
100
134
  except (AttributeError, KeyError):
101
- skip_obj = True
102
- break
103
- if obj_key_val != v:
104
- skip_obj = True
105
135
  break
106
- if skip_obj:
107
- continue
108
136
  else:
109
- specified_objs.append(obj)
137
+ yield obj
110
138
 
111
- return [self._get_item(i) for i in specified_objs]
139
+ def _get_all_from_objs(self, objs: Iterable[T], **kwargs):
140
+ # narrow down according to kwargs:
141
+ return [self._get_item(obj) for obj in self.__specified_objs(objs, kwargs)]
112
142
 
113
143
  def get_all(self, **kwargs):
114
144
  """Get one or more objects from the object list, by specifying the value of the
@@ -116,11 +146,11 @@ class ObjectList(JSONLike):
116
146
 
117
147
  return self._get_all_from_objs(self._objects, **kwargs)
118
148
 
119
- def _validate_get(self, result, kwargs):
149
+ def _validate_get(self, result: Sequence[T], kwargs: dict[str, Any]):
120
150
  if not result:
121
- available = []
151
+ available: list[dict[str, Any]] = []
122
152
  for obj in self._objects:
123
- attr_vals = {}
153
+ attr_vals: dict[str, Any] = {}
124
154
  for k in kwargs:
125
155
  try:
126
156
  attr_vals[k] = self._get_obj_attr(obj, k)
@@ -144,7 +174,21 @@ class ObjectList(JSONLike):
144
174
  attribute, and optionally additional keyword-argument attribute values."""
145
175
  return self._validate_get(self.get_all(**kwargs), kwargs)
146
176
 
147
- def add_object(self, obj, index=-1, skip_duplicates=False):
177
+ @overload
178
+ def add_object(
179
+ self, obj: T, index: int = -1, *, skip_duplicates: Literal[False] = False
180
+ ) -> int:
181
+ ...
182
+
183
+ @overload
184
+ def add_object(
185
+ self, obj: T, index: int = -1, *, skip_duplicates: Literal[True]
186
+ ) -> int | None:
187
+ ...
188
+
189
+ def add_object(
190
+ self, obj: T, index: int = -1, *, skip_duplicates: bool = False
191
+ ) -> None | int:
148
192
  """
149
193
  Add an object to this object list.
150
194
 
@@ -162,20 +206,35 @@ class ObjectList(JSONLike):
162
206
  The index of the added object, or ``None`` if the object was not added.
163
207
  """
164
208
  if skip_duplicates and obj in self:
165
- return
209
+ return None
166
210
 
167
211
  if index < 0:
168
212
  index += len(self) + 1
169
213
 
170
214
  if self._object_is_dict:
171
- obj = SimpleNamespace(**obj)
215
+ obj = cast("T", SimpleNamespace(**cast("dict", obj)))
172
216
 
173
217
  self._objects = self._objects[:index] + [obj] + self._objects[index:]
174
218
  self._validate()
175
219
  return index
176
220
 
177
221
 
178
- class DotAccessObjectList(ObjectList):
222
+ class DotAccessAttributeError(AttributeError):
223
+ def __init__(self, name: str, obj: DotAccessObjectList) -> None:
224
+ msg = f"{obj._descriptor.title()} {name!r} does not exist. "
225
+ if obj._objects:
226
+ attr = obj._access_attribute
227
+ obj_list = (f'"{getattr(obj, attr)}"' for obj in obj._objects)
228
+ msg += f"Available {obj._descriptor}s are: {', '.join(obj_list)}."
229
+ else:
230
+ msg += "The object list is empty."
231
+ if sys.version_info >= (3, 10):
232
+ super().__init__(msg, name=name, obj=obj)
233
+ else:
234
+ super().__init__(msg)
235
+
236
+
237
+ class DotAccessObjectList(ObjectList[T], Generic[T]):
179
238
  """
180
239
  Provide dot-notation access via an access attribute for the case where the access
181
240
  attribute uniquely identifies a single object.
@@ -190,15 +249,35 @@ class DotAccessObjectList(ObjectList):
190
249
  Descriptive name for the objects in the list.
191
250
  """
192
251
 
252
+ # This would be in the docstring except it renders really wrongly!
253
+ # Type Parameters
254
+ # ---------------
255
+ # T
256
+ # The type of elements of the list.
257
+
193
258
  # access attributes must not be named after any "public" methods, to avoid confusion!
194
- _pub_methods = ("get", "get_all", "add_object", "add_objects")
259
+ _pub_methods: ClassVar[tuple[str, ...]] = (
260
+ "get",
261
+ "get_all",
262
+ "add_object",
263
+ "add_objects",
264
+ )
195
265
 
196
- def __init__(self, _objects, access_attribute, descriptor=None):
266
+ def __init__(
267
+ self, _objects: Iterable[T], access_attribute: str, descriptor: str | None = None
268
+ ):
197
269
  self._access_attribute = access_attribute
270
+ self._index: Mapping[str, Sequence[int]]
198
271
  super().__init__(_objects, descriptor=descriptor)
199
272
  self._update_index()
200
273
 
201
- def _validate(self):
274
+ def __deepcopy__(self, memo: dict[int, Any]) -> Self:
275
+ obj = self.__class__(copy.deepcopy(self._objects, memo), self._access_attribute)
276
+ obj._descriptor = self._descriptor
277
+ obj._object_is_dict = self._object_is_dict
278
+ return obj
279
+
280
+ def _validate(self) -> None:
202
281
  for idx, obj in enumerate(self._objects):
203
282
  if not hasattr(obj, self._access_attribute):
204
283
  raise TypeError(
@@ -211,60 +290,48 @@ class DotAccessObjectList(ObjectList):
211
290
  f"cannot be the same as any of the methods of "
212
291
  f"{self.__class__.__name__!r}, which are: {self._pub_methods!r}."
213
292
  )
293
+ super()._validate()
214
294
 
215
- return super()._validate()
216
-
217
- def _update_index(self):
295
+ def _update_index(self) -> None:
218
296
  """For quick look-up by access attribute."""
219
297
 
220
- _index = {}
298
+ _index: dict[str, list[int]] = defaultdict(list)
221
299
  for idx, obj in enumerate(self._objects):
222
- attr_val = getattr(obj, self._access_attribute)
300
+ attr_val: str = getattr(obj, self._access_attribute)
223
301
  try:
224
- if attr_val in _index:
225
- _index[attr_val].append(idx)
226
- else:
227
- _index[attr_val] = [idx]
302
+ _index[attr_val].append(idx)
228
303
  except TypeError:
229
304
  raise TypeError(
230
305
  f"Access attribute values ({self._access_attribute!r}) must be hashable."
231
306
  )
232
307
  self._index = _index
233
308
 
234
- def __getattr__(self, attribute):
235
- if attribute in self._index:
236
- idx = self._index[attribute]
309
+ def __getattr__(self, attribute: str):
310
+ if idx := self._index.get(attribute):
237
311
  if len(idx) > 1:
238
312
  raise ValueError(
239
313
  f"Multiple objects with access attribute: {attribute!r}."
240
314
  )
241
315
  return self._get_item(self._objects[idx[0]])
242
-
243
316
  elif not attribute.startswith("__"):
244
- obj_list_fmt = ", ".join(
245
- [f'"{getattr(i, self._access_attribute)}"' for i in self._objects]
246
- )
247
- msg = f"{self._descriptor.title()} {attribute!r} does not exist. "
248
- if self._objects:
249
- msg += f"Available {self._descriptor}s are: {obj_list_fmt}."
250
- else:
251
- msg += "The object list is empty."
252
-
253
- raise AttributeError(msg)
317
+ raise DotAccessAttributeError(attribute, self)
254
318
  else:
255
319
  raise AttributeError
256
320
 
257
- def __dir__(self):
258
- return super().__dir__() + [
259
- getattr(i, self._access_attribute) for i in self._objects
260
- ]
321
+ def __dir__(self) -> Iterator[str]:
322
+ yield from super().__dir__()
323
+ yield from (getattr(obj, self._access_attribute) for obj in self._objects)
324
+
325
+ def list_attrs(self) -> tuple[str, ...]:
326
+ """Get a tuple of the unique access-attribute values of the constituent objects."""
327
+ return tuple(self._index)
261
328
 
262
- def get(self, access_attribute_value=None, **kwargs):
329
+ def get(self, access_attribute_value: str | None = None, **kwargs) -> T:
263
330
  """
264
331
  Get an object from this list that matches the given criteria.
265
332
  """
266
333
  vld_get_kwargs = kwargs
267
- if access_attribute_value:
334
+ if access_attribute_value is not None:
268
335
  vld_get_kwargs = {self._access_attribute: access_attribute_value, **kwargs}
269
336
 
270
337
  return self._validate_get(
@@ -272,57 +339,121 @@ class DotAccessObjectList(ObjectList):
272
339
  vld_get_kwargs,
273
340
  )
274
341
 
275
- def get_all(self, access_attribute_value=None, **kwargs):
342
+ def get_all(self, access_attribute_value: str | None = None, **kwargs):
276
343
  """
277
344
  Get all objects in this list that match the given criteria.
278
345
  """
279
346
  # use the index to narrow down the search first:
280
- if access_attribute_value:
281
- try:
282
- all_idx = self._index[access_attribute_value]
283
- except KeyError:
347
+ if access_attribute_value is not None:
348
+ if (all_idx := self._index.get(access_attribute_value)) is None:
284
349
  raise ValueError(
285
350
  f"Value {access_attribute_value!r} does not match the value of any "
286
351
  f"object's attribute {self._access_attribute!r}. Available attribute "
287
352
  f"values are: {self.list_attrs()!r}."
288
- ) from None
289
- all_objs = [self._objects[i] for i in all_idx]
353
+ )
354
+ all_objs: Iterable[T] = (self._objects[idx] for idx in all_idx)
290
355
  else:
291
356
  all_objs = self._objects
292
357
 
293
358
  return self._get_all_from_objs(all_objs, **kwargs)
294
359
 
295
- def add_object(self, obj, index=-1, skip_duplicates=False):
360
+ @overload
361
+ def add_object(
362
+ self, obj: T, index: int = -1, *, skip_duplicates: Literal[False] = False
363
+ ) -> int:
364
+ ...
365
+
366
+ @overload
367
+ def add_object(
368
+ self, obj: T, index: int = -1, *, skip_duplicates: Literal[True]
369
+ ) -> int | None:
370
+ ...
371
+
372
+ def add_object(
373
+ self, obj: T, index: int = -1, *, skip_duplicates: bool = False
374
+ ) -> int | None:
296
375
  """
297
376
  Add an object to this list.
298
377
  """
299
- index = super().add_object(obj, index, skip_duplicates)
378
+ if skip_duplicates:
379
+ new_index = super().add_object(obj, index, skip_duplicates=True)
380
+ else:
381
+ new_index = super().add_object(obj, index)
300
382
  self._update_index()
301
- return index
383
+ return new_index
302
384
 
303
- def add_objects(self, objs, index=-1, skip_duplicates=False):
385
+ def add_objects(
386
+ self, objs: Iterable[T], index: int = -1, *, skip_duplicates: bool = False
387
+ ) -> int:
304
388
  """
305
389
  Add multiple objects to the list.
306
390
  """
307
- for obj in objs:
308
- index = self.add_object(obj, index, skip_duplicates)
309
- if index is not None:
310
- index += 1
391
+ if skip_duplicates:
392
+ for obj in objs:
393
+ if (i := self.add_object(obj, index, skip_duplicates=True)) is not None:
394
+ index = i + 1
395
+ else:
396
+ for obj in objs:
397
+ index = self.add_object(obj, index) + 1
311
398
  return index
312
399
 
313
400
 
314
- class AppDataList(DotAccessObjectList):
401
+ class AppDataList(DotAccessObjectList[T], Generic[T]):
315
402
  """
316
403
  An application-aware object list.
404
+
405
+ Type Parameters
406
+ ---------------
407
+ T
408
+ The type of elements of the list.
317
409
  """
318
410
 
319
- _app_attr = "_app"
411
+ @override
412
+ def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
413
+ d = super()._postprocess_to_dict(d)
414
+ return {"_objects": d["_objects"]}
320
415
 
321
- def to_dict(self):
322
- return {"_objects": super().to_dict()["_objects"]}
416
+ @classmethod
417
+ def _get_default_shared_data(cls) -> Mapping[str, ObjectList[JSONable]]:
418
+ return cls._app._shared_data
419
+
420
+ @overload
421
+ @classmethod
422
+ def from_json_like(
423
+ cls,
424
+ json_like: str,
425
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
426
+ is_hashed: bool = False,
427
+ ) -> Self | None:
428
+ ...
429
+
430
+ @overload
431
+ @classmethod
432
+ def from_json_like(
433
+ cls,
434
+ json_like: Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]],
435
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
436
+ is_hashed: bool = False,
437
+ ) -> Self:
438
+ ...
439
+
440
+ @overload
441
+ @classmethod
442
+ def from_json_like(
443
+ cls,
444
+ json_like: None,
445
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
446
+ is_hashed: bool = False,
447
+ ) -> None:
448
+ ...
323
449
 
324
450
  @classmethod
325
- def from_json_like(cls, json_like, shared_data=None, is_hashed: bool = False):
451
+ def from_json_like(
452
+ cls,
453
+ json_like: str | Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]] | None,
454
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
455
+ is_hashed: bool = False,
456
+ ) -> Self | None:
326
457
  """
327
458
  Make an instance of this class from JSON (or YAML) data.
328
459
 
@@ -340,20 +471,23 @@ class AppDataList(DotAccessObjectList):
340
471
  The deserialised object.
341
472
  """
342
473
  if is_hashed:
343
- json_like = [
344
- {**obj_js, "_hash_value": hash_val}
345
- for hash_val, obj_js in json_like.items()
346
- ]
347
- if shared_data is None:
348
- shared_data = cls._app.template_components
349
- return super().from_json_like(json_like, shared_data=shared_data)
350
-
351
- def _remove_object(self, index):
474
+ assert isinstance(json_like, Mapping)
475
+ return super().from_json_like(
476
+ [
477
+ {**cast("Mapping", obj_js), "_hash_value": hash_val}
478
+ for hash_val, obj_js in json_like.items()
479
+ ],
480
+ shared_data=shared_data,
481
+ )
482
+ else:
483
+ return super().from_json_like(json_like, shared_data=shared_data)
484
+
485
+ def _remove_object(self, index: int):
352
486
  self._objects.pop(index)
353
487
  self._update_index()
354
488
 
355
489
 
356
- class TaskList(AppDataList):
490
+ class TaskList(AppDataList["Task"]):
357
491
  """A list-like container for a task-like list with dot-notation access by task
358
492
  unique-name.
359
493
 
@@ -363,7 +497,7 @@ class TaskList(AppDataList):
363
497
  The tasks in this list.
364
498
  """
365
499
 
366
- _child_objects = (
500
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
367
501
  ChildObjectSpec(
368
502
  name="_objects",
369
503
  class_name="Task",
@@ -372,11 +506,11 @@ class TaskList(AppDataList):
372
506
  ),
373
507
  )
374
508
 
375
- def __init__(self, _objects):
509
+ def __init__(self, _objects: Iterable[Task]):
376
510
  super().__init__(_objects, access_attribute="unique_name", descriptor="task")
377
511
 
378
512
 
379
- class TaskTemplateList(AppDataList):
513
+ class TaskTemplateList(AppDataList["TaskTemplate"]):
380
514
  """A list-like container for a task-like list with dot-notation access by task
381
515
  unique-name.
382
516
 
@@ -386,7 +520,7 @@ class TaskTemplateList(AppDataList):
386
520
  The task templates in this list.
387
521
  """
388
522
 
389
- _child_objects = (
523
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
390
524
  ChildObjectSpec(
391
525
  name="_objects",
392
526
  class_name="TaskTemplate",
@@ -395,11 +529,11 @@ class TaskTemplateList(AppDataList):
395
529
  ),
396
530
  )
397
531
 
398
- def __init__(self, _objects):
532
+ def __init__(self, _objects: Iterable[TaskTemplate]):
399
533
  super().__init__(_objects, access_attribute="name", descriptor="task template")
400
534
 
401
535
 
402
- class TaskSchemasList(AppDataList):
536
+ class TaskSchemasList(AppDataList["TaskSchema"]):
403
537
  """A list-like container for a task schema list with dot-notation access by task
404
538
  schema unique-name.
405
539
 
@@ -409,7 +543,7 @@ class TaskSchemasList(AppDataList):
409
543
  The task schemas in this list.
410
544
  """
411
545
 
412
- _child_objects = (
546
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
413
547
  ChildObjectSpec(
414
548
  name="_objects",
415
549
  class_name="TaskSchema",
@@ -418,11 +552,11 @@ class TaskSchemasList(AppDataList):
418
552
  ),
419
553
  )
420
554
 
421
- def __init__(self, _objects):
555
+ def __init__(self, _objects: Iterable[TaskSchema]):
422
556
  super().__init__(_objects, access_attribute="name", descriptor="task schema")
423
557
 
424
558
 
425
- class GroupList(AppDataList):
559
+ class GroupList(AppDataList["Group"]):
426
560
  """A list-like container for the task schema group list with dot-notation access by
427
561
  group name.
428
562
 
@@ -432,7 +566,7 @@ class GroupList(AppDataList):
432
566
  The groups in this list.
433
567
  """
434
568
 
435
- _child_objects = (
569
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
436
570
  ChildObjectSpec(
437
571
  name="_objects",
438
572
  class_name="Group",
@@ -441,11 +575,11 @@ class GroupList(AppDataList):
441
575
  ),
442
576
  )
443
577
 
444
- def __init__(self, _objects):
578
+ def __init__(self, _objects: Iterable[Group]):
445
579
  super().__init__(_objects, access_attribute="name", descriptor="group")
446
580
 
447
581
 
448
- class EnvironmentsList(AppDataList):
582
+ class EnvironmentsList(AppDataList["Environment"]):
449
583
  """
450
584
  A list-like container for environments with dot-notation access by name.
451
585
 
@@ -455,7 +589,7 @@ class EnvironmentsList(AppDataList):
455
589
  The environments in this list.
456
590
  """
457
591
 
458
- _child_objects = (
592
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
459
593
  ChildObjectSpec(
460
594
  name="_objects",
461
595
  class_name="Environment",
@@ -464,18 +598,18 @@ class EnvironmentsList(AppDataList):
464
598
  ),
465
599
  )
466
600
 
467
- def __init__(self, _objects):
601
+ def __init__(self, _objects: Iterable[Environment]):
468
602
  super().__init__(_objects, access_attribute="name", descriptor="environment")
469
603
 
470
- def _get_obj_attr(self, obj, attr):
604
+ def _get_obj_attr(self, obj: Environment, attr: str):
471
605
  """Overridden to lookup objects via the `specifiers` dict attribute"""
472
606
  if attr in ("name", "_hash_value"):
473
607
  return getattr(obj, attr)
474
608
  else:
475
- return getattr(obj, "specifiers")[attr]
609
+ return obj.specifiers[attr]
476
610
 
477
611
 
478
- class ExecutablesList(AppDataList):
612
+ class ExecutablesList(AppDataList["Executable"]):
479
613
  """
480
614
  A list-like container for environment executables with dot-notation access by
481
615
  executable label.
@@ -487,8 +621,8 @@ class ExecutablesList(AppDataList):
487
621
  """
488
622
 
489
623
  #: The environment containing these executables.
490
- environment = None
491
- _child_objects = (
624
+ environment: Environment | None = None
625
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
492
626
  ChildObjectSpec(
493
627
  name="_objects",
494
628
  class_name="Executable",
@@ -498,17 +632,17 @@ class ExecutablesList(AppDataList):
498
632
  ),
499
633
  )
500
634
 
501
- def __init__(self, _objects):
635
+ def __init__(self, _objects: Iterable[Executable]):
502
636
  super().__init__(_objects, access_attribute="label", descriptor="executable")
503
637
  self._set_parent_refs()
504
638
 
505
- def __deepcopy__(self, memo):
639
+ def __deepcopy__(self, memo: dict[int, Any]):
506
640
  obj = super().__deepcopy__(memo)
507
641
  obj.environment = self.environment
508
642
  return obj
509
643
 
510
644
 
511
- class ParametersList(AppDataList):
645
+ class ParametersList(AppDataList["Parameter"]):
512
646
  """
513
647
  A list-like container for parameters with dot-notation access by parameter type.
514
648
 
@@ -518,7 +652,7 @@ class ParametersList(AppDataList):
518
652
  The parameters in this list.
519
653
  """
520
654
 
521
- _child_objects = (
655
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
522
656
  ChildObjectSpec(
523
657
  name="_objects",
524
658
  class_name="Parameter",
@@ -527,16 +661,16 @@ class ParametersList(AppDataList):
527
661
  ),
528
662
  )
529
663
 
530
- def __init__(self, _objects):
664
+ def __init__(self, _objects: Iterable[Parameter]):
531
665
  super().__init__(_objects, access_attribute="typ", descriptor="parameter")
532
666
 
533
- def __getattr__(self, attribute):
667
+ def __getattr__(self, attribute: str) -> Parameter:
534
668
  """Overridden to provide a default Parameter object if none exists."""
535
- if not attribute.startswith("__"):
536
- try:
669
+ try:
670
+ if not attribute.startswith("__"):
537
671
  return super().__getattr__(attribute)
538
- except (AttributeError, ValueError):
539
- return self._app.Parameter(typ=attribute)
672
+ except (AttributeError, ValueError):
673
+ return self._app.Parameter(typ=attribute)
540
674
  raise AttributeError
541
675
 
542
676
  def get_all(self, access_attribute_value=None, **kwargs):
@@ -552,7 +686,7 @@ class ParametersList(AppDataList):
552
686
  return all_out or [self._app.Parameter(typ=typ)]
553
687
 
554
688
 
555
- class CommandFilesList(AppDataList):
689
+ class CommandFilesList(AppDataList["FileSpec"]):
556
690
  """
557
691
  A list-like container for command files with dot-notation access by label.
558
692
 
@@ -562,7 +696,7 @@ class CommandFilesList(AppDataList):
562
696
  The files in this list.
563
697
  """
564
698
 
565
- _child_objects = (
699
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
566
700
  ChildObjectSpec(
567
701
  name="_objects",
568
702
  class_name="FileSpec",
@@ -571,11 +705,11 @@ class CommandFilesList(AppDataList):
571
705
  ),
572
706
  )
573
707
 
574
- def __init__(self, _objects):
708
+ def __init__(self, _objects: Iterable[FileSpec]):
575
709
  super().__init__(_objects, access_attribute="label", descriptor="command file")
576
710
 
577
711
 
578
- class WorkflowTaskList(DotAccessObjectList):
712
+ class WorkflowTaskList(DotAccessObjectList["WorkflowTask"]):
579
713
  """
580
714
  A list-like container for workflow tasks with dot-notation access by unique name.
581
715
 
@@ -585,26 +719,28 @@ class WorkflowTaskList(DotAccessObjectList):
585
719
  The tasks in this list.
586
720
  """
587
721
 
588
- def __init__(self, _objects):
722
+ def __init__(self, _objects: Iterable[WorkflowTask]):
589
723
  super().__init__(_objects, access_attribute="unique_name", descriptor="task")
590
724
 
591
- def _reindex(self):
725
+ def _reindex(self) -> None:
592
726
  """Re-assign the WorkflowTask index attributes so they match their order."""
593
- for idx, i in enumerate(self._objects):
594
- i._index = idx
727
+ for idx, item in enumerate(self._objects):
728
+ item._index = idx
595
729
  self._update_index()
596
730
 
597
- def add_object(self, obj, index=-1):
731
+ def add_object(
732
+ self, obj: WorkflowTask, index: int = -1, skip_duplicates=False
733
+ ) -> int:
598
734
  index = super().add_object(obj, index)
599
735
  self._reindex()
600
736
  return index
601
737
 
602
- def _remove_object(self, index):
738
+ def _remove_object(self, index: int):
603
739
  self._objects.pop(index)
604
740
  self._reindex()
605
741
 
606
742
 
607
- class WorkflowLoopList(DotAccessObjectList):
743
+ class WorkflowLoopList(DotAccessObjectList["WorkflowLoop"]):
608
744
  """
609
745
  A list-like container for workflow loops with dot-notation access by name.
610
746
 
@@ -614,14 +750,14 @@ class WorkflowLoopList(DotAccessObjectList):
614
750
  The loops in this list.
615
751
  """
616
752
 
617
- def __init__(self, _objects):
753
+ def __init__(self, _objects: Iterable[WorkflowLoop]):
618
754
  super().__init__(_objects, access_attribute="name", descriptor="loop")
619
755
 
620
- def _remove_object(self, index):
756
+ def _remove_object(self, index: int):
621
757
  self._objects.pop(index)
622
758
 
623
759
 
624
- class ResourceList(ObjectList):
760
+ class ResourceList(ObjectList["ResourceSpec"]):
625
761
  """
626
762
  A list-like container for resources.
627
763
  Each contained resource must have a unique scope.
@@ -632,8 +768,7 @@ class ResourceList(ObjectList):
632
768
  The resource descriptions in this list.
633
769
  """
634
770
 
635
- _app_attr = "_app"
636
- _child_objects = (
771
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
637
772
  ChildObjectSpec(
638
773
  name="_objects",
639
774
  class_name="ResourceSpec",
@@ -644,13 +779,15 @@ class ResourceList(ObjectList):
644
779
  ),
645
780
  )
646
781
 
647
- def __init__(self, _objects):
782
+ def __init__(self, _objects: Iterable[ResourceSpec]):
648
783
  super().__init__(_objects, descriptor="resource specification")
649
- self._element_set = None # assigned by parent ElementSet
650
- self._workflow_template = None # assigned by parent WorkflowTemplate
784
+ self._element_set: ElementSet | None = None # assigned by parent ElementSet
785
+ self._workflow_template: WorkflowTemplate | None = (
786
+ None # assigned by parent WorkflowTemplate
787
+ )
651
788
 
652
789
  # check distinct scopes for each item:
653
- scopes = [i.to_string() for i in self.get_scopes()]
790
+ scopes = [scope.to_string() for scope in self.get_scopes()]
654
791
  if len(set(scopes)) < len(scopes):
655
792
  raise ValueError(
656
793
  "Multiple `ResourceSpec` objects have the same scope. The scopes are "
@@ -659,97 +796,123 @@ class ResourceList(ObjectList):
659
796
 
660
797
  self._set_parent_refs()
661
798
 
662
- def __deepcopy__(self, memo):
799
+ def __deepcopy__(self, memo: dict[int, Any]):
663
800
  obj = super().__deepcopy__(memo)
664
801
  obj._element_set = self._element_set
665
802
  obj._workflow_template = self._workflow_template
666
803
  return obj
667
804
 
668
805
  @property
669
- def element_set(self):
806
+ def element_set(self) -> ElementSet | None:
670
807
  """
671
808
  The parent element set, if a child of an element set.
672
809
  """
673
810
  return self._element_set
674
811
 
675
812
  @property
676
- def workflow_template(self):
813
+ def workflow_template(self) -> WorkflowTemplate | None:
677
814
  """
678
815
  The parent workflow template, if a child of a workflow template.
679
816
  """
680
817
  return self._workflow_template
681
818
 
682
- def to_json_like(self, dct=None, shared_data=None, exclude=None, path=None):
683
- """Overridden to write out as a dict keyed by action scope (like as can be
819
+ def _postprocess_to_json(self, json_like):
820
+ """Convert JSON doc to a dict keyed by action scope (like as can be
684
821
  specified in the input YAML) instead of list."""
822
+ return {
823
+ self._app.ActionScope.from_json_like(
824
+ res_spec_js.pop("scope")
825
+ ).to_string(): res_spec_js
826
+ for res_spec_js in json_like
827
+ }
828
+
829
+ @staticmethod
830
+ def __ensure_non_persistent(resource_spec: ResourceSpec) -> ResourceSpec:
831
+ """
832
+ For any resources that are persistent, if they have a
833
+ `_resource_list` attribute, this means they are sourced from some
834
+ other persistent workflow, rather than, say, a workflow being
835
+ loaded right now, so make a non-persistent copy
836
+
837
+ Part of `normalise`.
838
+ """
839
+ if resource_spec._value_group_idx is not None and (
840
+ resource_spec._resource_list is not None
841
+ ):
842
+ return resource_spec.copy_non_persistent()
843
+ return resource_spec
685
844
 
686
- out, shared_data = super().to_json_like(dct, shared_data, exclude, path)
687
- as_dict = {}
688
- for res_spec_js in out:
689
- scope = self._app.ActionScope.from_json_like(res_spec_js.pop("scope"))
690
- as_dict[scope.to_string()] = res_spec_js
691
- return as_dict, shared_data
845
+ @classmethod
846
+ def __is_ResourceSpec(cls, value) -> TypeIs[ResourceSpec]:
847
+ return isinstance(value, cls._app.ResourceSpec)
692
848
 
693
849
  @classmethod
694
- def normalise(cls, resources):
850
+ def normalise(cls, resources: Resources) -> Self:
695
851
  """Generate from resource-specs specified in potentially several ways."""
696
852
 
697
- def _ensure_non_persistent(resource_spec):
698
- # for any resources that are persistent, if they have a
699
- # `_resource_list` attribute, this means they are sourced from some
700
- # other persistent workflow, rather than, say, a workflow being
701
- # loaded right now, so make a non-persistent copy:
702
- if res_i._value_group_idx is not None and res_i._resource_list is not None:
703
- return resource_spec.copy_non_persistent()
704
- return resource_spec
705
-
706
- if isinstance(resources, cls._app.ResourceSpec):
707
- return resources
708
853
  if not resources:
709
- resources = cls([cls._app.ResourceSpec()])
854
+ return cls([cls._app.ResourceSpec()])
855
+ elif isinstance(resources, ResourceList):
856
+ # Already a ResourceList
857
+ return cast("Self", resources)
710
858
  elif isinstance(resources, dict):
711
- resources = cls.from_json_like(resources)
712
- elif isinstance(resources, list):
713
- for idx, res_i in enumerate(resources):
714
- if isinstance(res_i, dict):
715
- resources[idx] = cls._app.ResourceSpec.from_json_like(res_i)
716
- else:
717
- resources[idx] = _ensure_non_persistent(resources[idx])
718
- resources = cls(resources)
719
-
720
- return resources
721
-
722
- def get_scopes(self):
859
+ return cls.from_json_like(cast("dict", resources))
860
+ elif cls.__is_ResourceSpec(resources):
861
+ return cls([resources])
862
+ else:
863
+ return cls(
864
+ cls._app.ResourceSpec.from_json_like(cast("dict", res_i))
865
+ if isinstance(res_i, dict)
866
+ else cls.__ensure_non_persistent(res_i)
867
+ for res_i in resources
868
+ )
869
+
870
+ def get_scopes(self) -> Iterator[ActionScope]:
723
871
  """
724
872
  Get the scopes of the contained resources.
725
873
  """
726
- return tuple(i.scope for i in self._objects)
874
+ for rs in self._objects:
875
+ if rs.scope is not None:
876
+ yield rs.scope
727
877
 
728
- def merge_other(self, other):
878
+ def __get_for_scope(self, scope: ActionScope):
879
+ try:
880
+ return self.get(scope=scope)
881
+ except ValueError:
882
+ return None
883
+
884
+ def __merge(self, our_spec: ResourceSpec | None, other_spec: ResourceSpec):
885
+ """
886
+ Merge two resource specs that have the same scope, or just add the other one to
887
+ the list if we didn't already have it.
888
+ """
889
+ if our_spec is not None:
890
+ for k, v in other_spec._get_members().items():
891
+ if getattr(our_spec, k, None) is None:
892
+ setattr(our_spec, f"_{k}", copy.deepcopy(v))
893
+ else:
894
+ self.add_object(copy.deepcopy(other_spec))
895
+
896
+ def merge_other(self, other: ResourceList):
729
897
  """Merge lower-precedence other resource list into this resource list."""
730
898
  for scope_i in other.get_scopes():
731
- try:
732
- self_scoped = self.get(scope=scope_i)
733
- except ValueError:
734
- in_self = False
735
- else:
736
- in_self = True
899
+ self.__merge(self.__get_for_scope(scope_i), other.get(scope=scope_i))
737
900
 
738
- other_scoped = other.get(scope=scope_i)
739
- if in_self:
740
- for k, v in other_scoped._get_members().items():
741
- if getattr(self_scoped, k) is None:
742
- setattr(self_scoped, f"_{k}", copy.deepcopy(v))
743
- else:
744
- self.add_object(copy.deepcopy(other_scoped))
901
+ def merge_one(self, other: ResourceSpec):
902
+ """Merge lower-precedence other resource spec into this resource list.
903
+
904
+ This is a simplified version of :py:meth:`merge_other`.
905
+ """
906
+ if other.scope is not None:
907
+ self.__merge(self.__get_for_scope(other.scope), other)
745
908
 
746
909
 
747
- def index(obj_lst, obj):
910
+ def index(obj_lst: ObjectList[T], obj: T) -> int:
748
911
  """
749
912
  Get the index of the object in the list.
750
913
  The item is checked for by object identity, not equality.
751
914
  """
752
- for idx, i in enumerate(obj_lst._objects):
753
- if obj is i:
915
+ for idx, item in enumerate(obj_lst._objects):
916
+ if obj is item:
754
917
  return idx
755
918
  raise ValueError(f"{obj!r} not in list.")