hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a199__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 (176) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +9 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/bad_script.py +2 -0
  5. hpcflow/data/scripts/do_nothing.py +2 -0
  6. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  7. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  8. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  11. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  12. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  13. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  15. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  16. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  23. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  24. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  25. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  26. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  27. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  28. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  29. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  30. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  31. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  32. hpcflow/data/scripts/script_exit_test.py +5 -0
  33. hpcflow/data/template_components/environments.yaml +1 -1
  34. hpcflow/sdk/__init__.py +26 -15
  35. hpcflow/sdk/app.py +2192 -768
  36. hpcflow/sdk/cli.py +506 -296
  37. hpcflow/sdk/cli_common.py +105 -7
  38. hpcflow/sdk/config/__init__.py +1 -1
  39. hpcflow/sdk/config/callbacks.py +115 -43
  40. hpcflow/sdk/config/cli.py +126 -103
  41. hpcflow/sdk/config/config.py +674 -318
  42. hpcflow/sdk/config/config_file.py +131 -95
  43. hpcflow/sdk/config/errors.py +125 -84
  44. hpcflow/sdk/config/types.py +148 -0
  45. hpcflow/sdk/core/__init__.py +25 -1
  46. hpcflow/sdk/core/actions.py +1771 -1059
  47. hpcflow/sdk/core/app_aware.py +24 -0
  48. hpcflow/sdk/core/cache.py +139 -79
  49. hpcflow/sdk/core/command_files.py +263 -287
  50. hpcflow/sdk/core/commands.py +145 -112
  51. hpcflow/sdk/core/element.py +828 -535
  52. hpcflow/sdk/core/enums.py +192 -0
  53. hpcflow/sdk/core/environment.py +74 -93
  54. hpcflow/sdk/core/errors.py +455 -52
  55. hpcflow/sdk/core/execute.py +207 -0
  56. hpcflow/sdk/core/json_like.py +540 -272
  57. hpcflow/sdk/core/loop.py +751 -347
  58. hpcflow/sdk/core/loop_cache.py +164 -47
  59. hpcflow/sdk/core/object_list.py +370 -207
  60. hpcflow/sdk/core/parameters.py +1100 -627
  61. hpcflow/sdk/core/rule.py +59 -41
  62. hpcflow/sdk/core/run_dir_files.py +21 -37
  63. hpcflow/sdk/core/skip_reason.py +7 -0
  64. hpcflow/sdk/core/task.py +1649 -1339
  65. hpcflow/sdk/core/task_schema.py +308 -196
  66. hpcflow/sdk/core/test_utils.py +191 -114
  67. hpcflow/sdk/core/types.py +440 -0
  68. hpcflow/sdk/core/utils.py +485 -309
  69. hpcflow/sdk/core/validation.py +82 -9
  70. hpcflow/sdk/core/workflow.py +2544 -1178
  71. hpcflow/sdk/core/zarr_io.py +98 -137
  72. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  73. hpcflow/sdk/demo/cli.py +53 -33
  74. hpcflow/sdk/helper/cli.py +18 -15
  75. hpcflow/sdk/helper/helper.py +75 -63
  76. hpcflow/sdk/helper/watcher.py +61 -28
  77. hpcflow/sdk/log.py +122 -71
  78. hpcflow/sdk/persistence/__init__.py +8 -31
  79. hpcflow/sdk/persistence/base.py +1360 -606
  80. hpcflow/sdk/persistence/defaults.py +6 -0
  81. hpcflow/sdk/persistence/discovery.py +38 -0
  82. hpcflow/sdk/persistence/json.py +568 -188
  83. hpcflow/sdk/persistence/pending.py +382 -179
  84. hpcflow/sdk/persistence/store_resource.py +39 -23
  85. hpcflow/sdk/persistence/types.py +318 -0
  86. hpcflow/sdk/persistence/utils.py +14 -11
  87. hpcflow/sdk/persistence/zarr.py +1337 -433
  88. hpcflow/sdk/runtime.py +44 -41
  89. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  90. hpcflow/sdk/submission/jobscript.py +1651 -692
  91. hpcflow/sdk/submission/schedulers/__init__.py +167 -39
  92. hpcflow/sdk/submission/schedulers/direct.py +121 -81
  93. hpcflow/sdk/submission/schedulers/sge.py +170 -129
  94. hpcflow/sdk/submission/schedulers/slurm.py +291 -268
  95. hpcflow/sdk/submission/schedulers/utils.py +12 -2
  96. hpcflow/sdk/submission/shells/__init__.py +14 -15
  97. hpcflow/sdk/submission/shells/base.py +150 -29
  98. hpcflow/sdk/submission/shells/bash.py +283 -173
  99. hpcflow/sdk/submission/shells/os_version.py +31 -30
  100. hpcflow/sdk/submission/shells/powershell.py +228 -170
  101. hpcflow/sdk/submission/submission.py +1014 -335
  102. hpcflow/sdk/submission/types.py +140 -0
  103. hpcflow/sdk/typing.py +182 -12
  104. hpcflow/sdk/utils/arrays.py +71 -0
  105. hpcflow/sdk/utils/deferred_file.py +55 -0
  106. hpcflow/sdk/utils/hashing.py +16 -0
  107. hpcflow/sdk/utils/patches.py +12 -0
  108. hpcflow/sdk/utils/strings.py +33 -0
  109. hpcflow/tests/api/test_api.py +32 -0
  110. hpcflow/tests/conftest.py +27 -6
  111. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  112. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  113. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  114. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  115. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  116. hpcflow/tests/scripts/test_main_scripts.py +866 -85
  117. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  118. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  119. hpcflow/tests/shells/wsl/test_wsl_submission.py +12 -4
  120. hpcflow/tests/unit/test_action.py +262 -75
  121. hpcflow/tests/unit/test_action_rule.py +9 -4
  122. hpcflow/tests/unit/test_app.py +33 -6
  123. hpcflow/tests/unit/test_cache.py +46 -0
  124. hpcflow/tests/unit/test_cli.py +134 -1
  125. hpcflow/tests/unit/test_command.py +71 -54
  126. hpcflow/tests/unit/test_config.py +142 -16
  127. hpcflow/tests/unit/test_config_file.py +21 -18
  128. hpcflow/tests/unit/test_element.py +58 -62
  129. hpcflow/tests/unit/test_element_iteration.py +50 -1
  130. hpcflow/tests/unit/test_element_set.py +29 -19
  131. hpcflow/tests/unit/test_group.py +4 -2
  132. hpcflow/tests/unit/test_input_source.py +116 -93
  133. hpcflow/tests/unit/test_input_value.py +29 -24
  134. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  135. hpcflow/tests/unit/test_json_like.py +44 -35
  136. hpcflow/tests/unit/test_loop.py +1396 -84
  137. hpcflow/tests/unit/test_meta_task.py +325 -0
  138. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  139. hpcflow/tests/unit/test_object_list.py +17 -12
  140. hpcflow/tests/unit/test_parameter.py +29 -7
  141. hpcflow/tests/unit/test_persistence.py +237 -42
  142. hpcflow/tests/unit/test_resources.py +20 -18
  143. hpcflow/tests/unit/test_run.py +117 -6
  144. hpcflow/tests/unit/test_run_directories.py +29 -0
  145. hpcflow/tests/unit/test_runtime.py +2 -1
  146. hpcflow/tests/unit/test_schema_input.py +23 -15
  147. hpcflow/tests/unit/test_shell.py +23 -2
  148. hpcflow/tests/unit/test_slurm.py +8 -7
  149. hpcflow/tests/unit/test_submission.py +38 -89
  150. hpcflow/tests/unit/test_task.py +352 -247
  151. hpcflow/tests/unit/test_task_schema.py +33 -20
  152. hpcflow/tests/unit/test_utils.py +9 -11
  153. hpcflow/tests/unit/test_value_sequence.py +15 -12
  154. hpcflow/tests/unit/test_workflow.py +114 -83
  155. hpcflow/tests/unit/test_workflow_template.py +0 -1
  156. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  157. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  158. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  159. hpcflow/tests/unit/utils/test_patches.py +5 -0
  160. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  161. hpcflow/tests/workflows/__init__.py +0 -0
  162. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  163. hpcflow/tests/workflows/test_jobscript.py +334 -1
  164. hpcflow/tests/workflows/test_run_status.py +198 -0
  165. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  166. hpcflow/tests/workflows/test_submission.py +140 -0
  167. hpcflow/tests/workflows/test_workflows.py +160 -15
  168. hpcflow/tests/workflows/test_zip.py +18 -0
  169. hpcflow/viz_demo.ipynb +6587 -3
  170. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +8 -4
  171. hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -0
  172. hpcflow/sdk/core/parallel.py +0 -21
  173. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  174. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/LICENSE +0 -0
  175. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
  176. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
