hpcflow-new2 0.2.0a188__py3-none-any.whl → 0.2.0a190__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  5. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  6. hpcflow/sdk/__init__.py +21 -15
  7. hpcflow/sdk/app.py +2133 -770
  8. hpcflow/sdk/cli.py +281 -250
  9. hpcflow/sdk/cli_common.py +6 -2
  10. hpcflow/sdk/config/__init__.py +1 -1
  11. hpcflow/sdk/config/callbacks.py +77 -42
  12. hpcflow/sdk/config/cli.py +126 -103
  13. hpcflow/sdk/config/config.py +578 -311
  14. hpcflow/sdk/config/config_file.py +131 -95
  15. hpcflow/sdk/config/errors.py +112 -85
  16. hpcflow/sdk/config/types.py +145 -0
  17. hpcflow/sdk/core/actions.py +1054 -994
  18. hpcflow/sdk/core/app_aware.py +24 -0
  19. hpcflow/sdk/core/cache.py +81 -63
  20. hpcflow/sdk/core/command_files.py +275 -185
  21. hpcflow/sdk/core/commands.py +111 -107
  22. hpcflow/sdk/core/element.py +724 -503
  23. hpcflow/sdk/core/enums.py +192 -0
  24. hpcflow/sdk/core/environment.py +74 -93
  25. hpcflow/sdk/core/errors.py +398 -51
  26. hpcflow/sdk/core/json_like.py +540 -272
  27. hpcflow/sdk/core/loop.py +380 -334
  28. hpcflow/sdk/core/loop_cache.py +160 -43
  29. hpcflow/sdk/core/object_list.py +370 -207
  30. hpcflow/sdk/core/parameters.py +728 -600
  31. hpcflow/sdk/core/rule.py +59 -41
  32. hpcflow/sdk/core/run_dir_files.py +33 -22
  33. hpcflow/sdk/core/task.py +1546 -1325
  34. hpcflow/sdk/core/task_schema.py +240 -196
  35. hpcflow/sdk/core/test_utils.py +126 -88
  36. hpcflow/sdk/core/types.py +387 -0
  37. hpcflow/sdk/core/utils.py +410 -305
  38. hpcflow/sdk/core/validation.py +82 -9
  39. hpcflow/sdk/core/workflow.py +1192 -1028
  40. hpcflow/sdk/core/zarr_io.py +98 -137
  41. hpcflow/sdk/demo/cli.py +46 -33
  42. hpcflow/sdk/helper/cli.py +18 -16
  43. hpcflow/sdk/helper/helper.py +75 -63
  44. hpcflow/sdk/helper/watcher.py +61 -28
  45. hpcflow/sdk/log.py +83 -59
  46. hpcflow/sdk/persistence/__init__.py +8 -31
  47. hpcflow/sdk/persistence/base.py +988 -586
  48. hpcflow/sdk/persistence/defaults.py +6 -0
  49. hpcflow/sdk/persistence/discovery.py +38 -0
  50. hpcflow/sdk/persistence/json.py +408 -153
  51. hpcflow/sdk/persistence/pending.py +158 -123
  52. hpcflow/sdk/persistence/store_resource.py +37 -22
  53. hpcflow/sdk/persistence/types.py +307 -0
  54. hpcflow/sdk/persistence/utils.py +14 -11
  55. hpcflow/sdk/persistence/zarr.py +477 -420
  56. hpcflow/sdk/runtime.py +44 -41
  57. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  58. hpcflow/sdk/submission/jobscript.py +444 -404
  59. hpcflow/sdk/submission/schedulers/__init__.py +133 -40
  60. hpcflow/sdk/submission/schedulers/direct.py +97 -71
  61. hpcflow/sdk/submission/schedulers/sge.py +132 -126
  62. hpcflow/sdk/submission/schedulers/slurm.py +263 -268
  63. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  64. hpcflow/sdk/submission/shells/__init__.py +14 -15
  65. hpcflow/sdk/submission/shells/base.py +102 -29
  66. hpcflow/sdk/submission/shells/bash.py +72 -55
  67. hpcflow/sdk/submission/shells/os_version.py +31 -30
  68. hpcflow/sdk/submission/shells/powershell.py +37 -29
  69. hpcflow/sdk/submission/submission.py +203 -257
  70. hpcflow/sdk/submission/types.py +143 -0
  71. hpcflow/sdk/typing.py +163 -12
  72. hpcflow/tests/conftest.py +8 -6
  73. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  74. hpcflow/tests/scripts/test_main_scripts.py +60 -30
  75. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
  76. hpcflow/tests/unit/test_action.py +86 -75
  77. hpcflow/tests/unit/test_action_rule.py +9 -4
  78. hpcflow/tests/unit/test_app.py +13 -6
  79. hpcflow/tests/unit/test_cli.py +1 -1
  80. hpcflow/tests/unit/test_command.py +71 -54
  81. hpcflow/tests/unit/test_config.py +20 -15
  82. hpcflow/tests/unit/test_config_file.py +21 -18
  83. hpcflow/tests/unit/test_element.py +58 -62
  84. hpcflow/tests/unit/test_element_iteration.py +3 -1
  85. hpcflow/tests/unit/test_element_set.py +29 -19
  86. hpcflow/tests/unit/test_group.py +4 -2
  87. hpcflow/tests/unit/test_input_source.py +116 -93
  88. hpcflow/tests/unit/test_input_value.py +29 -24
  89. hpcflow/tests/unit/test_json_like.py +44 -35
  90. hpcflow/tests/unit/test_loop.py +65 -58
  91. hpcflow/tests/unit/test_object_list.py +17 -12
  92. hpcflow/tests/unit/test_parameter.py +16 -7
  93. hpcflow/tests/unit/test_persistence.py +48 -35
  94. hpcflow/tests/unit/test_resources.py +20 -18
  95. hpcflow/tests/unit/test_run.py +8 -3
  96. hpcflow/tests/unit/test_runtime.py +2 -1
  97. hpcflow/tests/unit/test_schema_input.py +23 -15
  98. hpcflow/tests/unit/test_shell.py +3 -2
  99. hpcflow/tests/unit/test_slurm.py +8 -7
  100. hpcflow/tests/unit/test_submission.py +39 -19
  101. hpcflow/tests/unit/test_task.py +352 -247
  102. hpcflow/tests/unit/test_task_schema.py +33 -20
  103. hpcflow/tests/unit/test_utils.py +9 -11
  104. hpcflow/tests/unit/test_value_sequence.py +15 -12
  105. hpcflow/tests/unit/test_workflow.py +114 -83
  106. hpcflow/tests/unit/test_workflow_template.py +0 -1
  107. hpcflow/tests/workflows/test_jobscript.py +2 -1
  108. hpcflow/tests/workflows/test_workflows.py +18 -13
  109. {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
  110. hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
  111. hpcflow/sdk/core/parallel.py +0 -21
  112. hpcflow_new2-0.2.0a188.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -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
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,22 @@ 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)
76
83
 
