Skip to content

Commit a0f068b

Browse files
committed
♻️ Improved dependency management in BatchFile
xtl.automate.batchfile:BatchFile - The constructor now also accepts a list of dependencies that are required for execution of the script - New .dependencies property that returns a list of DependencySettings from the requested dependencies - New .get_preamble() method that get a list of commands from the compute site which are added on the top of the script, e.g. module commands xtl.automate.sites:ComputeSite - New .get_preamble() abstract method xtl.automate.sites:LocalSite - The ._default_shell attribute is now set to DefaultShell, not None xtl.automate.sites:ModulesSite - New compute site that handles loading of environment modules in batch files xtl.config.settings:DependencySettings - Added .provides attribute to list all executables provided by a certain dependency - Added .modules attribute to list all modules that are required to provide a certain dependency
1 parent 9fdb057 commit a0f068b

File tree

4 files changed

+154
-31
lines changed

4 files changed

+154
-31
lines changed

src/xtl/automate/batchfile.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
__all__ = ['BatchFile']
22

33
from pathlib import Path, PurePosixPath
4-
import stat
5-
from typing import Any, Sequence, Optional
4+
from typing import Any, Iterable, TYPE_CHECKING
65

6+
from xtl import Logger
77
from xtl.automate.sites import ComputeSite, LocalSite
88
from xtl.automate.shells import Shell, DefaultShell, WslShell
99
from xtl.common.os import FilePermissions
1010
from xtl.common.compatibility import OS_POSIX
1111

12+
if TYPE_CHECKING:
13+
from xtl.config.settings import DependencySettings
14+
15+
logger = Logger(__name__)
16+
1217

1318
class BatchFile:
1419

15-
def __init__(self, filename: str | Path, compute_site: Optional[ComputeSite] = None,
16-
shell: Shell | WslShell = DefaultShell):
20+
def __init__(self, filename: str | Path, compute_site: ComputeSite = None,
21+
shell: Shell | WslShell = DefaultShell,
22+
dependencies: str | Iterable[str] = None):
1723
"""
1824
A class for programmatically creating batch files. Additional configuration can be done by passing a ComputeSite
1925
instance.
@@ -42,6 +48,14 @@ def __init__(self, filename: str | Path, compute_site: Optional[ComputeSite] = N
4248
raise TypeError(f"compute_site must be an instance of ComputeSite, not {type(compute_site)}")
4349
self._compute_site = compute_site
4450

51+
# Set dependencies
52+
if dependencies is None:
53+
self._dependencies = set()
54+
elif isinstance(dependencies, str):
55+
self._dependencies = {dependencies}
56+
elif isinstance(dependencies, Iterable):
57+
self._dependencies = set(dependencies)
58+
4559
# List of lines of the batch file
4660
self._lines = []
4761

@@ -83,6 +97,22 @@ def permissions(self, value: int | str | FilePermissions):
8397
"""
8498
self._permissions = FilePermissions(value)
8599

100+
@property
101+
def dependencies(self) -> list['DependencySettings']:
102+
"""
103+
Returns a list of DependencySettings that are required by this batch file.
104+
"""
105+
from xtl import settings
106+
deps = list()
107+
108+
for dname, dep in settings.dependencies:
109+
if dname in self._dependencies:
110+
deps.append(dep)
111+
else:
112+
logger.warning('Dependency %(name)s not found in xtl.settings, '
113+
'skipping', {'name': dname})
114+
return deps
115+
86116
def get_execute_command(self, arguments: list = None, as_list: bool = False) -> str:
87117
if self._wsl_filename:
88118
return self.shell.get_batch_command(batch_file=self._wsl_filename, batch_arguments=arguments, as_list=as_list)
@@ -112,11 +142,11 @@ def add_command(self, command: str) -> None:
112142
"""
113143
self._add_line(command)
114144

115-
def add_commands(self, *commands: str | Sequence[str]) -> None:
145+
def add_commands(self, *commands: str | Iterable[str]) -> None:
116146
"""
117147
Add multiple commands to the batch file all at once.
118148
"""
119-
if len(commands) == 1 and isinstance(commands[0], Sequence): # unpack a list or tuple of commands
149+
if len(commands) == 1 and isinstance(commands[0], Iterable): # unpack a list or tuple of commands
120150
commands = commands[0]
121151
for command in commands:
122152
if not isinstance(command, str):
@@ -129,18 +159,24 @@ def add_comment(self, comment):
129159
"""
130160
self._add_line(f"{self.shell.comment_char} {comment}")
131161

132-
def load_modules(self, modules: str | Sequence[str]):
162+
def load_modules(self, modules: str | Iterable[str]):
133163
"""
134164
Add command for loading one or more modules on the compute site.
135165
"""
166+
# TODO: Remove when refactoring AutoPROCJob
136167
self.add_command(self.compute_site.load_modules(modules))
137168

