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
@@ -1,11 +1,16 @@
1
1
  """
2
2
  Helper for running a subprocess.
3
3
  """
4
-
4
+ from __future__ import annotations
5
5
  import subprocess
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Sequence
10
+ from logging import Logger
6
11
 
7
12
 
8
- def run_cmd(cmd, logger=None):
13
+ def run_cmd(cmd: str | Sequence[str], logger: Logger | None = None) -> tuple[str, str]:
9
14
  """Execute a command and return stdout, stderr as strings."""
10
15
  if logger:
11
16
  logger.debug(f"running shell command: {cmd}")
@@ -1,17 +1,17 @@
1
1
  """
2
2
  Adapters for various shells.
3
3
  """
4
+ from __future__ import annotations
4
5
  import os
5
- from typing import Dict, Optional
6
6
 
7
7
  from hpcflow.sdk.core.errors import UnsupportedShellError
8
8
 
9
- from .base import Shell
10
- from .bash import Bash, WSLBash
11
- from .powershell import WindowsPowerShell
9
+ from hpcflow.sdk.submission.shells.base import Shell
10
+ from hpcflow.sdk.submission.shells.bash import Bash, WSLBash
11
+ from hpcflow.sdk.submission.shells.powershell import WindowsPowerShell
12
12
 
13
13
  #: All supported shells.
