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,29 +2,43 @@
2
2
  Abstract task, prior to instantiation.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  from contextlib import contextmanager
6
7
  import copy
7
8
  from dataclasses import dataclass
8
9
  from importlib import import_module
9
- from typing import Any, Dict, List, Optional, Tuple, Union
10
+ from itertools import chain
11
+ from typing import TYPE_CHECKING
10
12
  from html import escape
11
13
 
12
14
  from rich import print as rich_print
13
- from rich.console import Console
14
15
  from rich.table import Table
15
16
  from rich.panel import Panel
16
17
  from rich.markup import escape as rich_esc
17
18
  from rich.text import Text
18
19
 
19
- from hpcflow.sdk import app
20
+ from hpcflow.sdk.typing import hydrate
21
+ from hpcflow.sdk.core.enums import ParameterPropagationMode
20
22
  from hpcflow.sdk.core.errors import EnvironmentPresetUnknownEnvironmentError
23
+ from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
21
24
  from hpcflow.sdk.core.parameters import Parameter
22
- from .json_like import ChildObjectSpec, JSONLike
23
- from .parameters import NullDefault, ParameterPropagationMode, SchemaInput
24
- from .utils import check_valid_py_identifier
25
+ from hpcflow.sdk.core.utils import check_valid_py_identifier
26
+
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Iterable, Iterator, Mapping, Sequence
29
+ from typing import Any, ClassVar
30
+ from typing_extensions import Self, TypeIs
31
+ from .actions import Action
32
+ from .object_list import ParametersList, TaskSchemasList
33
+ from .parameters import InputValue, SchemaInput, SchemaOutput, SchemaParameter
34
+ from .task import TaskTemplate
35
+ from .types import ActParameterDependence
36
+ from .workflow import Workflow
37
+ from ..typing import ParamSource
25
38
 
26
39
 
27
40
  @dataclass
41
+ @hydrate
28
42
  class TaskObjective(JSONLike):
29
43
  """
30
44
  A thing that a task is attempting to achieve.
@@ -35,7 +49,7 @@ class TaskObjective(JSONLike):
35
49
  The name of the objective. A valid Python identifier.
36
50
  """
37
51
 