77
84
  def __repr__(self) -> str:
78
85
  out = []
@@ -95,61 +102,63 @@ class Command(JSONLike):
95
102
 
96
103
  return f"{self.__class__.__name__}({', '.join(out)})"
97
104
 
98
- def _get_initial_command_line(self) -> str:
105
+ def __get_initial_command_line(self) -> str:
99
106
  if self.command:
100
107
  return self.command
101
108
  else:
102
109
  return self.executable or ""
103
110
 
104
- def get_command_line(self, EAR, shell, env) -> Tuple[str, List[Tuple[str, str]]]:
111
+ __EXE_SCRIPT_RE: ClassVar[Pattern] = re.compile(r"\<\<(executable|script):(.*?)\>\>")
112
+ __ENV_SPEC_RE: ClassVar[Pattern] = re.compile(r"\<\<env:(.*?)\>\>")
113
+
114
+ def get_command_line(
115
+ self, EAR: ElementActionRun, shell: Shell, env: Environment
116
+ ) -> tuple[str, list[tuple[str, ...]]]:
105
117
  """Return the resolved command line.
106
118
 
107
119
  This is ordinarily called at run-time by `Workflow.write_commands`.
108
-
109
120
  """
110
121
 
111
- self.app.persistence_logger.debug("Command.get_command_line")
112
- cmd_str = self._get_initial_command_line()
122
+ self._app.persistence_logger.debug("Command.get_command_line")
123
+ cmd_str = self.__get_initial_command_line()
113
124
 
