hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a190__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.0a189.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.0a189.dist-info/RECORD +0 -158
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,192 @@
|
|
1
|
+
"""
|
2
|
+
Core enumeration types.
|
3
|
+
"""
|
4
|
+
from __future__ import annotations
|
5
|
+
from collections.abc import Sequence
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from enum import Enum
|
8
|
+
|
9
|
+
|
10
|
+
class ActionScopeType(Enum):
|
11
|
+
"""
|
12
|
+
Types of action scope.
|
13
|
+
"""
|
14
|
+
|
15
|
+
#: Scope that applies to anything.
|
16
|
+
ANY = 0
|
17
|
+
#: Scope that only applies to main scripts.
|
18
|
+
MAIN = 1
|
19
|
+
#: Scope that applies to processing steps.
|
20
|
+
PROCESSING = 2
|
21
|
+
#: Scope that applies to input file generators.
|
22
|
+
INPUT_FILE_GENERATOR = 3
|
23
|
+
#: Scope that applies to output file parsers.
|
24
|
+
OUTPUT_FILE_PARSER = 4
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass(frozen=True)
|
28
|
+
class _EARStatus:
|
29
|
+
"""
|
30
|
+
Model of the state of an EARStatus.
|
31
|
+
"""
|
32
|
+
|
33
|
+
_value: int
|
34
|
+
#: Symbol to use when rendering a status.
|
35
|
+
symbol: str
|
36
|
+
#: Colour to use when rendering a status.
|
37
|
+
colour: str
|
38
|
+
__doc__: str = ""
|
39
|
+
|
40
|
+
|
41
|
+
class EARStatus(_EARStatus, Enum):
|
42
|
+
"""Enumeration of all possible EAR statuses, and their associated status colour."""
|
43
|
+
|
44
|
+
#: Not yet associated with a submission.
|
45
|
+
pending = (
|
46
|
+
0,
|
47
|
+
".",
|
48
|
+
"grey46",
|
49
|
+
"Not yet associated with a submission.",
|
50
|
+
)
|
51
|
+
#: Associated with a prepared submission that is not yet submitted.
|
52
|
+
prepared = (
|
53
|
+
1,
|
54
|
+
".",
|
55
|
+
"grey46",
|
56
|
+
"Associated with a prepared submission that is not yet submitted.",
|
57
|
+
)
|
58
|
+
#: Submitted for execution.
|
59
|
+
submitted = (
|
60
|
+
2,
|
61
|
+
".",
|
62
|
+
"grey46",
|
63
|
+
"Submitted for execution.",
|
64
|
+
)
|
65
|
+
#: Executing now.
|
66
|
+
running = (
|
67
|
+
3,
|
68
|
+
"●",
|
69
|
+
"dodger_blue1",
|
70
|
+
"Executing now.",
|
71
|
+
)
|
72
|
+
#: Not attempted due to a failure of an upstream action on which this depends,
|
73
|
+
#: or a loop termination condition being satisfied.
|
74
|
+
skipped = (
|
75
|
+
4,
|
76
|
+
"s",
|
77
|
+
"dark_orange",
|
78
|
+
(
|
79
|
+
"Not attempted due to a failure of an upstream action on which this depends, "
|
80
|
+
"or a loop termination condition being satisfied."
|
81
|
+
),
|
82
|
+
)
|
83
|
+
#: Aborted by the user; downstream actions will be attempted.
|
84
|
+
aborted = (
|
85
|
+
5,
|
86
|
+
"A",
|
87
|
+
"deep_pink4",
|
88
|
+
"Aborted by the user; downstream actions will be attempted.",
|
89
|
+
)
|
90
|
+
#: Probably exited successfully.
|
91
|
+
success = (
|
92
|
+
6,
|
93
|
+
"■",
|
94
|
+
"green3",
|
95
|
+
"Probably exited successfully.",
|
96
|
+
)
|
97
|
+
#: Probably failed.
|
98
|
+
error = (
|
99
|
+
7,
|
100
|
+
"E",
|
101
|
+
"red3",
|
102
|
+
"Probably failed.",
|
103
|
+
)
|
104
|
+
|
105
|
+
@property
|
106
|
+
def value(self) -> int:
|
107
|
+
#: The value of the status.
|
108
|
+
return self._value
|
109
|
+
|
110
|
+
@classmethod
|
111
|
+
def get_non_running_submitted_states(cls) -> frozenset[EARStatus]:
|
112
|
+
"""Return the set of all non-running states, excluding those before submission."""
|
113
|
+
return frozenset(
|
114
|
+
{
|
115
|
+
cls.skipped,
|
116
|
+
cls.aborted,
|
117
|
+
cls.success,
|
118
|
+
cls.error,
|
119
|
+
}
|
120
|
+
)
|
121
|
+
|
122
|
+
@property
|
123
|
+
def rich_repr(self) -> str:
|
124
|
+
"""
|
125
|
+
The rich representation of the value.
|
126
|
+
"""
|
127
|
+
return f"[{self.colour}]{self.symbol}[/{self.colour}]"
|
128
|
+
|
129
|
+
|
130
|
+
class InputSourceType(Enum):
|
131
|
+
"""
|
132
|
+
The types of input sources.
|
133
|
+
"""
|
134
|
+
|
135
|
+
#: Input source is an import.
|
136
|
+
IMPORT = 0
|
137
|
+
#: Input source is local.
|
138
|
+
LOCAL = 1
|
139
|
+
#: Input source is a default.
|
140
|
+
DEFAULT = 2
|
141
|
+
#: Input source is a task.
|
142
|
+
TASK = 3
|
143
|
+
|
144
|
+
|
145
|
+
class ParallelMode(Enum):
|
146
|
+
"""
|
147
|
+
Potential parallel modes.
|
148
|
+
"""
|
149
|
+
|
150
|
+
#: Use distributed-memory parallelism (e.g. MPI).
|
151
|
+
DISTRIBUTED = 0
|
152
|
+
#: Use shared-memory parallelism (e.g. OpenMP).
|
153
|
+
SHARED = 1
|
154
|
+
#: Use both distributed- and shared-memory parallelism.
|
155
|
+
#:
|
156
|
+
#: Note
|
157
|
+
#: ----
|
158
|
+
#: This is not yet implemented in any meaningful way!
|
159
|
+
HYBRID = 2
|
160
|
+
|
161
|
+
|
162
|
+
class ParameterPropagationMode(Enum):
|
163
|
+
"""
|
164
|
+
How a parameter is propagated.
|
165
|
+
"""
|
166
|
+
|
167
|
+
#: Parameter is propagated implicitly.
|
168
|
+
IMPLICIT = 0
|
169
|
+
#: Parameter is propagated explicitly.
|
170
|
+
EXPLICIT = 1
|
171
|
+
#: Parameter is never propagated.
|
172
|
+
NEVER = 2
|
173
|
+
|
174
|
+
|
175
|
+
class TaskSourceType(Enum):
|
176
|
+
"""
|
177
|
+
The types of task-based input sources.
|
178
|
+
"""
|
179
|
+
|
180
|
+
#: Input source is a task input.
|
181
|
+
INPUT = 0
|
182
|
+
#: Input source is a task output.
|
183
|
+
OUTPUT = 1
|
184
|
+
#: Input source is unspecified.
|
185
|
+
ANY = 2
|
186
|
+
|
187
|
+
@classmethod
|
188
|
+
def names(cls) -> Sequence[str]:
|
189
|
+
"""
|
190
|
+
Get the names of the task source types.
|
191
|
+
"""
|
192
|
+
return cls._member_names_
|
hpcflow/sdk/core/environment.py
CHANGED
@@ -5,16 +5,18 @@ Model of an execution environment.
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
7
|
from dataclasses import dataclass
|
8
|
-
from typing import
|
8
|
+
from typing import TYPE_CHECKING
|
9
9
|
|
10
|
-
from
|
11
|
-
|
12
|
-
from hpcflow.sdk import app
|
10
|
+
from hpcflow.sdk.typing import hydrate
|
13
11
|
from hpcflow.sdk.core.errors import DuplicateExecutableError
|
14
12
|
from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
15
13
|
from hpcflow.sdk.core.object_list import ExecutablesList
|
16
14
|
from hpcflow.sdk.core.utils import check_valid_py_identifier, get_duplicate_items
|
17
15
|
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from collections.abc import Mapping, Sequence
|
18
|
+
from typing import Any, ClassVar
|
19
|
+
|
18
20
|
|
19
21
|
@dataclass
|
20
22
|
class NumCores(JSONLike):
|
@@ -36,30 +38,14 @@ class NumCores(JSONLike):
|
|
36
38
|
#: The maximum number of cores supported.
|
37
39
|
stop: int
|
38
40
|
#: The step in the number of cores supported. Normally 1.
|
39
|
-
step: int =
|
40
|
-
|
41
|
-
def __post_init__(self):
|
42
|
-
if self.step is None:
|
43
|
-
self.step = 1
|
44
|
-
|
45
|
-
def __contains__(self, x):
|
46
|
-
if x in range(self.start, self.stop + 1, self.step):
|
47
|
-
return True
|
48
|
-
else:
|
49
|
-
return False
|
41
|
+
step: int = 1
|
50
42
|
|
51
|
-
def
|
52
|
-
|
53
|
-
type(self) == type(other)
|
54
|
-
and self.start == other.start
|
55
|
-
and self.stop == other.stop
|
56
|
-
and self.step == other.step
|
57
|
-
):
|
58
|
-
return True
|
59
|
-
return False
|
43
|
+
def __contains__(self, x: int) -> bool:
|
44
|
+
return x in range(self.start, self.stop + 1, self.step)
|
60
45
|
|
61
46
|
|
62
47
|
@dataclass
|
48
|
+
@hydrate
|
63
49
|
class ExecutableInstance(JSONLike):
|
64
50
|
"""
|
65
51
|
A particular instance of an executable that can support some mode of operation.
|
@@ -75,30 +61,26 @@ class ExecutableInstance(JSONLike):
|
|
75
61
|
"""
|
76
62
|
|
77
63
|
#: What parallel mode is supported by this executable instance.
|
78
|
-
parallel_mode: str
|
64
|
+
parallel_mode: str | None
|
79
65
|
#: The number of cores supported by this executable instance.
|
80
|
-
num_cores:
|
66
|
+
num_cores: NumCores
|
81
67
|
#: The actual command to use for this executable instance.
|
82
68
|
command: str
|
83
69
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
and self.command == other.command
|
96
|
-
):
|
97
|
-
return True
|
98
|
-
return False
|
70
|
+
def __init__(
|
71
|
+
self, parallel_mode: str | None, num_cores: NumCores | int | dict, command: str
|
72
|
+
):
|
73
|
+
self.parallel_mode = parallel_mode
|
74
|
+
self.command = command
|
75
|
+
if isinstance(num_cores, NumCores):
|
76
|
+
self.num_cores = num_cores
|
77
|
+
elif isinstance(num_cores, int):
|
78
|
+
self.num_cores = NumCores(num_cores, num_cores)
|
79
|
+
else:
|
80
|
+
self.num_cores = NumCores(**num_cores)
|
99
81
|
|
100
82
|
@classmethod
|
101
|
-
def from_spec(cls, spec):
|
83
|
+
def from_spec(cls, spec: dict[str, Any]) -> ExecutableInstance:
|
102
84
|
"""
|
103
85
|
Construct an instance from a specification dictionary.
|
104
86
|
"""
|
@@ -117,7 +99,7 @@ class Executable(JSONLike):
|
|
117
99
|
The concrete instances of the application that may be present.
|
118
100
|
"""
|
119
101
|
|
120
|
-
_child_objects = (
|
102
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
121
103
|
ChildObjectSpec(
|
122
104
|
name="instances",
|
123
105
|
class_name="ExecutableInstance",
|
@@ -125,13 +107,13 @@ class Executable(JSONLike):
|
|
125
107
|
),
|
126
108
|
)
|
127
109
|
|
128
|
-
def __init__(self, label: str, instances:
|
110
|
+
def __init__(self, label: str, instances: list[ExecutableInstance]):
|
129
111
|
#: The abstract name of the program.
|
130
112
|
self.label = check_valid_py_identifier(label)
|
131
113
|
#: The concrete instances of the application that may be present.
|
132
114
|
self.instances = instances
|
133
115
|
|
134
|
-
self._executables_list = None # assigned by parent
|
116
|
+
self._executables_list: ExecutablesList | None = None # assigned by parent
|
135
117
|
|
136
118
|
def __repr__(self):
|
137
119
|
return (
|
@@ -141,24 +123,31 @@ class Executable(JSONLike):
|
|
141
123
|
f")"
|
142
124
|
)
|
143
125
|
|
144
|
-
def __eq__(self, other):
|
145
|
-
|
146
|
-
|
126
|
+
def __eq__(self, other: Any) -> bool:
|
127
|
+
return (
|
128
|
+
isinstance(other, self.__class__)
|
147
129
|
and self.label == other.label
|
148
130
|
and self.instances == other.instances
|
149
|
-
and
|
150
|
-
|
151
|
-
|
152
|
-
|
131
|
+
and (
|
132
|
+
(
|
133
|
+
self.environment
|
134
|
+
and other.environment
|
135
|
+
and self.environment.name == other.environment.name
|
136
|
+
)
|
137
|
+
or (not self.environment and not other.environment)
|
138
|
+
)
|
139
|
+
)
|
153
140
|
|
154
141
|
@property
|
155
|
-
def environment(self):
|
142
|
+
def environment(self) -> Environment | None:
|
156
143
|
"""
|
157
144
|
The environment that the executable is going to run in.
|
158
145
|
"""
|
159
|
-
return self._executables_list.environment
|
146
|
+
return None if (el := self._executables_list) is None else el.environment
|
160
147
|
|
161
|
-
def filter_instances(
|
148
|
+
def filter_instances(
|
149
|
+
self, parallel_mode: str | None = None, num_cores: int | None = None
|
150
|
+
) -> list[ExecutableInstance]:
|
162
151
|
"""
|
163
152
|
Select the instances of the executable that are compatible with the given
|
164
153
|
requirements.
|
@@ -175,12 +164,12 @@ class Executable(JSONLike):
|
|
175
164
|
list[ExecutableInstance]:
|
176
165
|
The known executable instances that match the requirements.
|
177
166
|
"""
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
167
|
+
return [
|
168
|
+
inst
|
169
|
+
for inst in self.instances
|
170
|
+
if (parallel_mode is None or inst.parallel_mode == parallel_mode)
|
171
|
+
and (num_cores is None or num_cores in inst.num_cores)
|
172
|
+
]
|
184
173
|
|
185
174
|
|
186
175
|
class Environment(JSONLike):
|
@@ -200,9 +189,8 @@ class Environment(JSONLike):
|
|
200
189
|
List of abstract executables in the environment.
|
201
190
|
"""
|
202
191
|
|
203
|
-
|
204
|
-
|
205
|
-
_child_objects = (
|
192
|
+
_validation_schema: ClassVar[str] = "environments_spec_schema.yaml"
|
193
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
206
194
|
ChildObjectSpec(
|
207
195
|
name="executables",
|
208
196
|
class_name="ExecutablesList",
|
@@ -212,59 +200,52 @@ class Environment(JSONLike):
|
|
212
200
|
|
213
201
|
def __init__(
|
214
202
|
self,
|
215
|
-
name,
|
216
|
-
setup=None,
|
217
|
-
specifiers=None,
|
218
|
-
executables=None,
|
219
|
-
doc="",
|
220
|
-
_hash_value=None,
|
203
|
+
name: str,
|
204
|
+
setup: Sequence[str] | None = None,
|
205
|
+
specifiers: Mapping[str, str] | None = None,
|
206
|
+
executables: ExecutablesList | Sequence[Executable] | None = None,
|
207
|
+
doc: str = "",
|
208
|
+
_hash_value: str | None = None,
|
221
209
|
):
|
222
210
|
#: The name of the environment.
|
223
211
|
self.name = name
|
224
212
|
#: Documentation for the environment.
|
225
213
|
self.doc = doc
|
226
|
-
#: Commands to run to enter the environment.
|
227
|
-
self.setup = setup
|
228
214
|
#: Dictionary of attributes that may be used to supply addional key/value pairs
|
229
215
|
#: to look up an environment by.
|
230
|
-
self.specifiers = specifiers or {}
|
216
|
+
self.specifiers: Mapping[str, str] = specifiers or {}
|
231
217
|
#: List of abstract executables in the environment.
|
232
218
|
self.executables = (
|
233
219
|
executables
|
234
220
|
if isinstance(executables, ExecutablesList)
|
235
|
-
else self.
|
221
|
+
else self._app.ExecutablesList(executables or ())
|
236
222
|
)
|
237
223
|
self._hash_value = _hash_value
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
224
|
+
#: Commands to run to enter the environment.
|
225
|
+
self.setup: tuple[str, ...] | None
|
226
|
+
if not setup:
|
227
|
+
self.setup = None
|
228
|
+
elif isinstance(setup, str):
|
229
|
+
self.setup = tuple(cmd.strip() for cmd in setup.strip().split("\n"))
|
230
|
+
else:
|
231
|
+
self.setup = tuple(setup)
|
245
232
|
self._set_parent_refs()
|
246
233
|
self._validate()
|
247
234
|
|
248
|
-
def __eq__(self, other):
|
249
|
-
|
250
|
-
|
235
|
+
def __eq__(self, other: Any) -> bool:
|
236
|
+
return (
|
237
|
+
isinstance(other, self.__class__)
|
251
238
|
and self.setup == other.setup
|
252
239
|
and self.executables == other.executables
|
253
240
|
and self.specifiers == other.specifiers
|
254
|
-
)
|
255
|
-
return True
|
256
|
-
return False
|
241
|
+
)
|
257
242
|
|
258
243
|
def __repr__(self):
|
259
244
|
return f"{self.__class__.__name__}({self.name!r})"
|
260
245
|
|
261
246
|
def _validate(self):
|
262
|
-
dup_labels
|
263
|
-
|
264
|
-
raise DuplicateExecutableError(
|
265
|
-
f"Executables must have unique `label`s within each environment, but "
|
266
|
-
f"found label(s) multiple times: {dup_labels!r}"
|
267
|
-
)
|
247
|
+
if dup_labels := get_duplicate_items(exe.label for exe in self.executables):
|
248
|
+
raise DuplicateExecutableError(dup_labels)
|
268
249
|
|
269
250
|
@property
|
270
251
|
def documentation(self) -> str:
|