@@ -2,22 +2,32 @@
2
2
  Model of a command run in an action.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  from dataclasses import dataclass, field
6
7
  from functools import partial
7
8
  from pathlib import Path
8
9
  import re
9
- from typing import Dict, Iterable, List, Optional, Tuple, Union
10
+ from typing import Any, ClassVar, TYPE_CHECKING
10
11
 
11
12
  import numpy as np
12
13
 
13
- from hpcflow.sdk import app
14
+ from hpcflow.sdk.typing import hydrate
14
15
  from hpcflow.sdk.core.element import ElementResources
15
16
  from hpcflow.sdk.core.errors import NoCLIFormatMethodError
16
17
  from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
17
18
  from hpcflow.sdk.core.parameters import ParameterValue
18
19
 
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Callable, Iterable, Mapping, Sequence
22
+ from re import Pattern
23
+ from .actions import ActionRule, Action
24
+ from .element import ElementActionRun
25
+ from .environment import Environment
26
+ from ..submission.shells import Shell
27
+
19
28
 
20
29
  @dataclass
30
+ @hydrate
21
31
  class Command(JSONLike):
22
32
  """
23
33
  A command that may be run within a workflow action.
@@ -43,10 +53,7 @@ class Command(JSONLike):
43
53
  Rules that state whether this command is eligible to run.
44
54
  """