114
125
  def _format_sum(iterable: Iterable) -> str:
115
126
  return str(sum(iterable))
116
127
 
117
128
  def _join(iterable: Iterable, delim: str) -> str:
118
- return delim.join(str(i) for i in iterable)
129
+ return delim.join(map(str, iterable))
119
130
 
120
- parse_types = {
131
+ parse_types: dict[str, Callable[..., str]] = {
121
132
  "sum": _format_sum,
122
133
  "join": _join,
123
134
  }
124
135
 
125
- def exec_script_repl(match_obj):
136
+ def exec_script_repl(match_obj: re.Match[str]) -> str:
126
137
  typ, val = match_obj.groups()
127
138
  if typ == "executable":
128
139
  executable = env.executables.get(val)
129
140
  filterable = ElementResources.get_env_instance_filterable_attributes()
130
- filter_exec = {j: EAR.get_resources().get(j) for j in filterable}
141
+ filter_exec = {attr: EAR.get_resources().get(attr) for attr in filterable}
131
142
  exec_cmd = executable.filter_instances(**filter_exec)[0].command
132
- out = exec_cmd.replace("<<num_cores>>", str(EAR.resources.num_cores))
143
+ return exec_cmd.replace("<<num_cores>>", str(EAR.resources.num_cores))
133
144
  elif typ == "script":
134
- out = EAR.action.get_script_name(val)
135
- return out
145
+ return EAR.action.get_script_name(val)
146
+ else:
147
+ raise ValueError("impossible match occurred")
136
148
 
137
- def input_param_repl(match_obj, inp_val):
149
+ def input_param_repl(match_obj: re.Match[str], inp_val) -> str:
138
150
  _, func, func_kwargs, method, method_kwargs = match_obj.groups()
139
151
 
140
152
  if isinstance(inp_val, ParameterValue):
141
153
  if not method:
142
154
  method = "CLI_format"
143
155
  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)
156
+ raise NoCLIFormatMethodError(method, inp_val)
157
+ kwargs = self.__prepare_kwargs_from_string(args_str=method_kwargs)
149
158
  inp_val = getattr(inp_val, method)(**kwargs)
150
159
 
151
160
  if func:
