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.
- hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
- hpcflow/_version.py +1 -1
- hpcflow/app.py +1 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
- hpcflow/sdk/__init__.py +21 -15
- hpcflow/sdk/app.py +2133 -770
- hpcflow/sdk/cli.py +281 -250
- hpcflow/sdk/cli_common.py +6 -2
- hpcflow/sdk/config/__init__.py +1 -1
- hpcflow/sdk/config/callbacks.py +77 -42
- hpcflow/sdk/config/cli.py +126 -103
- hpcflow/sdk/config/config.py +578 -311
- hpcflow/sdk/config/config_file.py +131 -95
- hpcflow/sdk/config/errors.py +112 -85
- hpcflow/sdk/config/types.py +145 -0
- hpcflow/sdk/core/actions.py +1054 -994
- hpcflow/sdk/core/app_aware.py +24 -0
- hpcflow/sdk/core/cache.py +81 -63
- hpcflow/sdk/core/command_files.py +275 -185
- hpcflow/sdk/core/commands.py +111 -107
- hpcflow/sdk/core/element.py +724 -503
- hpcflow/sdk/core/enums.py +192 -0
- hpcflow/sdk/core/environment.py +74 -93
- hpcflow/sdk/core/errors.py +398 -51
- hpcflow/sdk/core/json_like.py +540 -272
- hpcflow/sdk/core/loop.py +380 -334
- hpcflow/sdk/core/loop_cache.py +160 -43
- hpcflow/sdk/core/object_list.py +370 -207
- hpcflow/sdk/core/parameters.py +728 -600
- hpcflow/sdk/core/rule.py +59 -41
- hpcflow/sdk/core/run_dir_files.py +33 -22
- hpcflow/sdk/core/task.py +1546 -1325
- hpcflow/sdk/core/task_schema.py +240 -196
- hpcflow/sdk/core/test_utils.py +126 -88
- hpcflow/sdk/core/types.py +387 -0
- hpcflow/sdk/core/utils.py +410 -305
- hpcflow/sdk/core/validation.py +82 -9
- hpcflow/sdk/core/workflow.py +1192 -1028
- hpcflow/sdk/core/zarr_io.py +98 -137
- hpcflow/sdk/demo/cli.py +46 -33
- hpcflow/sdk/helper/cli.py +18 -16
- hpcflow/sdk/helper/helper.py +75 -63
- hpcflow/sdk/helper/watcher.py +61 -28
- hpcflow/sdk/log.py +83 -59
- hpcflow/sdk/persistence/__init__.py +8 -31
- hpcflow/sdk/persistence/base.py +988 -586
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +38 -0
- hpcflow/sdk/persistence/json.py +408 -153
- hpcflow/sdk/persistence/pending.py +158 -123
- hpcflow/sdk/persistence/store_resource.py +37 -22
- hpcflow/sdk/persistence/types.py +307 -0
- hpcflow/sdk/persistence/utils.py +14 -11
- hpcflow/sdk/persistence/zarr.py +477 -420
- hpcflow/sdk/runtime.py +44 -41
- hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
- hpcflow/sdk/submission/jobscript.py +444 -404
- hpcflow/sdk/submission/schedulers/__init__.py +133 -40
- hpcflow/sdk/submission/schedulers/direct.py +97 -71
- hpcflow/sdk/submission/schedulers/sge.py +132 -126
- hpcflow/sdk/submission/schedulers/slurm.py +263 -268
- hpcflow/sdk/submission/schedulers/utils.py +7 -2
- hpcflow/sdk/submission/shells/__init__.py +14 -15
- hpcflow/sdk/submission/shells/base.py +102 -29
- hpcflow/sdk/submission/shells/bash.py +72 -55
- hpcflow/sdk/submission/shells/os_version.py +31 -30
- hpcflow/sdk/submission/shells/powershell.py +37 -29
- hpcflow/sdk/submission/submission.py +203 -257
- hpcflow/sdk/submission/types.py +143 -0
- hpcflow/sdk/typing.py +163 -12
- hpcflow/tests/conftest.py +8 -6
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
- hpcflow/tests/scripts/test_main_scripts.py +60 -30
- hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
- hpcflow/tests/unit/test_action.py +86 -75
- hpcflow/tests/unit/test_action_rule.py +9 -4
- hpcflow/tests/unit/test_app.py +13 -6
- hpcflow/tests/unit/test_cli.py +1 -1
- hpcflow/tests/unit/test_command.py +71 -54
- hpcflow/tests/unit/test_config.py +20 -15
- hpcflow/tests/unit/test_config_file.py +21 -18
- hpcflow/tests/unit/test_element.py +58 -62
- hpcflow/tests/unit/test_element_iteration.py +3 -1
- hpcflow/tests/unit/test_element_set.py +29 -19
- hpcflow/tests/unit/test_group.py +4 -2
- hpcflow/tests/unit/test_input_source.py +116 -93
- hpcflow/tests/unit/test_input_value.py +29 -24
- hpcflow/tests/unit/test_json_like.py +44 -35
- hpcflow/tests/unit/test_loop.py +65 -58
- hpcflow/tests/unit/test_object_list.py +17 -12
- hpcflow/tests/unit/test_parameter.py +16 -7
- hpcflow/tests/unit/test_persistence.py +48 -35
- hpcflow/tests/unit/test_resources.py +20 -18
- hpcflow/tests/unit/test_run.py +8 -3
- hpcflow/tests/unit/test_runtime.py +2 -1
- hpcflow/tests/unit/test_schema_input.py +23 -15
- hpcflow/tests/unit/test_shell.py +3 -2
- hpcflow/tests/unit/test_slurm.py +8 -7
- hpcflow/tests/unit/test_submission.py +39 -19
- hpcflow/tests/unit/test_task.py +352 -247
- hpcflow/tests/unit/test_task_schema.py +33 -20
- hpcflow/tests/unit/test_utils.py +9 -11
- hpcflow/tests/unit/test_value_sequence.py +15 -12
- hpcflow/tests/unit/test_workflow.py +114 -83
- hpcflow/tests/unit/test_workflow_template.py +0 -1
- hpcflow/tests/workflows/test_jobscript.py +2 -1
- hpcflow/tests/workflows/test_workflows.py +18 -13
- {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
- hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
- hpcflow/sdk/core/parallel.py +0 -21
- hpcflow_new2-0.2.0a188.dist-info/RECORD +0 -158
- {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a188.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
- {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:
|
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
|
-
|
33
|
-
return {k: v
|
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:
|
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 =
|
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
|
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
|
-
|
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
|
7
|
-
from typing import
|
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
|
-
|
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
|
-
|
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
|
-
|
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) ->
|
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) ->
|
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:
|
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(
|
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(
|
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
|
-
|
83
|
-
if
|
84
|
-
app_invoc
|
85
|
-
|
86
|
-
|
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:
|
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(
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
189
|
-
return app_invoc_exe
|
191
|
+
return app_invoc_exe.replace(" ", r"\ ")
|
190
192
|
|
191
|
-
|
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
|
-
|
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:
|
299
|
-
WSL_distribution:
|
300
|
-
WSL_user:
|
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) ->
|
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) ->
|
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:
|
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
|
-
|
345
|
-
return wsl_path
|
357
|
+
return "/".join(parts)
|
346
358
|
|
347
|
-
def process_JS_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"]
|
350
|
-
|
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:
|
358
|
-
return [[
|
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
|
-
|
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"] =
|
386
|
+
vers_info["shell_name"] = f"wsl+{vers_info['shell_name']}".lower()
|
372
387
|
vers_info["WSL_executable"] = self.WSL_executable
|
373
|
-
|
374
|
-
|
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
|
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
|
|