45
55
 
46
- # TODO: What is the difference between command and executable?
47
-
48
- _app_attr = "app"
49
- _child_objects = (
56
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
50
57
  ChildObjectSpec(
51
58
  name="rules",
52
59
  class_name="ActionRule",
@@ -57,22 +64,27 @@ class Command(JSONLike):
57
64
 
58
65
  #: The actual command.
59
66
  #: Overrides :py:attr:`executable`.
60
- command: Optional[str] = None
67
+ command: str | None = None
61
68
  #: The executable to run,
62
69
  #: from the set of executable managed by the environment.
63
- executable: Optional[str] = None
70
+ executable: str | None = None
64
71
  #: The arguments to pass in.
65
- arguments: Optional[List[str]] = None
72
+ arguments: list[str] | None = None
66
73
  #: Values that may be substituted when preparing the arguments.
67
- variables: Optional[Dict[str, str]] = None
74
+ variables: dict[str, str] | None = None
68
75
  #: The name of a file to write standard output to.
69
- stdout: Optional[str] = None
76
+ stdout: str | None = None
70
77
  #: The name of a file to write standard error to.
71
- stderr: Optional[str] = None
78
+ stderr: str | None = None
72
79
  #: The name of a file to read standard input from.
73
- stdin: Optional[str] = None
80
+ stdin: str | None = None
74
81
  #: Rules that state whether this command is eligible to run.
75
- rules: Optional[List[app.ActionRule]] = field(default_factory=lambda: [])
82
+ rules: list[ActionRule] = field(default_factory=list)
83
+
84
+ action: Action | None = None # assigned by parent Action
85
+
86
+ def __post_init__(self):
87
+ self._set_parent_refs()
76
88
 
77
89
  def __repr__(self) -> str:
78
90
  out = []
@@ -95,61 +107,78 @@ class Command(JSONLike):
95
107
 
96
108
  return f"{self.__class__.__name__}({', '.join(out)})"
97
109
 
98
- def _get_initial_command_line(self) -> str:
110
+ def __eq__(self, other):
111
+ if not isinstance(other, self.__class__):
112
+ return False
113
+ return (
114
+ self.command == other.command
115
+ and self.executable == other.executable
116
+ and self.arguments == other.arguments
117
+ and self.variables == other.variables
118
+ and self.stdout == other.stdout
119
+ and self.stderr == other.stderr
120
+ and self.stdin == other.stdin
121
+ and self.rules == other.rules
122
+ )
123
+
124
+ def __get_initial_command_line(self) -> str:
99
125
  if self.command:
100
126
  return self.command
101
127
  else:
102
128
  return self.executable or ""
103
129
 
104
- def get_command_line(self, EAR, shell, env) -> Tuple[str, List[Tuple[str, str]]]:
130
+ __EXE_SCRIPT_RE: ClassVar[Pattern] = re.compile(r"\<\<(executable|script):(.*?)\>\>")
131
+ __ENV_SPEC_RE: ClassVar[Pattern] = re.compile(r"\<\<env:(.*?)\>\>")
132
+
133
+ def get_command_line(
134
+ self, EAR: ElementActionRun, shell: Shell, env: Environment
135
+ ) -> tuple[str, list[tuple[str, ...]]]:
105
136
  """Return the resolved command line.
106
137
 
107
138
  This is ordinarily called at run-time by `Workflow.write_commands`.
108
-
109
139
  """
110
140
 
111
- self.app.persistence_logger.debug("Command.get_command_line")
112
- cmd_str = self._get_initial_command_line()
141
+ self._app.persistence_logger.debug("Command.get_command_line")
142
+ cmd_str = self.__get_initial_command_line()
113
143
 
114
144
  def _format_sum(iterable: Iterable) -> str:
115
145
  return str(sum(iterable))
116
146
 
117
147
  def _join(iterable: Iterable, delim: str) -> str:
118
- return delim.join(str(i) for i in iterable)
148
+ return delim.join(map(str, iterable))
119
149
 
120
- parse_types = {
150
+ parse_types: dict[str, Callable[..., str]] = {
121
151
  "sum": _format_sum,
122
152
  "join": _join,
123
153
  }
124
154
 
125
- def exec_script_repl(match_obj):
155
+ def exec_script_repl(match_obj: re.Match[str]) -> str:
126
156
  typ, val = match_obj.groups()
127
157
  if typ == "executable":
128
158
  executable = env.executables.get(val)
129
159
  filterable = ElementResources.get_env_instance_filterable_attributes()
130
- filter_exec = {j: EAR.get_resources().get(j) for j in filterable}
160
+ filter_exec = {attr: EAR.get_resources().get(attr) for attr in filterable}
131
161
  exec_cmd = executable.filter_instances(**filter_exec)[0].command
132
- out = exec_cmd.replace("<<num_cores>>", str(EAR.resources.num_cores))
162
+ return exec_cmd.replace("<<num_cores>>", str(EAR.resources.num_cores))
133
163
  elif typ == "script":
134
- out = EAR.action.get_script_name(val)
135
- return out
164
+ # TODO: is this needed? we have <<script_name>> <<script_path>> etc as command variables
165
+ return EAR.action.get_script_name(val)
166
+ else:
167
+ raise ValueError("impossible match occurred")
136
168
 
137
- def input_param_repl(match_obj, inp_val):
169
+ def input_param_repl(match_obj: re.Match[str], inp_val) -> str:
138
170
  _, func, func_kwargs, method, method_kwargs = match_obj.groups()
139
171
 
140
172
  if isinstance(inp_val, ParameterValue):
141
173
  if not method:
142
174
  method = "CLI_format"
143
175
  if not hasattr(inp_val, method):
144
- raise NoCLIFormatMethodError(
145
- f"No CLI format method {method!r} exists for the "
146
- f"object {inp_val!r}."
147
- )
148
- kwargs = self._prepare_kwargs_from_string(args_str=method_kwargs)
176
+ raise NoCLIFormatMethodError(method, inp_val)
177
+ kwargs = self.__prepare_kwargs_from_string(args_str=method_kwargs)
149
178
  inp_val = getattr(inp_val, method)(**kwargs)
150
179
 
151
180
  if func:
152
- kwargs = self._prepare_kwargs_from_string(
181
+ kwargs = self.__prepare_kwargs_from_string(
153
182
  args_str=func_kwargs,
154
183
  doubled_quoted_args=["delim"],
155
184
  )
@@ -158,12 +187,9 @@ class Command(JSONLike):
158
187
  return str(inp_val)
159
188
 
160
189
  file_regex = r"(\<\<file:{}\>\>?)"
161
- exe_script_regex = r"\<\<(executable|script):(.*?)\>\>"
162
- env_specs_regex = r"\<\<env:(.*?)\>\>"
163
190
 
164
191
  # substitute executables:
165
- cmd_str = re.sub(
166
- pattern=exe_script_regex,
192
+ cmd_str = self.__EXE_SCRIPT_RE.sub(
167
193
  repl=exec_script_repl,
168
194
  string=cmd_str,
169
195
  )
@@ -172,21 +198,24 @@ class Command(JSONLike):
172
198
  # an `<<args>>` variable::
173
199
  for var_key, var_val in (self.variables or {}).items():
174
200
  # substitute any `<<env:>>` specifiers
175
- var_val = re.sub(
176
- pattern=env_specs_regex,
177
- repl=lambda match_obj: EAR.env_spec[match_obj.group(1)],
201
+ var_val = self.__ENV_SPEC_RE.sub(
202
+ repl=lambda match_obj: EAR.env_spec[match_obj[1]],
178
203
  string=var_val,
179
204
  )
180
205
  cmd_str = cmd_str.replace(f"<<{var_key}>>", var_val)
181
206
  if "<<args>>" in cmd_str:
182
- args_str = " ".join(self.arguments or [])
207
+ args_str = " ".join(self.arguments or ())
183
208
  ends_in_args = cmd_str.endswith("<<args>>")
184
209
  cmd_str = cmd_str.replace("<<args>>", args_str)
185
210
  if ends_in_args and not args_str:
186
211
  cmd_str = cmd_str.rstrip()
187
212
 
188
213
  # remove any left over "<<args>>" and "<<script_name>>"s:
189
- cmd_str = cmd_str.replace("<<args>>", "").replace("<<script_name>>", "")
214
+ cmd_str = (
215
+ cmd_str.replace("<<args>>", "")
216
+ .replace("<<script_name>>", "")
217
+ .replace("<<script_path>>", "")
218
+ )
190
219
 
191
220
  # substitute input parameters in command:
192
221
  types_pattern = "|".join(parse_types)
@@ -203,7 +232,10 @@ class Command(JSONLike):
203
232
  cmd_inp = ".".join(cmd_inp_parts[:-1])
204
233
  else:
205
234
  cmd_inp = cmd_inp_full
206
- inp_val = EAR.get(f"inputs.{cmd_inp}") # TODO: what if schema output?
235
+ inp_val = EAR.get(
236
+ f"inputs.{cmd_inp}",
237
+ raise_on_unset=True,
238
+ ) # TODO: what if schema output?
207
239
  pattern_i = pattern.format(
208
240
  types_pattern=types_pattern,
209
241
  name=re.escape(cmd_inp),
@@ -214,9 +246,11 @@ class Command(JSONLike):
214
246
  string=cmd_str,
215
247
  )
216
248
 
217
- # substitute input files in command:
218
- for cmd_file in EAR.action.get_command_input_file_labels():
219
- file_path = EAR.get(f"input_files.{cmd_file}") # TODO: what if out file?
249
+ # substitute input/output files in command:
250
+ for cmd_file in EAR.action.get_command_file_labels():
251
+ file_path = EAR.get(
252
+ f"input_files.{cmd_file}", raise_on_unset=True
253
+ ) or EAR.get(f"output_files.{cmd_file}", raise_on_unset=True)
220
254
  # assuming we have copied this file to the EAR directory, then we just
221
255
  # need the file name:
222
256
  file_name = Path(file_path).name
@@ -226,7 +260,7 @@ class Command(JSONLike):
226
260
  string=cmd_str,
227
261
  )
228
262
 
229
- shell_vars = []
263
+ shell_vars: list[tuple[str, ...]] = []
230
264
  out_types = self.get_output_types()
231
265
  if out_types["stdout"]:
232
266
  # TODO: also map stderr/both if possible
@@ -246,56 +280,53 @@ class Command(JSONLike):
246
280
 
247
281
  return cmd_str, shell_vars
248
282
 
249
- def get_output_types(self):
283
+ # note: we use "parameter" rather than "output", because it could be a schema
284
+ # output or schema input.
285
+ __PARAM_RE: ClassVar[Pattern] = re.compile(
286
+ r"(?:\<\<(?:\w+(?:\[(?:.*)\])?\()?parameter:(\w+)"
287
+ r"(?:\.(?:\w+)\((?:.*?)\))?\)?\>\>?)"
288
+ )
289
+
290
+ def get_output_types(self) -> Mapping[str, str | None]:
250
291
  """
251
292
  Get whether stdout and stderr are workflow parameters.
252
293
  """
253
- # note: we use "parameter" rather than "output", because it could be a schema
254
- # output or schema input.
255
- pattern = (
256
- r"(?:\<\<(?:\w+(?:\[(?:.*)\])?\()?parameter:(\w+)"
257
- r"(?:\.(?:\w+)\((?:.*?)\))?\)?\>\>?)"
258
- )
259
- out = {"stdout": None, "stderr": None}
294
+ out: dict[str, str | None] = {"stdout": None, "stderr": None}
260
295
  for i, label in zip((self.stdout, self.stderr), ("stdout", "stderr")):
261
- if i:
262
- match = re.search(pattern, i)
263
- if match:
264
- param_typ = match.group(1)
265
- if match.span(0) != (0, len(i)):
266
- raise ValueError(
267
- f"If specified as a parameter, `{label}` must not include"
268
- f" any characters other than the parameter "
269
- f"specification, but this was given: {i!r}."
270
- )
271
- out[label] = param_typ
296
+ if i and (match := self.__PARAM_RE.search(i)):
297
+ param_typ: str = match[1]
298
+ if match.span(0) != (0, len(i)):
299
+ raise ValueError(
300
+ f"If specified as a parameter, `{label}` must not include"
301
+ f" any characters other than the parameter "
302
+ f"specification, but this was given: {i!r}."
303
+ )
304
+ out[label] = param_typ
272
305
  return out
273
306
 
274
307
  @staticmethod
275
- def _prepare_kwargs_from_string(args_str: Union[str, None], doubled_quoted_args=None):
276
- kwargs = {}
308
+ def __prepare_kwargs_from_string(
309
+ args_str: str | None, doubled_quoted_args: list[str] | None = None
310
+ ) -> dict[str, str]:
277
311
  if args_str is None:
278
- return kwargs
312
+ return {}
279
313
 
314
+ kwargs: dict[str, str] = {}
280
315
  # deal with specified double-quoted arguments first if it exists:
281
- for quote_arg in doubled_quoted_args or []:
316
+ for quote_arg in doubled_quoted_args or ():
282
317
  quote_pat = r'.*({quote_arg}="(.*)").*'.format(quote_arg=quote_arg)
283
- match = re.match(quote_pat, args_str)
284
- if match:
318
+ if match := re.match(quote_pat, args_str):
285
319
  quote_str, quote_contents = match.groups()
286
320
  args_str = args_str.replace(quote_str, "")
287
321
  kwargs[quote_arg] = quote_contents
288
322
 
289
- args_str = args_str.strip().strip(",")
290
- if args_str:
291
- for i in args_str.split(","):
292
- i_split = i.split("=")
293
- name_i = i_split[0].strip()
294
- value = i_split[1].strip()
295
- kwargs[name_i] = value
323
+ if args_str := args_str.strip().strip(","):
324
+ for arg_part in args_str.split(","):
325
+ name_i, value_i = map(str.strip, arg_part.split("="))
326
+ kwargs[name_i] = value_i
296
327
  return kwargs
297
328
 
298
- def process_std_stream(self, name: str, value: str, stderr: bool):
329
+ def process_std_stream(self, name: str, value: str, stderr: bool) -> Any:
299
330
  """
300
331
  Process a description of a standard stread from a command to get how it becomes
301
332
  a workflow parameter for later actions.
@@ -310,15 +341,19 @@ class Command(JSONLike):
310
341
  If true, this is handling the stderr stream. If false, the stdout stream.
311
342
  """
312
343
 
313
- def _parse_list(lst_str: str, item_type: str = "str", delim: str = " "):
344
+ def _parse_list(
345
+ lst_str: str, item_type: str = "str", delim: str = " "
346
+ ) -> list[Any]:
314
347
  return [parse_types[item_type](i) for i in lst_str.split(delim)]
315
348
 
316
- def _parse_array(arr_str: str, item_type: str = "float", delim: str = " "):
349
+ def _parse_array(
350
+ arr_str: str, item_type: str = "float", delim: str = " "
351
+ ) -> np.ndarray[Any, np.dtype[Any]]:
317
352
  return np.array(
318
353
  _parse_list(lst_str=arr_str, item_type=item_type, delim=delim)
319
354
  )
320
355
 
321
- def _parse_bool(bool_str):
356
+ def _parse_bool(bool_str: str) -> bool:
322
357
  bool_str = bool_str.lower()
323
358
  if bool_str in ("true", "1"):
324
359
  return True
@@ -331,7 +366,7 @@ class Command(JSONLike):
331
366
  f"{self.stderr if stderr else self.stdout!r}."
332
367
  )
333
368
 
334
- parse_types = {
369
+ parse_types: dict[str, Callable[[str], Any]] = {
335
370
  "str": str,
336
371
  "int": int,
337
372
  "float": float,
@@ -341,6 +376,7 @@ class Command(JSONLike):
341
376
  }
342
377
  types_pattern = "|".join(parse_types)
343
378
 
379
+ # TODO: use str.removeprefix in 3.9 onwards
344
380
  out_name = name.replace("outputs.", "")
345
381
  pattern = (
346
382
  r"(\<\<(?:({types_pattern})(?:\[(.*)\])?\()?parameter:{name}(?:\.(\w+)"
@@ -348,42 +384,39 @@ class Command(JSONLike):
348
384
  )
349
385
  pattern = pattern.format(types_pattern=types_pattern, name=out_name)
350
386
  spec = self.stderr if stderr else self.stdout
351
- self.app.submission_logger.info(
387
+ assert spec is not None
388
+ self._app.submission_logger.info(
352
389
  f"processing shell standard stream according to spec: {spec!r}"
353
390
  )
354
- param = self.app.Parameter(out_name)
355
- match = re.match(pattern, spec)
356
- try:
357
- groups = match.groups()
358
- except AttributeError:
391
+ param = self._app.Parameter(out_name)
392
+ if (match := re.match(pattern, spec)) is None:
359
393
  return value
360
- else:
361
- parse_type, parse_args_str = groups[1:3]
362
- parse_args = self._prepare_kwargs_from_string(
363
- args_str=parse_args_str,
394
+ groups = match.groups()
395
+ parse_type, parse_args_str = groups[1:3]
396
+ parse_args = self.__prepare_kwargs_from_string(
397
+ args_str=parse_args_str,
398
+ doubled_quoted_args=["delim"],
399
+ )
400
+ if param._value_class:
401
+ method, method_args_str = groups[3:5]
402
+ method_args = self.__prepare_kwargs_from_string(
403
+ args_str=method_args_str,
364
404
  doubled_quoted_args=["delim"],
365
405
  )
366
- if param._value_class:
367
- method, method_args_str = groups[3:5]
368
- method_args = self._prepare_kwargs_from_string(
369
- args_str=method_args_str,
370
- doubled_quoted_args=["delim"],
371
- )
372
- method = method or "CLI_parse"
373
- value = getattr(param._value_class, method)(value, **method_args)
374
- if parse_type:
375
- value = parse_types[parse_type](value, **parse_args)
406
+ method = method or "CLI_parse"
407
+ value = getattr(param._value_class, method)(value, **method_args)
408
+ if parse_type:
409
+ value = parse_types[parse_type](value, **parse_args)
376
410
 
377
411
  return value
378
412
 
379
- @staticmethod
380
- def _extract_executable_labels(cmd_str) -> List[str]:
381
- exe_regex = r"\<\<(?:executable):(.*?)\>\>"
382
- return re.findall(exe_regex, cmd_str)
413
+ __EXE_RE: ClassVar[Pattern] = re.compile(r"\<\<(?:executable):(.*?)\>\>")
414
+
415
+ @classmethod
416
+ def _extract_executable_labels(cls, cmd_str: str) -> Sequence[str]:
417
+ return cls.__EXE_RE.findall(cmd_str)
383
418
 
384
- def get_required_executables(self) -> List[str]:
419
+ def get_required_executables(self) -> Sequence[str]:
385
420
  """Return executable labels required by this command."""
386
421
  # an executable label might appear in the `command` or `executable` attribute:
387
- cmd_str = self._get_initial_command_line()
388
- exe_labels = self._extract_executable_labels(cmd_str)
389
- return exe_labels
422
+ return self._extract_executable_labels(self.__get_initial_command_line())