138169
def purge_modules(self):
139170
"""
140171
Add command for purging all loaded modules on the compute site.
141172
"""
173+
# TODO: Remove when refactoring AutoPROCJob
142174
self.add_command(self.compute_site.purge_modules())
143175

176+
def get_preamble(self) -> list[str]:
177+
return self.compute_site.get_preamble(dependencies=self.dependencies,
178+
shell=self.shell)
179+
144180
def assign_variable(self, variable: str, value: Any):
145181
"""
146182
Assign a value to a variable in the batch file.
@@ -161,9 +197,13 @@ def save(self, change_permissions: bool = True):
161197
self._filename.unlink(missing_ok=True)
162198

163199
# Write contents to file
164-
text = self.shell.shebang + self.shell.new_line_char if self.shell.shebang else ''
165-
text += self.shell.new_line_char.join(self._lines)
166-
self._filename.write_text(text, encoding='utf-8', newline=self.shell.new_line_char)
200+
nl = self.shell.new_line_char
201+
preamble = self.get_preamble()
202+
text = self.shell.shebang + nl if self.shell.shebang else ''
203+
if preamble:
204+
text += nl.join(self.get_preamble()) + nl
205+
text += nl.join(self._lines)
206+
self._filename.write_text(text, encoding='utf-8', newline=nl)
167207

168208
# Update permissions (user: read, write, execute; group: read, write)
169209
if change_permissions and OS_POSIX:

src/xtl/automate/sites.py

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from abc import ABC, abstractmethod
22
from dataclasses import dataclass
3-
from typing import Optional, Sequence
3+
from typing import Optional, Iterable, TYPE_CHECKING
44

55
from xtl.automate.priority_system import PrioritySystemType, DefaultPrioritySystem, NicePrioritySystem
6-
from xtl.automate.shells import Shell, DefaultShell, BashShell
6+
from xtl.automate.shells import Shell, DefaultShell, BashShell, CmdShell, WslShell
7+
8+
if TYPE_CHECKING:
9+
from xtl.config.settings import DependencySettings
710

811

912
@dataclass
@@ -15,7 +18,7 @@ class ComputeSite(ABC):
1518

1619
_priority_system: Optional[PrioritySystemType] = None
1720
_default_shell: Optional[Shell] = None
18-
_supported_shells: Sequence[Shell] | None = None
21+
_supported_shells: Iterable[Shell] | None = None
1922

2023
@property
2124
def priority_system(self):
@@ -38,7 +41,7 @@ def default_shell(self) -> Optional[Shell]:
3841
return self._default_shell
3942

4043
@property
41-
def supported_shells(self) -> Sequence[Shell] | None:
44+
def supported_shells(self) -> Iterable[Shell] | None:
4245
"""
4346
The supported shells for this compute site. This is used to validate the shell passed to the BatchFile.
4447
"""
@@ -53,17 +56,25 @@ def is_valid_shell(self, shell: Shell) -> bool:
5356
return shell in self.supported_shells
5457

5558
@abstractmethod
56-
def load_modules(self, modules: str | Sequence[str]) -> str:
59+
def load_modules(self, modules: str | Iterable[str]) -> str:
5760
"""
5861
Generates a command for loading the specified modules on the compute site.
5962
"""
63+
# TODO: Remove when refactoring AutoPROCJob
6064
pass
6165

6266
@abstractmethod
6367
def purge_modules(self) -> str:
6468
"""
6569
Generates a command for purging all loaded modules on the compute site.
6670
"""
71+
# TODO: Remove when refactoring AutoPROCJob
72+
pass
73+
74+
@abstractmethod
75+
def get_preamble(self, dependencies: 'DependencySettings' |
76+
Iterable['DependencySettings'],
77+
shell: Shell | WslShell = None) -> list[str]:
6778
pass
6879

6980
def prepare_command(self, command: str) -> str:
@@ -80,13 +91,64 @@ def __init__(self):
8091
assumes that all required executables are available on PATH.
8192
"""
8293
self._priority_system = DefaultPrioritySystem()
94+
self._default_shell = DefaultShell
8395

8496
def load_modules(self, modules) -> str:
8597
return ''
8698

8799
def purge_modules(self) -> str:
88100
return ''
89101