152
- kwargs = self._prepare_kwargs_from_string(
161
+ kwargs = self.__prepare_kwargs_from_string(
153
162
  args_str=func_kwargs,
154
163
  doubled_quoted_args=["delim"],
155
164
  )
@@ -158,12 +167,9 @@ class Command(JSONLike):
158
167
  return str(inp_val)
159
168
 
160
169
  file_regex = r"(\<\<file:{}\>\>?)"
161
- exe_script_regex = r"\<\<(executable|script):(.*?)\>\>"
162
- env_specs_regex = r"\<\<env:(.*?)\>\>"
163
170
 
164
171
  # substitute executables:
165
- cmd_str = re.sub(
166
- pattern=exe_script_regex,
172
+ cmd_str = self.__EXE_SCRIPT_RE.sub(
167
173
  repl=exec_script_repl,
168
174
  string=cmd_str,
169
175
  )
@@ -172,14 +178,13 @@ class Command(JSONLike):
172
178
  # an `<<args>>` variable::
173
179
  for var_key, var_val in (self.variables or {}).items():
174
180
  # 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)],
181
+ var_val = self.__ENV_SPEC_RE.sub(
182
+ repl=lambda match_obj: EAR.env_spec[match_obj[1]],
178
183
  string=var_val,
179
184
  )
180
185
  cmd_str = cmd_str.replace(f"<<{var_key}>>", var_val)
181
186
  if "<<args>>" in cmd_str:
182
- args_str = " ".join(self.arguments or [])
187
+ args_str = " ".join(self.arguments or ())
183
188
  ends_in_args = cmd_str.endswith("<<args>>")
184
189
  cmd_str = cmd_str.replace("<<args>>", args_str)
185
190
  if ends_in_args and not args_str:
@@ -226,7 +231,7 @@ class Command(JSONLike):
226
231
  string=cmd_str,
227
232
  )
228
233
 
229
- shell_vars = []
234
+ shell_vars: list[tuple[str, ...]] = []
230
235
  out_types = self.get_output_types()
231
236
  if out_types["stdout"]:
232
237
  # TODO: also map stderr/both if possible
@@ -246,56 +251,53 @@ class Command(JSONLike):
246
251
 
247
252
  return cmd_str, shell_vars
248
253
 
249
- def get_output_types(self):
254
+ # note: we use "parameter" rather than "output", because it could be a schema
255
+ # output or schema input.
256
+ __PARAM_RE: ClassVar[Pattern] = re.compile(
257
+ r"(?:\<\<(?:\w+(?:\[(?:.*)\])?\()?parameter:(\w+)"
258
+ r"(?:\.(?:\w+)\((?:.*?)\))?\)?\>\>?)"
259
+ )
260
+
261
+ def get_output_types(self) -> Mapping[str, str | None]:
250
262
  """
251
263
  Get whether stdout and stderr are workflow parameters.
252
264
  """
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}
265
+ out: dict[str, str | None] = {"stdout": None, "stderr": None}
260
266
  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
267
+ if i and (match := self.__PARAM_RE.search(i)):
268
+ param_typ: str = match[1]
269
+ if match.span(0) != (0, len(i)):
270
+ raise ValueError(
271
+ f"If specified as a parameter, `{label}` must not include"
272
+ f" any characters other than the parameter "
273
+ f"specification, but this was given: {i!r}."
274
+ )
275
+ out[label] = param_typ
272
276
  return out
273
277
 
274
278
  @staticmethod
275
- def _prepare_kwargs_from_string(args_str: Union[str, None], doubled_quoted_args=None):
276
- kwargs = {}
279
+ def __prepare_kwargs_from_string(
280
+ args_str: str | None, doubled_quoted_args: list[str] | None = None
281
+ ) -> dict[str, str]:
277
282
  if args_str is None:
278
- return kwargs
283
+ return {}
279
284
 
285
+ kwargs: dict[str, str] = {}
280
286
  # deal with specified double-quoted arguments first if it exists:
281
- for quote_arg in doubled_quoted_args or []:
287
+ for quote_arg in doubled_quoted_args or ():
282
288
  quote_pat = r'.*({quote_arg}="(.*)").*'.format(quote_arg=quote_arg)
283
- match = re.match(quote_pat, args_str)
284
- if match:
289
+ if match := re.match(quote_pat, args_str):
285
290
  quote_str, quote_contents = match.groups()
286
291
  args_str = args_str.replace(quote_str, "")
287
292
  kwargs[quote_arg] = quote_contents
288
293
 
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
294
+ if args_str := args_str.strip().strip(","):
295
+ for arg_part in args_str.split(","):
296
+ name_i, value_i = map(str.strip, arg_part.split("="))
297
+ kwargs[name_i] = value_i
296
298
  return kwargs
297
299
 
298
- def process_std_stream(self, name: str, value: str, stderr: bool):
300
+ def process_std_stream(self, name: str, value: str, stderr: bool) -> Any:
299
301
  """
300
302
  Process a description of a standard stread from a command to get how it becomes
301
303
  a workflow parameter for later actions.
@@ -310,15 +312,19 @@ class Command(JSONLike):
310
312
  If true, this is handling the stderr stream. If false, the stdout stream.
311
313
  """
312
314
 