14
- ALL_SHELLS = {
14
+ ALL_SHELLS: dict[str, dict[str, type[Shell]]] = {
15
15
  "bash": {"posix": Bash},
16
16
  "powershell": {"nt": WindowsPowerShell},
17
17
  "wsl+bash": {"nt": WSLBash},
@@ -25,28 +25,27 @@ DEFAULT_SHELL_NAMES = {
25
25
  }
26
26
 
27
27
 
28
- def get_supported_shells(os_name: Optional[str] = None) -> Dict[str, Shell]:
28
+ def get_supported_shells(os_name: str | None = None) -> dict[str, type[Shell]]:
29
29
  """
30
30
  Get shells supported on the current or given OS.
31
31
  """
32
- os_name = os_name or os.name
33
- return {k: v.get(os_name) for k, v in ALL_SHELLS.items() if v.get(os_name)}
32
+ os_name_ = os_name or os.name
33
+ return {k: v[os_name_] for k, v in ALL_SHELLS.items() if v.get(os_name_)}
34
34
 
35
35
 
36
- def get_shell(shell_name, os_name: Optional[str] = None, **kwargs) -> Shell:
36
+ def get_shell(shell_name: str | None, os_name: str | None = None, **kwargs) -> Shell:
37
37
  """
38
38
  Get a shell interface with the given name for a given OS (or the current one).
39
39
  """
40
40
  # TODO: apply config default shell args?
41
41
 
42
42
  os_name = os_name or os.name
43
- shell_name = shell_name.lower()
43
+ shell_name = (
44
+ DEFAULT_SHELL_NAMES[os_name] if shell_name is None else shell_name.lower()
45
+ )
44
46
 
45
47
  supported = get_supported_shells(os_name.lower())
46
- shell_cls = supported.get(shell_name)
47
- if not shell_cls:
48
+ if not (shell_cls := supported.get(shell_name)):
48
49
  raise UnsupportedShellError(shell=shell_name, supported=supported)
49
50
 
50
- shell_obj = shell_cls(**kwargs)
51
-
52
- return shell_obj
51
+ return shell_cls(**kwargs)
@@ -2,11 +2,19 @@
2
2
  Base model of a shell.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  from abc import ABC, abstractmethod
6
- from pathlib import Path
7
- from typing import Dict, List, Optional
7
+ from typing import TYPE_CHECKING
8
+ from hpcflow.sdk.typing import hydrate
8
9
 
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Mapping
12
+ from pathlib import Path
13
+ from typing import Any, ClassVar
14
+ from ..types import JobscriptHeaderArgs, VersionInfo
9
15
 
16
+
17
+ @hydrate
10
18
  class Shell(ABC):
11
19
  """Class to represent a shell and templates for jobscript composition.
12
20
 
@@ -22,68 +30,97 @@ class Shell(ABC):
22
30
  Arguments to pass to the shell.
23
31
  """
24
32
 
25
- def __init__(self, executable=None, os_args=None):
33
+ #: File extension for jobscripts.
34
+ JS_EXT: ClassVar[str]
35
+ #: Default for executable name.
36
+ DEFAULT_EXE: ClassVar[str]
37
+ #: Indent for environment setup.
38
+ JS_ENV_SETUP_INDENT: ClassVar[str]
39
+ #: Template for the jobscript shebang line.
40
+ JS_SHEBANG: ClassVar[str]
41
+ #: Template for the common part of the jobscript header.
42
+ JS_HEADER: ClassVar[str]
43
+ #: Template for the jobscript header when scheduled.
44
+ JS_SCHEDULER_HEADER: ClassVar[str]
45
+ #: Template for the jobscript header when directly executed.
46
+ JS_DIRECT_HEADER: ClassVar[str]
47
+ #: Template for the jobscript body.
48
+ JS_MAIN: ClassVar[str]
49
+ #: Template for the array handling code in a jobscript.
50
+ JS_ELEMENT_ARRAY: ClassVar[str]
51
+ #: Template for the element processing loop in a jobscript.
52
+ JS_ELEMENT_LOOP: ClassVar[str]
53
+ #: Basic indent.
54
+ JS_INDENT: ClassVar[str]
55
+ __slots__ = ("_executable", "os_args")
56
+
57
+ def __init__(
58
+ self, executable: str | None = None, os_args: dict[str, str] | None = None
59
+ ):
60
+ #: Which executable implements the shell.
26
61
  self._executable = executable or self.DEFAULT_EXE
27
- self.os_args = os_args
62
+ #: Arguments to pass to the shell.
63
+ self.os_args = os_args or {}
28
64
 
29
- def __eq__(self, other) -> bool:
65
+ def __eq__(self, other: Any) -> bool:
30
66
  if not isinstance(other, self.__class__):
31
67
  return False
32
- if self._executable == other._executable and self.os_args == other.os_args:
33
- return True
34
- return False
68
+ return self._executable == other._executable and self.os_args == other.os_args
35
69
 
36
70
  @property
37
- def executable(self) -> List[str]:
71
+ def executable(self) -> list[str]:
38
72
  """
39
73
  The executable to use plus any mandatory arguments.
40
74
  """
41
75
  return [self._executable]
42
76
 
43
77
  @property
44
- def shebang_executable(self) -> str:
78
+ def shebang_executable(self) -> list[str]:
45
79
  """
46
80
  The executable to use in a shebang line.
47
81
  """
48
82
  return self.executable
49
83
 
50
- def get_direct_submit_command(self, js_path) -> List[str]:
84
+ def get_direct_submit_command(self, js_path: str) -> list[str]:
51
85
  """Get the command for submitting a non-scheduled jobscript."""
52
86
  return self.executable + [js_path]
53
87
 
54
88
  @abstractmethod
55
- def get_version_info(self, exclude_os: Optional[bool] = False) -> Dict:
89
+ def get_version_info(self, exclude_os: bool = False) -> VersionInfo:
56
90
  """Get shell and operating system information."""
57
91
 
58
- def get_wait_command(self, workflow_app_alias: str, sub_idx: int, deps: Dict):
92
+ def get_wait_command(
93
+ self, workflow_app_alias: str, sub_idx: int, deps: Mapping[int, Any]
94
+ ):
59
95
  """
60
96
  Get the command to wait for a workflow.
61
97
  """
62
- if deps:
63
- return (
64
- f'{workflow_app_alias} workflow $WK_PATH_ARG wait --jobscripts "{sub_idx}:'
65
- + ",".join(str(i) for i in deps.keys())
66
- + '"'
67
- )
68
- else:
98
+ if not deps:
69
99
  return ""
100
+ return (
101
+ f"{workflow_app_alias} workflow $WK_PATH_ARG wait --jobscripts "
102
+ f'"{sub_idx}:{",".join(str(i) for i in deps)}"'
103
+ )
70
104
 
71
105
  @staticmethod
72
- def process_app_invoc_executable(app_invoc_exe):
106
+ def process_app_invoc_executable(app_invoc_exe: str) -> str:
73
107
  """
74
108
  Perform any post-processing of an application invocation command name.
75
109
  """
76
110
  return app_invoc_exe
77
111
 
78
- def process_JS_header_args(self, header_args: Dict) -> Dict:
112
+ def process_JS_header_args(
113
+ self, header_args: JobscriptHeaderArgs
114
+ ) -> JobscriptHeaderArgs:
79
115
  """
80
116
  Process the application invocation key in the jobscript header arguments.
81
117
  """
82
- app_invoc = self.process_app_invoc_executable(header_args["app_invoc"][0])
83
- if len(header_args["app_invoc"]) > 1:
84
- app_invoc += ' "' + header_args["app_invoc"][1] + '"'
85
-
86
- header_args["app_invoc"] = app_invoc
118
+ app_invoc_ = header_args["app_invoc"]
119
+ if not isinstance(app_invoc_, str):
120
+ app_invoc = self.process_app_invoc_executable(app_invoc_[0])
121
+ for item in app_invoc_[1:]:
122
+ app_invoc += f' "{item}"'
123
+ header_args["app_invoc"] = app_invoc
87
124
  return header_args
88
125
 
89
126
  def prepare_JS_path(self, js_path: Path) -> str:
@@ -92,8 +129,44 @@ class Shell(ABC):
92
129
  """
93
130
  return str(js_path)
94
131
 
95
- def prepare_element_run_dirs(self, run_dirs: List[List[Path]]) -> List[List[str]]:
132
+ def prepare_element_run_dirs(self, run_dirs: list[list[Path]]) -> list[list[str]]:
96
133
  """
97
134
  Prepare the element run directory names for use.
98
135
  """
99
- return [[str(j) for j in i] for i in run_dirs]
136
+ return [[str(path) for path in i] for i in run_dirs]
137
+
138
+ @abstractmethod
139
+ def format_save_parameter(
140
+ self,
141
+ workflow_app_alias: str,
142
+ param_name: str,
143
+ shell_var_name: str,
144
+ EAR_ID: int,
145
+ cmd_idx: int,
146
+ stderr: bool,
147
+ ):
148
+ """
149
+ Format instructions to save a parameter.
150
+ """
151
+
152
+ @abstractmethod
153
+ def wrap_in_subshell(self, commands: str, abortable: bool) -> str:
154
+ """
155
+ Format commands to run within a child scope.
156
+
157
+ This assumes `commands` ends in a newline.
158
+ """
159
+
160
+ @abstractmethod
161
+ def format_loop_check(
162
+ self, workflow_app_alias: str, loop_name: str, run_ID: int
163
+ ) -> str:
164
+ """
165
+ Format a loop check.
166
+ """
167
+
168
+ @abstractmethod
169
+ def format_stream_assignment(self, shell_var_name: str, command: str) -> str:
170
+ """
171
+ Format a stream assignment.
172
+ """
@@ -1,37 +1,46 @@
1
1
  """
2
- Shell models based on the Bourne-Again Shell.
2
+ Shell models based on the GNU Bourne-Again Shell.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  from pathlib import Path
6
7
  import subprocess
7
8
  from textwrap import dedent, indent
8
- from typing import Dict, List, Optional, Union
9
+ from typing import TYPE_CHECKING
10
+ from typing_extensions import override
11
+ from hpcflow.sdk.typing import hydrate
9
12
  from hpcflow.sdk.core import ABORT_EXIT_CODE
10
- from hpcflow.sdk.submission.shells import Shell
13
+ from hpcflow.sdk.submission.shells.base import Shell
11
14
  from hpcflow.sdk.submission.shells.os_version import (
12
15
  get_OS_info_POSIX,
13
16
  get_OS_info_windows,
14
17
  )
15
18
 
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Mapping
21
+ from typing import Any, ClassVar
22
+ from .base import VersionInfo, JobscriptHeaderArgs
16
23
 
24
+
25
+ @hydrate
17
26
  class Bash(Shell):
18
27
  """
19
28
  Class to represent using bash on a POSIX OS to generate and submit a jobscript.
20
29
  """
21
30
 
22
31
  #: Default for executable name.
23
- DEFAULT_EXE = "/bin/bash"
32
+ DEFAULT_EXE: ClassVar[str] = "/bin/bash"
24
33
 
25
34
  #: File extension for jobscripts.
26
- JS_EXT = ".sh"
35
+ JS_EXT: ClassVar[str] = ".sh"
27
36
  #: Basic indent.
28
- JS_INDENT = " "
37
+ JS_INDENT: ClassVar[str] = " "
29
38
  #: Indent for environment setup.
30
- JS_ENV_SETUP_INDENT = 2 * JS_INDENT
39
+ JS_ENV_SETUP_INDENT: ClassVar[str] = 2 * JS_INDENT
31
40
  #: Template for the jobscript shebang line.
32
- JS_SHEBANG = """#!{shebang_executable} {shebang_args}"""
41
+ JS_SHEBANG: ClassVar[str] = """#!{shebang_executable} {shebang_args}"""
33
42
  #: Template for the common part of the jobscript header.
34
- JS_HEADER = dedent(
43
+ JS_HEADER: ClassVar[str] = dedent(
35
44
  """\
36
45
  {workflow_app_alias} () {{
37
46
  (
@@ -52,7 +61,7 @@ class Bash(Shell):
52
61
  """
53
62
  )
54
63
  #: Template for the jobscript header when scheduled.
55
- JS_SCHEDULER_HEADER = dedent(
64
+ JS_SCHEDULER_HEADER: ClassVar[str] = dedent(
56
65
  """\
57
66
  {shebang}
58
67
 
@@ -61,7 +70,7 @@ class Bash(Shell):
61
70
  """
62
71
  )
63
72
  #: Template for the jobscript header when directly executed.
64
- JS_DIRECT_HEADER = dedent(
73
+ JS_DIRECT_HEADER: ClassVar[str] = dedent(
65
74
  """\
66
75
  {shebang}
67
76
 
@@ -70,7 +79,7 @@ class Bash(Shell):
70
79
  """
71
80
  )
72
81
  #: Template for the jobscript body.
73
- JS_MAIN = dedent(
82
+ JS_MAIN: ClassVar[str] = dedent(
74
83
  """\
75
84
  elem_EAR_IDs=`sed "$((${{JS_elem_idx}} + 1))q;d" "$EAR_ID_FILE"`
76
85
  elem_run_dirs=`sed "$((${{JS_elem_idx}} + 1))q;d" "$ELEM_RUN_DIR_FILE"`
@@ -119,7 +128,7 @@ class Bash(Shell):
119
128
  """
120
129
  )
121
130
  #: Template for the element processing loop in a jobscript.
122
- JS_ELEMENT_LOOP = dedent(
131
+ JS_ELEMENT_LOOP: ClassVar[str] = dedent(
123
132
  """\
124
133
  for ((JS_elem_idx=0;JS_elem_idx<{num_elements};JS_elem_idx++))
125
134
  do
@@ -129,7 +138,7 @@ class Bash(Shell):
129
138
  """
130
139
  )
131
140
  #: Template for the array handling code in a jobscript.
132
- JS_ELEMENT_ARRAY = dedent(
141
+ JS_ELEMENT_ARRAY: ClassVar[str] = dedent(
133
142
  """\
134
143
  JS_elem_idx=$(({scheduler_array_item_var} - 1))
135
144
  {main}
@@ -137,20 +146,18 @@ class Bash(Shell):
137
146
  """
138
147
  )
139
148
 
140
- def __init__(self, *args, **kwargs):
141
- super().__init__(*args, **kwargs)
142
-
143
149
  @property
144
- def linux_release_file(self):
150
+ def linux_release_file(self) -> str:
145
151
  """
146
152
  The name of the file describing the Linux version.
147
153
  """
148
154
  return self.os_args["linux_release_file"]
149
155
 
150
- def _get_OS_info_POSIX(self):
156
+ def _get_OS_info_POSIX(self) -> Mapping[str, str]:
151
157
  return get_OS_info_POSIX(linux_release_file=self.linux_release_file)
152
158
 
153
- def get_version_info(self, exclude_os: Optional[bool] = False) -> Dict:
159
+ @override
160
+ def get_version_info(self, exclude_os: bool = False) -> VersionInfo:
154
161
  """Get bash version information.
155
162
 
156
163
  Parameters
@@ -171,29 +178,27 @@ class Bash(Shell):
171
178
  else:
172
179
  raise RuntimeError("Failed to parse bash version information.")
173
180
 
174
- out = {
181
+ return {
175
182
  "shell_name": "bash",
176
183
  "shell_executable": self.executable,
177
184
  "shell_version": bash_version,
185
+ **({} if exclude_os else self._get_OS_info_POSIX()),
178
186
  }
179
187
 
180
- if not exclude_os:
181
- out.update(**self._get_OS_info_POSIX())
182
-
183
- return out
184
-
185
188
  @staticmethod
186
- def process_app_invoc_executable(app_invoc_exe):
189
+ def process_app_invoc_executable(app_invoc_exe: str) -> str:
187
190
  # escape spaces with a back slash:
188
- app_invoc_exe = app_invoc_exe.replace(" ", r"\ ")
189
- return app_invoc_exe
191
+ return app_invoc_exe.replace(" ", r"\ ")
190
192
 
191
- def format_stream_assignment(self, shell_var_name, command):
193
+ @override
194
+ @staticmethod
195
+ def format_stream_assignment(shell_var_name: str, command: str) -> str:
192
196
  """
193
197
  Produce code to assign the output of the command to a shell variable.
194
198
  """
195
199
  return f"{shell_var_name}=`{command}`"
196
200
 
201
+ @override
197
202
  def format_save_parameter(
198
203
  self,
199
204
  workflow_app_alias: str,
@@ -217,7 +222,10 @@ class Bash(Shell):
217
222
  f"\n"
218
223
  )
219
224
 
220
- def format_loop_check(self, workflow_app_alias: str, loop_name: str, run_ID: int):
225
+ @override
226
+ def format_loop_check(
227
+ self, workflow_app_alias: str, loop_name: str, run_ID: int
228
+ ) -> str:
221
229
  """
222
230
  Produce code to check the looping status of part of a workflow.
223
231
  """
@@ -229,6 +237,7 @@ class Bash(Shell):
229
237
  f"\n"
230
238
  )
231
239
 
240
+ @override
232
241
  def wrap_in_subshell(self, commands: str, abortable: bool) -> str:
233
242
  """Format commands to run within a subshell.
234
243
 
@@ -276,16 +285,17 @@ class Bash(Shell):
276
285
  ).format(commands=commands)
277
286
 
278
287
 
288
+ @hydrate
279
289
  class WSLBash(Bash):
280
290
  """
281
291
  A variant of bash that handles running under WSL on Windows.
282
292
  """
283
293
 
284
294
  #: Default name of the WSL interface executable.
285
- DEFAULT_WSL_EXE = "wsl"
295
+ DEFAULT_WSL_EXE: ClassVar[str] = "wsl"
286
296
 
287
297
  #: Template for the common part of the jobscript header.
288
- JS_HEADER = Bash.JS_HEADER.replace(
298
+ JS_HEADER: ClassVar[str] = Bash.JS_HEADER.replace(
289
299
  'WK_PATH_ARG="$WK_PATH"',
290
300
  'WK_PATH_ARG=`wslpath -m "$WK_PATH"`',
291
301
  ).replace(
@@ -295,25 +305,28 @@ class WSLBash(Bash):
295
305
 
296
306
  def __init__(
297
307
  self,
298
- WSL_executable: Optional[str] = None,
299
- WSL_distribution: Optional[str] = None,
300
- WSL_user: Optional[str] = None,
308
+ WSL_executable: str | None = None,
309
+ WSL_distribution: str | None = None,
310
+ WSL_user: str | None = None,
301
311
  *args,
302
312
  **kwargs,
303
313
  ):
314
+ #: The WSL executable wrapper.
304
315
  self.WSL_executable = WSL_executable or self.DEFAULT_WSL_EXE
316
+ #: The WSL distribution to use, if any.
305
317
  self.WSL_distribution = WSL_distribution
318
+ #: The WSL user to use, if any.
306
319
  self.WSL_user = WSL_user
307
320
  super().__init__(*args, **kwargs)
308
321
 
309
- def __eq__(self, other) -> bool:
322
+ def __eq__(self, other: Any) -> bool:
310
323
  return super().__eq__(other) and (
311
324
  self.WSL_executable == other.WSL_executable
312
325
  and self.WSL_distribution == other.WSL_distribution
313
326
  and self.WSL_user == other.WSL_user
314
327
  )
315
328
 
316
- def _get_WSL_command(self):
329
+ def _get_WSL_command(self) -> list[str]:
317
330
  out = [self.WSL_executable]
318
331
  if self.WSL_distribution:
319
332
  out += ["--distribution", self.WSL_distribution]
@@ -322,14 +335,14 @@ class WSLBash(Bash):
322
335
  return out
323
336
 
324
337
  @property
325
- def executable(self) -> List[str]:
338
+ def executable(self) -> list[str]:
326
339
  return self._get_WSL_command() + super().executable
327
340
 
328
341
  @property
329
- def shebang_executable(self) -> List[str]:
342
+ def shebang_executable(self) -> list[str]:
330
343
  return super().executable
331
344
 
332
- def _get_OS_info_POSIX(self):
345
+ def _get_OS_info_POSIX(self) -> Mapping[str, str]:
333
346
  return get_OS_info_POSIX(
334
347
  WSL_executable=self._get_WSL_command(),
335
348
  use_py=False,
@@ -337,27 +350,29 @@ class WSLBash(Bash):
337
350
  )
338
351
 
339
352
  @staticmethod
340
- def _convert_to_wsl_path(win_path: Union[str, Path]) -> str:
353
+ def _convert_to_wsl_path(win_path: str | Path) -> str:
341
354
  win_path = Path(win_path)
342
355
  parts = list(win_path.parts)
343
356
  parts[0] = f"/mnt/{win_path.drive.lower().rstrip(':')}"
344
- wsl_path = "/".join(parts)
345
- return wsl_path
357
+ return "/".join(parts)
346
358
 
347
- def process_JS_header_args(self, header_args):
359
+ def process_JS_header_args(
360
+ self, header_args: JobscriptHeaderArgs
361
+ ) -> JobscriptHeaderArgs:
348
362
  # convert executable windows paths to posix style as expected by WSL:
349
- header_args["app_invoc"][0] = self._convert_to_wsl_path(
350
- header_args["app_invoc"][0]
351
- )
363
+ ai = header_args["app_invoc"]
364
+ if isinstance(ai, list):
365
+ ai[0] = self._convert_to_wsl_path(ai[0])
352
366
  return super().process_JS_header_args(header_args)
353
367
 
354
368
  def prepare_JS_path(self, js_path: Path) -> str:
355
369
  return self._convert_to_wsl_path(js_path)
356
370
 
357
- def prepare_element_run_dirs(self, run_dirs: List[List[Path]]) -> List[List[str]]:
358
- return [["/".join(str(j).split("\\")) for j in i] for i in run_dirs]
371
+ def prepare_element_run_dirs(self, run_dirs: list[list[Path]]) -> list[list[str]]:
372
+ return [[str(path).replace("\\", "/") for path in i] for i in run_dirs]
359
373
 
360
- def get_version_info(self, exclude_os: Optional[bool] = False) -> Dict:
374
+ @override
375
+ def get_version_info(self, exclude_os: bool = False) -> VersionInfo:
361
376
  """Get WSL and bash version information.
362
377
 
363
378
  Parameters
@@ -368,12 +383,14 @@ class WSLBash(Bash):
368
383
  """
369
384
  vers_info = super().get_version_info(exclude_os=exclude_os)
370
385
 
371
- vers_info["shell_name"] = ("wsl+" + vers_info["shell_name"]).lower()
386
+ vers_info["shell_name"] = f"wsl+{vers_info['shell_name']}".lower()
372
387
  vers_info["WSL_executable"] = self.WSL_executable
373
- vers_info["WSL_distribution"] = self.WSL_distribution
374
- vers_info["WSL_user"] = self.WSL_user
388
+ if self.WSL_distribution:
389
+ vers_info["WSL_distribution"] = self.WSL_distribution
390
+ if self.WSL_user:
391
+ vers_info["WSL_user"] = self.WSL_user
375
392
 
376
- for key in list(vers_info.keys()):
393
+ for key in tuple(vers_info):
377
394
  if key.startswith("OS_"):
378
395
  vers_info[f"WSL_{key}"] = vers_info.pop(key)
379
396