102+
def get_preamble(self, dependencies: 'DependencySettings' |
103+
Iterable['DependencySettings'],
104+
shell: Shell | WslShell = None) -> list[str]:
105+
# Returns an empty preamble for the local site, as it assumes that all required
106+
# executables are available on PATH
107+
return []
108+
109+
110+
class ModulesSite(ComputeSite):
111+
def __init__(self):
112+
self._default_shell = DefaultShell
113+
self._priority_system = DefaultPrioritySystem()
114+
115+
def load_modules(self, modules: str | Iterable[str]) -> str: ...
116+
117+
def purge_modules(self) -> str: ...
118+
119+
@staticmethod
120+
def _load_modules(modules: Iterable[str], shell: Shell = None) -> str:
121+
cmd = 'module load ' + ' '.join(modules)
122+
if shell is CmdShell:
123+
# For Windows CMD, we need to use 'call' to execute the module commands
124+
cmd = f'call {cmd}'
125+
return cmd
126+
127+
@staticmethod
128+
def _purge_modules(shell: Shell = None) -> str:
129+
cmd = 'module purge'
130+
if shell is CmdShell:
131+
cmd = f'call {cmd}'
132+
return cmd
133+
134+
def get_preamble(self, dependencies: 'DependencySettings' |
135+
Iterable['DependencySettings'],
136+
shell: Shell | WslShell = None) -> list[str]:
137+
if shell is None:
138+
shell = self._default_shell
139+
if not isinstance(dependencies, Iterable):
140+
dependencies = [dependencies]
141+
142+
modules = []
143+
for dep in dependencies:
144+
if dep.modules:
145+
modules.extend(dep.modules)
146+
147+
cmds = [self._purge_modules(shell=shell)]
148+
if modules:
149+
cmds.append(self._load_modules(modules, shell=shell))
150+
return cmds
151+
90152

91153
class BiotixHPC(ComputeSite):
92154
def __init__(self):
@@ -99,13 +161,13 @@ def __init__(self):
99161
self._default_shell = BashShell
100162
self._supported_shells = [BashShell]
101163

102-
def load_modules(self, modules: str | Sequence[str]) -> str:
164+
def load_modules(self, modules: str | Iterable[str]) -> str:
103165
mods = []
104166
if isinstance(modules, str):
105167
# Check for space-separated modules in a single string
106168
for mod in modules.split():
107169
mods.append(mod)
108-
elif isinstance(modules, Sequence):
170+
elif isinstance(modules, Iterable):
109171
# Otherwise append each module in the list
110172
for i, mod in enumerate(modules):
111173
if not isinstance(mod, str):
@@ -122,5 +184,11 @@ def load_modules(self, modules: str | Sequence[str]) -> str:
122184
def purge_modules(self) -> str:
123185
return 'module purge'
124186

187+
def get_preamble(self, dependencies: 'DependencySettings' |
188+
Iterable['DependencySettings'],
189+
shell: Shell | WslShell = None) -> list[str]:
190+
# TODO: Implement this
191+
return []
192+
125193

126-
ComputeSiteType = LocalSite | BiotixHPC
194+
ComputeSiteType = LocalSite | ModulesSite | BiotixHPC

src/xtl/config/settings.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"""
66

77
from pathlib import Path
8-
from pprint import pprint
9-
from typing import ClassVar, Optional
8+
from typing import ClassVar, Optional, TYPE_CHECKING
109

1110
from pydantic import PrivateAttr
1211

@@ -100,14 +99,25 @@ class DependencySettings(Settings):
10099
"""
101100

102101
# Model attributes
103-
path: Optional[Path] = Option(default=None,
104-
desc='Directory containing binaries',
105-
validator=CastAsNoneIfEmpty())
106-
# TODO: Enable when ModuleSite is implemented
107-
modules: Optional[list[str]] = Option(default=None,
108-
desc='Modules that provide the dependency',
109-
validator=CastAsNoneIfEmpty(),
110-
exclude=True)
102+
provides: set[str] = \
103+
Option(
104+
default_factory=set,
105+
desc='List of executables provided by this dependency',
106+
exclude=True
107+
)
108+
109+
path: Optional[Path] = \
110+
Option(
111+
default=None,
112+
desc='Directory containing binaries',
113+
validator=CastAsNoneIfEmpty()
114+
)
115+
116+
modules: set[str] = \
117+
Option(
118+
default_factory=set,
119+
desc='Modules that provide the dependency'
120+
)
111121

112122

113123
class DependenciesSettings(Settings):
@@ -116,7 +126,12 @@ class DependenciesSettings(Settings):
116126
"""
117127

118128
# Model attributes
119-
autoproc: DependencySettings = Option(default=DependencySettings())
129+
autoproc: DependencySettings = \
130+
Option(
131+
default=DependencySettings(
132+
provides={'process', 'process_wf'}
133+
)
134+
)
120135

121136

122137
class CLIAutoprocSettings(Settings):

tests/automate/test_sites.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from xtl.automate.priority_system import DefaultPrioritySystem, NicePrioritySystem
4-
from xtl.automate.shells import BashShell
4+
from xtl.automate.shells import BashShell, DefaultShell
55
from xtl.automate.sites import ComputeSite, LocalSite, BiotixHPC
66

77

@@ -17,7 +17,7 @@ def test_priority_system(self):
1717

1818
def test_default_shell(self):
1919
cs = LocalSite()
20-
assert cs.default_shell is None
20+
assert cs.default_shell is DefaultShell
2121
assert cs.supported_shells is None
2222

2323
def test_load_modules(self):

0 commit comments

Comments
 (0)