313
- def _parse_list(lst_str: str, item_type: str = "str", delim: str = " "):
315
+ def _parse_list(
316
+ lst_str: str, item_type: str = "str", delim: str = " "
317
+ ) -> list[Any]:
314
318
  return [parse_types[item_type](i) for i in lst_str.split(delim)]
315
319
 
316
- def _parse_array(arr_str: str, item_type: str = "float", delim: str = " "):
320
+ def _parse_array(
321
+ arr_str: str, item_type: str = "float", delim: str = " "
322
+ ) -> np.ndarray[Any, np.dtype[Any]]:
317
323
  return np.array(
318
324
  _parse_list(lst_str=arr_str, item_type=item_type, delim=delim)
319
325
  )
320
326
 
321
- def _parse_bool(bool_str):
327
+ def _parse_bool(bool_str: str) -> bool:
322
328
  bool_str = bool_str.lower()
323
329
  if bool_str in ("true", "1"):
324
330
  return True
@@ -331,7 +337,7 @@ class Command(JSONLike):
331
337
  f"{self.stderr if stderr else self.stdout!r}."
332
338
  )
333
339
 
334
- parse_types = {
340
+ parse_types: dict[str, Callable[[str], Any]] = {
335
341
  "str": str,
336
342
  "int": int,
337
343
  "float": float,
@@ -341,6 +347,7 @@ class Command(JSONLike):
341
347
  }
342
348
  types_pattern = "|".join(parse_types)
343
349
 
350
+ # TODO: use str.removeprefix in 3.9 onwards
344
351
  out_name = name.replace("outputs.", "")
345
352
  pattern = (
346
353
  r"(\<\<(?:({types_pattern})(?:\[(.*)\])?\()?parameter:{name}(?:\.(\w+)"
@@ -348,42 +355,39 @@ class Command(JSONLike):
348
355
  )
349
356
  pattern = pattern.format(types_pattern=types_pattern, name=out_name)
350
357
  spec = self.stderr if stderr else self.stdout
351
- self.app.submission_logger.info(
358
+ assert spec is not None
359
+ self._app.submission_logger.info(
352
360
  f"processing shell standard stream according to spec: {spec!r}"
353
361
  )
354
- param = self.app.Parameter(out_name)
355
- match = re.match(pattern, spec)
356
- try:
357
- groups = match.groups()
358
- except AttributeError:
362
+ param = self._app.Parameter(out_name)
363
+ if (match := re.match(pattern, spec)) is None:
359
364
  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,
365
+ groups = match.groups()
366
+ parse_type, parse_args_str = groups[1:3]
367
+ parse_args = self.__prepare_kwargs_from_string(
368
+ args_str=parse_args_str,
369
+ doubled_quoted_args=["delim"],
370
+ )
371
+ if param._value_class:
372
+ method, method_args_str = groups[3:5]
373
+ method_args = self.__prepare_kwargs_from_string(
374
+ args_str=method_args_str,
364
375
  doubled_quoted_args=["delim"],
365
376
  )
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)
377
+ method = method or "CLI_parse"
378
+ value = getattr(param._value_class, method)(value, **method_args)
379
+ if parse_type:
380
+ value = parse_types[parse_type](value, **parse_args)
376
381
 
377
382
  return value
378
383
 
379
- @staticmethod
380
- def _extract_executable_labels(cmd_str) -> List[str]:
381
- exe_regex = r"\<\<(?:executable):(.*?)\>\>"
382
- return re.findall(exe_regex, cmd_str)
384
+ __EXE_RE: ClassVar[Pattern] = re.compile(r"\<\<(?:executable):(.*?)\>\>")
385
+
386
+ @classmethod
387
+ def _extract_executable_labels(cls, cmd_str: str) -> Sequence[str]:
388
+ return cls.__EXE_RE.findall(cmd_str)
383
389
 
384
- def get_required_executables(self) -> List[str]:
390
+ def get_required_executables(self) -> Sequence[str]:
385
391
  """Return executable labels required by this command."""
386
392
  # 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
393
+ return self._extract_executable_labels(self.__get_initial_command_line())