38
- _child_objects = (
52
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
39
53
  ChildObjectSpec(
40
54
  name="name",
41
55
  is_single_attribute=True,
@@ -48,6 +62,10 @@ class TaskObjective(JSONLike):
48
62
  def __post_init__(self):
49
63
  self.name = check_valid_py_identifier(self.name)
50
64
 
65
+ @classmethod
66
+ def _parse_from_string(cls, string):
67
+ return string
68
+
51
69
 
52
70
  class TaskSchema(JSONLike):
53
71
  """Class to represent the inputs, outputs and implementation mechanism of a given
@@ -79,11 +97,11 @@ class TaskSchema(JSONLike):
79
97
  cases in the concrete tasks.
80
98
  """
81
99
 
82
- _validation_schema = "task_schema_spec_schema.yaml"
100
+ _validation_schema: ClassVar[str] = "task_schema_spec_schema.yaml"
83
101
  _hash_value = None
84
102
  _validate_actions = True
85
103
 
86
- _child_objects = (
104
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
87
105
  ChildObjectSpec(name="objective", class_name="TaskObjective"),
88
106
  ChildObjectSpec(
89
107
  name="inputs",
@@ -100,23 +118,35 @@ class TaskSchema(JSONLike):
100
118
  ),
101
119
  )
102
120
 
121
+ @classmethod
122
+ def __is_InputValue(cls, value) -> TypeIs[InputValue]:
123
+ return isinstance(value, cls._app.InputValue)
124
+
125
+ @classmethod
126
+ def __is_Parameter(cls, value) -> TypeIs[Parameter]:
127
+ return isinstance(value, cls._app.Parameter)
128
+
129
+ @classmethod
130
+ def __is_SchemaOutput(cls, value) -> TypeIs[SchemaOutput]:
131
+ return isinstance(value, cls._app.SchemaOutput)
132
+
103
133
  def __init__(
104
134
  self,
105
- objective: Union[app.TaskObjective, str],
106
- actions: List[app.Action] = None,
107
- method: Optional[str] = None,
108
- implementation: Optional[str] = None,
109
- inputs: Optional[List[Union[app.Parameter, app.SchemaInput]]] = None,
110
- outputs: Optional[List[Union[app.Parameter, app.SchemaOutput]]] = None,
111
- version: Optional[str] = None,
112
- parameter_class_modules: Optional[List[str]] = None,
113
- web_doc: Optional[bool] = True,
114
- environment_presets: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None,
135
+ objective: TaskObjective | str,
136
+ actions: list[Action] | None = None,
137
+ method: str | None = None,
138
+ implementation: str | None = None,
139
+ inputs: list[Parameter | SchemaInput] | None = None,
140
+ outputs: list[Parameter | SchemaParameter] | None = None,
141
+ version: str | None = None,
142
+ parameter_class_modules: list[str] | None = None,
143
+ web_doc: bool | None = True,
144
+ environment_presets: Mapping[str, Mapping[str, Mapping[str, Any]]] | None = None,
115
145
  doc: str = "",
116
- _hash_value: Optional[str] = None,
146
+ _hash_value: str | None = None,
117
147
  ):
118
148
  #: This is a string representing the objective of the task schema.
119
- self.objective = objective
149
+ self.objective = self.__coerce_objective(objective)
120
150
  #: A list of Action objects whose commands are to be executed by the task.
121
151
  self.actions = actions or []
122
152
  #: An optional string to label the task schema by its method.
@@ -124,9 +154,9 @@ class TaskSchema(JSONLike):
124
154
  #: An optional string to label the task schema by its implementation.
125
155
  self.implementation = implementation
126
156
  #: A list of SchemaInput objects that define the inputs to the task.
127
- self.inputs = inputs or []
157
+ self.inputs = self.__coerce_inputs(inputs or ())
128
158
  #: A list of SchemaOutput objects that define the outputs of the task.
129
- self.outputs = outputs or []
159
+ self.outputs = self.__coerce_outputs(outputs or ())
130
160
  #: Where to find implementations of parameter value handlers.
131
161
  self.parameter_class_modules = parameter_class_modules or []
132
162
  #: Whether this object should be included in the Sphinx documentation
@@ -141,67 +171,76 @@ class TaskSchema(JSONLike):
141
171
  self._set_parent_refs()
142
172
 
143
173
  # process `Action` script_data_in/out formats:
144
- for i in self.actions:
145
- i.process_script_data_formats()
174
+ for act in self.actions:
175
+ act.process_script_data_formats()
146
176
 
147
177
  self._validate()
148
- self.actions = self._expand_actions()
178
+ self.actions = self.__expand_actions()
149
179
  #: The version of this task schema.
150
180
  self.version = version
151
- self._task_template = None # assigned by parent Task
181
+ self._task_template: TaskTemplate | None = None # assigned by parent Task
152
182
 
153
- self._update_parameter_value_classes()
183
+ self.__update_parameter_value_classes()
154
184
 
155
185
  if self.environment_presets:
156
186
  # validate against env names in actions:
157
187
  env_names = {act.get_environment_name() for act in self.actions}
158
- preset_envs = {i for v in self.environment_presets.values() for i in v.keys()}
159
- bad_envs = preset_envs - env_names
160
- if bad_envs:
161
- raise EnvironmentPresetUnknownEnvironmentError(
162
- f"Task schema {self.name} has environment presets that refer to one "
163
- f"or more environments that are not referenced in any of the task "
164
- f"schema's actions: {', '.join(f'{i!r}' for i in bad_envs)}."
165
- )
188
+ preset_envs = {
189
+ preset_name
190
+ for preset in self.environment_presets.values()
191
+ for preset_name in preset
192
+ }
193
+ if bad_envs := preset_envs - env_names:
194
+ raise EnvironmentPresetUnknownEnvironmentError(self.name, bad_envs)
166
195
 
167
196
  # if version is not None: # TODO: this seems fragile
168
197
  # self.assign_versions(
169
198
  # version=version,
170
- # app_data_obj_list=self.app.task_schemas
199
+ # app_data_obj_list=self._app.task_schemas
171
200
  # if app.is_data_files_loaded
172
201
  # else [],
173
202
  # )
174
203
 
175
- def __repr__(self):
204
+ def __repr__(self) -> str:
176
205
  return f"{self.__class__.__name__}({self.objective.name!r})"
177
206
 
178
- def _get_info(self, include=None):
179
- def _get_param_type_str(parameter) -> str:
180
- type_fmt = "-"
181
- if parameter._validation:
182
- try:
183
- type_fmt = parameter._validation.to_tree()[0]["type_fmt"]
184
- except Exception:
185
- pass
186
- elif parameter._value_class:
187
- param_cls = parameter._value_class
188
- cls_url = (
189
- f"{self.app.docs_url}/reference/_autosummary/{param_cls.__module__}."
190
- f"{param_cls.__name__}"
191
- )
192
- type_fmt = f"[link={cls_url}]{param_cls.__name__}[/link]"
193
- return type_fmt
207
+ @classmethod
208
+ def __parameters(cls) -> ParametersList:
209
+ # Workaround for a dumb mypy bug
210
+ return cls._app.parameters
194
211
 
195
- def _format_parameter_type(param) -> str:
196
- param_typ_fmt = param.typ
197
- if param.typ in self.app.parameters.list_attrs():
198
- param_url = (
199
- f"{self.app.docs_url}/reference/template_components/"
200
- f"parameters.html#{param.url_slug}"
201
- )
202
- param_typ_fmt = f"[link={param_url}]{param_typ_fmt}[/link]"
203
- return param_typ_fmt
212
+ @classmethod
213
+ def __task_schemas(cls) -> TaskSchemasList:
214
+ # Workaround for a dumb mypy bug
215
+ return cls._app.task_schemas
216
+
217
+ def __get_param_type_str(self, param: Parameter) -> str:
218
+ type_fmt = "-"
219
+ if param._validation:
220
+ try:
221
+ type_fmt = param._validation.to_tree()[0]["type_fmt"]
222
+ except Exception:
223
+ pass
224
+ elif param._value_class:
225
+ param_cls = param._value_class
226
+ cls_url = (
227
+ f"{self._app.docs_url}/reference/_autosummary/{param_cls.__module__}."
228
+ f"{param_cls.__name__}"
229
+ )
230
+ type_fmt = f"[link={cls_url}]{param_cls.__name__}[/link]"
231
+ return type_fmt
232
+
233
+ def __format_parameter_type(self, param: Parameter) -> str:
234
+ param_typ_fmt = param.typ
235
+ if param.typ in self.__parameters().list_attrs():
236
+ param_url = (
237
+ f"{self._app.docs_url}/reference/template_components/"
238
+ f"parameters.html#{param.url_slug}"
239
+ )
240
+ param_typ_fmt = f"[link={param_url}]{param_typ_fmt}[/link]"
241
+ return param_typ_fmt
204
242
 
243
+ def __get_info(self, include: Sequence[str] = ()):
205
244
  if not include:
206
245
  include = ("inputs", "outputs", "actions")
207
246
 
@@ -209,9 +248,7 @@ class TaskSchema(JSONLike):
209
248
  tab.add_column(justify="right")
210
249
  tab.add_column()
211
250
 
212
- from rich.table import box
213
-
214
- tab_ins_outs = None
251
+ tab_ins_outs: Table | None = None
215
252
  if "inputs" in include or "outputs" in include:
216
253
  tab_ins_outs = Table(
217
254
  show_header=False,
@@ -226,6 +263,7 @@ class TaskSchema(JSONLike):
226
263
  tab_ins_outs.add_row()
227
264
 
228
265
  if "inputs" in include:
266
+ assert tab_ins_outs
229
267
  if self.inputs:
230
268
  tab_ins_outs.add_row(
231
269
  "",
@@ -236,19 +274,20 @@ class TaskSchema(JSONLike):
236
274
  for inp_idx, inp in enumerate(self.inputs):
237
275
  def_str = "-"
238
276
  if not inp.multiple:
239
- if inp.default_value is not NullDefault.NULL:
277
+ if self.__is_InputValue(inp.default_value):
240
278
  if inp.default_value.value is None:
241
279
  def_str = "None"
242
280
  else:
243
281
  def_str = f"{rich_esc(str(inp.default_value.value))!r}"
244
282
  tab_ins_outs.add_row(
245
283
  "" if inp_idx > 0 else "[bold]Inputs[/bold]",
246
- _format_parameter_type(inp.parameter),
247
- _get_param_type_str(inp.parameter),
284
+ self.__format_parameter_type(inp.parameter),
285
+ self.__get_param_type_str(inp.parameter),
248
286
  def_str,
249
287
  )
250
288
 
251
289
  if "outputs" in include:
290
+ assert tab_ins_outs
252
291
  if "inputs" in include:
253
292
  tab_ins_outs.add_row() # for spacing
254
293
  else:
@@ -261,8 +300,8 @@ class TaskSchema(JSONLike):
261
300
  for out_idx, out in enumerate(self.outputs):
262
301
  tab_ins_outs.add_row(
263
302
  "" if out_idx > 0 else "[bold]Outputs[/bold]",
264
- _format_parameter_type(out.parameter),
265
- _get_param_type_str(out.parameter),
303
+ self.__format_parameter_type(out.parameter),
304
+ self.__get_param_type_str(out.parameter),
266
305
  "",
267
306
  )
268
307
 
@@ -308,7 +347,7 @@ class TaskSchema(JSONLike):
308
347
  cmd_str = "cmd" if cmd.command else "exe"
309
348
  tab_cmds_i.add_row(
310
349
  f"[italic]{cmd_str}:[/italic]",
311
- rich_esc(cmd.command or cmd.executable),
350
+ rich_esc(cmd.command or cmd.executable or ""),
312
351
  )
313
352
  if cmd.stdout:
314
353
  tab_cmds_i.add_row(
@@ -329,36 +368,30 @@ class TaskSchema(JSONLike):
329
368
  panel = Panel(tab, title=f"Task schema: {rich_esc(self.objective.name)!r}")
330
369
  return panel
331
370
 
332
- def _show_info(self, include=None):
333
- panel = self._get_info(include=include)
334
- rich_print(panel)
335
-
336
- @property
337
- def basic_info(self):
371
+ def basic_info(self) -> None:
338
372
  """Show inputs and outputs, formatted in a table."""
339
- return self._show_info(include=("inputs", "outputs"))
373
+ rich_print(self.__get_info(include=("inputs", "outputs")))
340
374
 
341
- @property
342
- def info(self):
375
+ def info(self) -> None:
343
376
  """Show inputs, outputs, and actions, formatted in a table."""
344
- return self._show_info()
377
+ rich_print(self.__get_info(include=()))
345
378
 
346
379
  def get_info_html(self) -> str:
347
380
  """
348
381
  Describe the task schema as an HTML document.
349
382
  """
350
383
 
351
- def _format_parameter_type(param):
384
+ def _format_parameter_type(param: Parameter) -> str:
352
385
  param_typ_fmt = param.typ
353
386
  if param.typ in param_types:
354
387
  param_url = (
355
- f"{self.app.docs_url}/reference/template_components/"
388
+ f"{self._app.docs_url}/reference/template_components/"
356
389
  f"parameters.html#{param.url_slug}"
357
390
  )
358
391
  param_typ_fmt = f'<a href="{param_url}">{param_typ_fmt}</a>'
359
392
  return param_typ_fmt
360
393
 
361
- def _get_param_type_str(param) -> str:
394
+ def _get_param_type_str(param: Parameter) -> str:
362
395
  type_fmt = "-"
363
396
  if param._validation:
364
397
  try:
@@ -368,13 +401,15 @@ class TaskSchema(JSONLike):
368
401
  elif param._value_class:
369
402
  param_cls = param._value_class
370
403
  cls_url = (
371
- f"{self.app.docs_url}/reference/_autosummary/{param_cls.__module__}."
404
+ f"{self._app.docs_url}/reference/_autosummary/{param_cls.__module__}."
372
405
  f"{param_cls.__name__}"
373
406
  )
374
407
  type_fmt = f'<a href="{cls_url}">{param_cls.__name__}</a>'
375
408
  return type_fmt
376
409
 
377
- def _prepare_script_data_format_table(script_data_grouped):
410
+ def _prepare_script_data_format_table(
411
+ script_data_grouped: Mapping[str, Mapping[str, Mapping[str, str]]]
412
+ ) -> str:
378
413
  out = ""
379
414
  rows = ""
380
415
  for fmt, params in script_data_grouped.items():
@@ -388,14 +423,14 @@ class TaskSchema(JSONLike):
388
423
 
389
424
  return out
390
425
 
391
- param_types = self.app.parameters.list_attrs()
426
+ param_types = self.__parameters().list_attrs()
392
427
 
393
- inputs_header_row = f"<tr><th>parameter</th><th>type</th><th>default</th></tr>"
428
+ inputs_header_row = "<tr><th>parameter</th><th>type</th><th>default</th></tr>"
394
429
  input_rows = ""
395
430
  for inp in self.inputs:
396
431
  def_str = "-"
397
432
  if not inp.multiple:
398
- if inp.default_value is not NullDefault.NULL:
433
+ if self.__is_InputValue(inp.default_value):
399
434
  if inp.default_value.value is None:
400
435
  def_str = "None"
401
436
  else:
@@ -418,11 +453,11 @@ class TaskSchema(JSONLike):
418
453
  )
419
454
  else:
420
455
  inputs_table = (
421
- f'<span class="schema-note-no-inputs">This task schema has no input '
422
- f"parameters.</span>"
456
+ '<span class="schema-note-no-inputs">This task schema has no input '
457
+ "parameters.</span>"
423
458
  )
424
459
 
425
- outputs_header_row = f"<tr><th>parameter</th><th>type</th></tr>"
460
+ outputs_header_row = "<tr><th>parameter</th><th>type</th></tr>"
426
461
  output_rows = ""
427
462
  for out in self.outputs:
428
463
  param_str = _format_parameter_type(out.parameter)
@@ -437,8 +472,8 @@ class TaskSchema(JSONLike):
437
472
 
438
473
  else:
439
474
  outputs_table = (
440
- f'<span class="schema-note-no-outputs">This task schema has no output '
441
- f"parameters.</span>"
475
+ '<span class="schema-note-no-outputs">This task schema has no output '
476
+ "parameters.</span>"
442
477
  )
443
478
 
444
479
  action_rows = ""
@@ -496,7 +531,7 @@ class TaskSchema(JSONLike):
496
531
  num_inp_fg_rows = 0
497
532
  if act.input_file_generators:
498
533
  inp_fg = act.input_file_generators[0] # should be only one
499
- inps = ", ".join(f"<code>{i.typ}</code>" for i in inp_fg.inputs)
534
+ inps = ", ".join(f"<code>{in_.typ}</code>" for in_ in inp_fg.inputs)
500
535
  inp_fg_rows += (
501
536
  f"<tr>"
502
537
  f'<td class="action-header-cell">input file:</td>'
@@ -513,11 +548,13 @@ class TaskSchema(JSONLike):
513
548
  num_out_fp_rows = 0
514
549
  if act.output_file_parsers:
515
550
  out_fp = act.output_file_parsers[0] # should be only one
516
- files = ", ".join(f"<code>{i.label}</code>" for i in out_fp.output_files)
551
+ files = ", ".join(
552
+ f"<code>{of_.label}</code>" for of_ in out_fp.output_files
553
+ )
517
554
  out_fp_rows += (
518
555
  f"<tr>"
519
556
  f'<td class="action-header-cell">output:</td>'
520
- f"<td><code>{out_fp.output.typ}</code></td>"
557
+ f"<td><code>{out_fp.output.typ if out_fp.output else ''}</code></td>"
521
558
  f"</tr>"
522
559
  f"<tr>"
523
560
  f'<td class="action-header-cell">output files:</td>'
@@ -534,7 +571,7 @@ class TaskSchema(JSONLike):
534
571
  f'<td rowspan="{bool(cmd.stdout) + bool(cmd.stderr) + 1}">'
535
572
  f'<span class="cmd-idx-numeral">{cmd_idx}</span></td>'
536
573
  f'<td class="command-header-cell">{"cmd" if cmd.command else "exe"}:'
537
- f"</td><td><code><pre>{escape(cmd.command or cmd.executable)}</pre>"
574
+ f"</td><td><code><pre>{escape(cmd.command or cmd.executable or '')}</pre>"
538
575
  f"</code></td></tr>"
539
576
  )
540
577
  if cmd.stdout:
@@ -549,8 +586,8 @@ class TaskSchema(JSONLike):
549
586
  )
550
587
  if cmd_idx < len(act.commands) - 1:
551
588
  cmd_j_tab_rows += (
552
- f'<tr><td colspan="3" class="commands-table-bottom-spacer-cell">'
553
- f"</td></tr>"
589
+ '<tr><td colspan="3" class="commands-table-bottom-spacer-cell">'
590
+ "</td></tr>"
554
591
  )
555
592
  act_i_cmds_tab_rows += cmd_j_tab_rows
556
593
 
@@ -580,22 +617,22 @@ class TaskSchema(JSONLike):
580
617
  if action_rows:
581
618
  action_table = f'<table class="action-table hidden">{action_rows}</table>'
582
619
  action_show_hide = (
583
- f'<span class="actions-show-hide-toggle">[<span class="action-show-text">'
584
- f'show ↓</span><span class="action-hide-text hidden">hide ↑</span>]'
585
- f"</span>"
620
+ '<span class="actions-show-hide-toggle">[<span class="action-show-text">'
621
+ 'show ↓</span><span class="action-hide-text hidden">hide ↑</span>]'
622
+ "</span>"
586
623
  )
587
624
  act_heading_class = ' class="actions-heading"'
588
625
  else:
589
626
  action_table = (
590
- f'<span class="schema-note-no-actions">'
591
- f"This task schema has no actions.</span>"
627
+ '<span class="schema-note-no-actions">'
628
+ "This task schema has no actions.</span>"
592
629
  )
593
630
  action_show_hide = ""
594
631
  act_heading_class = ""
595
632
  description = (
596
633
  f"<h3 class='task-desc'>Description</h3>{self.doc}" if self.doc else ""
597
634
  )
598
- out = (
635
+ return (
599
636
  f"{description}"
600
637
  f"<h3>Inputs</h3>{inputs_table}"
601
638
  f"<h3>Outputs</h3>{outputs_table}"
@@ -603,12 +640,13 @@ class TaskSchema(JSONLike):
603
640
  f"<h3{act_heading_class}>Actions{action_show_hide}</h3>"
604
641
  f"{action_table}"
605
642
  )
606
- return out
607
643
 
608
- def __eq__(self, other):
609
- if type(other) is not self.__class__:
644
+ def __eq__(self, other: Any):
645
+ if id(self) == id(other):
646
+ return True
647
+ if not isinstance(other, self.__class__):
610
648
  return False
611
- if (
649
+ return (
612
650
  self.objective == other.objective
613
651
  and self.actions == other.actions
614
652
  and self.method == other.method
@@ -617,11 +655,9 @@ class TaskSchema(JSONLike):
617
655
  and self.outputs == other.outputs
618
656
  and self.version == other.version
619
657
  and self._hash_value == other._hash_value
620
- ):
621
- return True
622
- return False
658
+ )
623
659
 
624
- def __deepcopy__(self, memo):
660
+ def __deepcopy__(self, memo: dict[int, Any]) -> Self:
625
661
  kwargs = self.to_dict()
626
662
  obj = self.__class__(**copy.deepcopy(kwargs, memo))
627
663
  obj._task_template = self._task_template
@@ -629,7 +665,7 @@ class TaskSchema(JSONLike):
629
665
 
630
666
  @classmethod
631
667
  @contextmanager
632
- def ignore_invalid_actions(cls):
668
+ def ignore_invalid_actions(cls) -> Iterator[None]:
633
669
  """
634
670
  A context manager within which invalid actions will be ignored.
635
671
  """
@@ -639,71 +675,82 @@ class TaskSchema(JSONLike):
639
675
  finally:
640
676
  cls._validate_actions = True
641
677
 
642
- def _validate(self):
643
- if isinstance(self.objective, str):
644
- self.objective = self.app.TaskObjective(self.objective)
678
+ @classmethod
679
+ def __coerce_objective(cls, objective: TaskObjective | str) -> TaskObjective:
680
+ if isinstance(objective, str):
681
+ return cls._app.TaskObjective(objective)
682
+ else:
683
+ return objective
684
+
685
+ @classmethod
686
+ def __coerce_one_input(cls, inp: Parameter | SchemaInput) -> SchemaInput:
687
+ return cls._app.SchemaInput(inp) if cls.__is_Parameter(inp) else inp
688
+
689
+ @classmethod
690
+ def __coerce_inputs(
691
+ cls, inputs: Iterable[Parameter | SchemaInput]
692
+ ) -> list[SchemaInput]:
693
+ """coerce Parameters to SchemaInputs"""
694
+ return [cls.__coerce_one_input(inp) for inp in inputs]
645
695
 
696
+ @classmethod
697
+ def __coerce_one_output(cls, out: Parameter | SchemaParameter) -> SchemaOutput:
698
+ return (
699
+ out
700
+ if cls.__is_SchemaOutput(out)
701
+ else cls._app.SchemaOutput(out if cls.__is_Parameter(out) else out.parameter)
702
+ )
703
+
704
+ @classmethod
705
+ def __coerce_outputs(
706
+ cls, outputs: Iterable[Parameter | SchemaParameter]
707
+ ) -> list[SchemaOutput]:
708
+ """coerce Parameters to SchemaOutputs"""
709
+ return [cls.__coerce_one_output(out) for out in outputs]
710
+
711
+ def _validate(self) -> None:
646
712
  if self.method:
647
713
  self.method = check_valid_py_identifier(self.method)
648
714
  if self.implementation:
649
715
  self.implementation = check_valid_py_identifier(self.implementation)
650
716
 
651
- # coerce Parameters to SchemaInputs
652
- for idx, i in enumerate(self.inputs):
653
- if isinstance(
654
- i, Parameter
655
- ): # TODO: doc. that we should use the sdk class for type checking!
656
- self.inputs[idx] = self.app.SchemaInput(i)
657
-
658
- # coerce Parameters to SchemaOutputs
659
- for idx, i in enumerate(self.outputs):
660
- if isinstance(i, Parameter):
661
- self.outputs[idx] = self.app.SchemaOutput(i)
662
- elif isinstance(i, SchemaInput):
663
- self.outputs[idx] = self.app.SchemaOutput(i.parameter)
664
-
665
717
  # check action input/outputs
666
718
  if self._validate_actions:
667
719
  has_script = any(
668
- i.script and not i.input_file_generators and not i.output_file_parsers
669
- for i in self.actions
720
+ act.script
721
+ and not act.input_file_generators
722
+ and not act.output_file_parsers
723
+ for act in self.actions
670
724
  )
671
725
 
672
- all_outs = []
726
+ all_outs: set[str] = set()
673
727
  extra_ins = set(self.input_types)
674
728
 
675
729
  act_ins_lst = [act.get_input_types() for act in self.actions]
676
730
  act_outs_lst = [act.get_output_types() for act in self.actions]
677
731
 
678
- schema_ins = set(self.input_types)
679
732
  schema_outs = set(self.output_types)
680
733
 
681
- all_act_ins = set(j for i in act_ins_lst for j in i)
682
- all_act_outs = set(j for i in act_outs_lst for j in i)
734
+ all_act_ins = set(chain.from_iterable(act_ins_lst))
735
+ all_act_outs = set(chain.from_iterable(act_outs_lst))
683
736
 
684
- non_schema_act_ins = all_act_ins - schema_ins
685
- non_schema_act_outs = set(all_act_outs - schema_outs)
737
+ non_schema_act_ins = all_act_ins.difference(self.input_types)
738
+ non_schema_act_outs = all_act_outs.difference(schema_outs)
686
739
 
687
740
  extra_act_outs = non_schema_act_outs
688
- seen_act_outs = []
741
+ seen_act_outs: set[str] = set()
689
742
  for act_idx in range(len(self.actions)):
690
- for act_in in [
691
- i for i in act_ins_lst[act_idx] if i in non_schema_act_ins
692
- ]:
693
- if act_in not in seen_act_outs:
743
+ for act_in in act_ins_lst[act_idx]:
744
+ if act_in in non_schema_act_ins and act_in not in seen_act_outs:
694
745
  raise ValueError(
695
746
  f"Action {act_idx} input {act_in!r} of schema {self.name!r} "
696
747
  f"is not a schema input, but nor is it an action output from "
697
748
  f"a preceding action."
698
749
  )
699
- seen_act_outs += [
700
- i for i in act_outs_lst[act_idx] if i not in seen_act_outs
701
- ]
702
- extra_act_outs = extra_act_outs - set(act_ins_lst[act_idx])
703
- act_inputs = set(act_ins_lst[act_idx])
704
- act_outputs = set(act_outs_lst[act_idx])
705
- extra_ins = extra_ins - act_inputs
706
- all_outs.extend(list(act_outputs))
750
+ seen_act_outs.update(act_outs_lst[act_idx])
751
+ extra_act_outs.difference_update(act_ins_lst[act_idx])
752
+ extra_ins.difference_update(act_ins_lst[act_idx])
753
+ all_outs.update(act_outs_lst[act_idx])
707
754
 
708
755
  if extra_act_outs:
709
756
  raise ValueError(
@@ -717,9 +764,9 @@ class TaskSchema(JSONLike):
717
764
  # i.e. are all schema inputs "consumed" by an action?
718
765
 
719
766
  # consider OFP inputs:
720
- for act_i in self.actions:
721
- for OFP_j in act_i.output_file_parsers:
722
- extra_ins = extra_ins - set(OFP_j.inputs or [])
767
+ for act in self.actions:
768
+ for ofp in act.output_file_parsers:
769
+ extra_ins.difference_update(ofp.inputs or ())
723
770
 
724
771
  if self.actions and extra_ins:
725
772
  # allow for no actions (e.g. defining inputs for downstream tasks)
@@ -728,7 +775,7 @@ class TaskSchema(JSONLike):
728
775
  f"by any actions."
729
776
  )
730
777
 
731
- missing_outs = set(self.output_types) - set(all_outs)
778
+ missing_outs = schema_outs - all_outs
732
779
  if missing_outs and not has_script:
733
780
  # TODO: bit of a hack, need to consider script ins/outs later
734
781
  raise ValueError(
@@ -736,12 +783,12 @@ class TaskSchema(JSONLike):
736
783
  f"generated by any actions."
737
784
  )
738
785
 
739
- def _expand_actions(self):
786
+ def __expand_actions(self) -> list[Action]:
740
787
  """Create new actions for input file generators and output parsers in existing
741
788
  actions."""
742
- return [j for i in self.actions for j in i.expand()]
789
+ return [new_act for act in self.actions for new_act in act.expand()]
743
790
 
744
- def _update_parameter_value_classes(self):
791
+ def __update_parameter_value_classes(self):
745
792
  # ensure any referenced parameter_class_modules are imported:
746
793
  for module in self.parameter_class_modules:
747
794
  import_module(module)
@@ -755,12 +802,14 @@ class TaskSchema(JSONLike):
755
802
  for out in self.outputs:
756
803
  out.parameter._set_value_class()
757
804
 
758
- def make_persistent(self, workflow: app.Workflow, source: Dict) -> List[int]:
805
+ def make_persistent(
806
+ self, workflow: Workflow, source: ParamSource
807
+ ) -> list[int | list[int]]:
759
808
  """
760
809
  Convert this task schema to persistent form within the context of the given
761
810
  workflow.
762
811
  """
763
- new_refs = []
812
+ new_refs: list[int | list[int]] = []
764
813
  for input_i in self.inputs:
765
814
  for lab_info in input_i.labelled_info():
766
815
  if "default_value" in lab_info:
@@ -771,78 +820,77 @@ class TaskSchema(JSONLike):
771
820
  return new_refs
772
821
 
773
822
  @property
774
- def name(self):
823
+ def name(self) -> str:
775
824
  """
776
825
  The name of this schema.
777
826
  """
778
- out = (
827
+ return (
779
828
  f"{self.objective.name}"
780
829
  f"{f'_{self.method}' if self.method else ''}"
781
830
  f"{f'_{self.implementation}' if self.implementation else ''}"
782
831
  )
783
- return out
784
832
 
785
833
  @property
786
- def input_types(self):
834
+ def input_types(self) -> list[str]:
787
835
  """
788
836
  The input types to the schema.
789
837
  """
790
- return tuple(j for i in self.inputs for j in i.all_labelled_types)
838
+ return [typ for inp in self.inputs for typ in inp.all_labelled_types]
791
839
 
792
840
  @property
793
- def output_types(self):
841
+ def output_types(self) -> list[str]:
794
842
  """
795
843
  The output types from the schema.
796
844
  """
797
- return tuple(i.typ for i in self.outputs)
845
+ return [out.typ for out in self.outputs]
798
846
 
799
847
  @property
800
- def provides_parameters(self) -> Tuple[Tuple[str, str]]:
848
+ def provides_parameters(self) -> Iterator[tuple[str, str]]:
801
849
  """
802
850
  The parameters that this schema provides.
803
851
  """
804
- out = []
805
852
  for schema_inp in self.inputs:
806
- for labelled_info in schema_inp.labelled_info():
807
- prop_mode = labelled_info["propagation_mode"]
853
+ for label, prop_mode in schema_inp._simple_labelled_info:
808
854
  if prop_mode is not ParameterPropagationMode.NEVER:
809
- out.append(
810
- (schema_inp.input_or_output, labelled_info["labelled_type"])
811
- )
855
+ yield (schema_inp.input_or_output, label)
812
856
  for schema_out in self.outputs:
813
857
  if schema_out.propagation_mode is not ParameterPropagationMode.NEVER:
814
- out.append((schema_out.input_or_output, schema_out.typ))
815
- return tuple(out)
858
+ yield (schema_out.input_or_output, schema_out.typ)
816
859
 
817
860
  @property
818
- def task_template(self):
861
+ def task_template(self) -> TaskTemplate | None:
819
862
  """
820
863
  The template that this schema is contained in.
821
864
  """
822
865
  return self._task_template
823
866
 
824
867
  @classmethod
825
- def get_by_key(cls, key):
868
+ def get_by_key(cls, key: str) -> TaskSchema:
826
869
  """Get a config-loaded task schema from a key."""
827
- return cls.app.task_schemas.get(key)
870
+ return cls.__task_schemas().get(key)
828
871
 
829
- def get_parameter_dependence(self, parameter: app.SchemaParameter):
872
+ def get_parameter_dependence(
873
+ self, parameter: SchemaParameter
874
+ ) -> ActParameterDependence:
830
875
  """Find if/where a given parameter is used by the schema's actions."""
831
- out = {"input_file_writers": [], "commands": []}
876
+ out: ActParameterDependence = {"input_file_writers": [], "commands": []}
832
877
  for act_idx, action in enumerate(self.actions):
833
878
  deps = action.get_parameter_dependence(parameter)
834
- for key in out:
835
- out[key].extend((act_idx, i) for i in deps[key])
879
+ out["input_file_writers"].extend(
880
+ (act_idx, ifw) for ifw in deps["input_file_writers"]
881
+ )
882
+ out["commands"].extend((act_idx, cmd) for cmd in deps["commands"])
836
883
  return out
837
884
 
838
- def get_key(self):
885
+ def get_key(self) -> tuple:
839
886
  """
840
887
  Get the hashable value that represents this schema.
841
888
  """
842
889
  return (str(self.objective), self.method, self.implementation)
843
890
 
844
- def _get_single_label_lookup(self, prefix="") -> Dict[str, str]:
845
- """Get a mapping between schema input types that have a single label (i.e.
891
+ def _get_single_label_lookup(self, prefix: str = "") -> Mapping[str, str]:
892
+ """
893
+ Get a mapping between schema input types that have a single label (i.e.
846
894
  labelled but with `multiple=False`) and the non-labelled type string.
847
895
 
848
896
  For example, if a task schema has a schema input like:
@@ -854,7 +902,7 @@ class TaskSchema(JSONLike):
854
902
  `{"inputs.p1[one]": "inputs.p1"}`.
855
903
 
856
904
  """
857
- lookup = {}
905
+ lookup: dict[str, str] = {}
858
906
  if prefix and not prefix.endswith("."):
859
907
  prefix += "."
860
908
  for sch_inp in self.inputs:
@@ -864,10 +912,6 @@ class TaskSchema(JSONLike):
864
912
  return lookup
865
913
 
866
914
  @property
867
- def multi_input_types(self) -> List[str]:
915
+ def multi_input_types(self) -> list[str]:
868
916
  """Get a list of input types that have multiple labels."""
869
- out = []
870
- for inp in self.inputs:
871
- if inp.multiple:
872
- out.append(inp.parameter.typ)
873
- return out
917
+ return [inp.parameter.typ for inp in self.inputs if inp.multiple]