Skip to content

Commit bd76c3d

Browse files
committed
adding the Singularity environment class
1 parent 4d2098b commit bd76c3d

File tree

7 files changed

+187
-395
lines changed

7 files changed

+187
-395
lines changed

pydra/engine/environments.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ def execute(self, task):
3232

3333

3434
class Docker(Environment):
35-
def __init__(
36-
self, image, tag="latest", binds=None, output_cpath="/output_pydra", xargs=None
37-
):
35+
def __init__(self, image, tag="latest", output_cpath="/output_pydra", xargs=None):
3836
self.image = image
3937
self.tag = tag
4038
self.xargs = xargs
@@ -82,3 +80,44 @@ def execute(self, task, root="/mnt/pydra"):
8280
# to be de-rooted
8381
# task.finalize_outputs("/mnt/pydra") TODO: probably don't need it
8482
return output
83+
84+
85+
class Singularity(Docker):
86+
def execute(self, task, root="/mnt/pydra"):
87+
# XXX Need to mount all input locations
88+
singularity_img = f"{self.image}:{self.tag}"
89+
# TODO ?
90+
# Skips over any inputs in task.cache_dir
91+
# Needs to include `out_file`s when not relative to working dir
92+
# Possibly a `TargetFile` type to distinguish between `File` and `str`?
93+
mounts = task.get_bindings(root=root)
94+
95+
# todo adding xargsy etc
96+
singularity_args = [
97+
"singularity",
98+
"exec",
99+
"-B",
100+
self.bind(task.cache_dir, "rw"),
101+
]
102+
singularity_args.extend(
103+
" ".join(
104+
[f"-B {key}:{val[0]}:{val[1]}" for (key, val) in mounts.items()]
105+
).split()
106+
)
107+
singularity_args.extend(["--pwd", f"{root}{task.output_dir}"])
108+
keys = ["return_code", "stdout", "stderr"]
109+
110+
values = execute(
111+
singularity_args + [singularity_img] + task.command_args(root="/mnt/pydra"),
112+
strip=task.strip,
113+
)
114+
output = dict(zip(keys, values))
115+
if output["return_code"]:
116+
if output["stderr"]:
117+
raise RuntimeError(output["stderr"])
118+
else:
119+
raise RuntimeError(output["stdout"])
120+
# Any outputs that have been created with a re-rooted path need
121+
# to be de-rooted
122+
# task.finalize_outputs("/mnt/pydra") TODO: probably don't need it
123+
return output

pydra/engine/specs.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -693,13 +693,6 @@ class ContainerSpec(ShellSpec):
693693
)
694694

695695

696-
@attr.s(auto_attribs=True, kw_only=True)
697-
class SingularitySpec(ContainerSpec):
698-
"""Particularize container specifications to Singularity."""
699-
700-
container: str = attr.ib("singularity", metadata={"help_string": "container type"})
701-
702-
703696
@attr.s
704697
class LazyInterface:
705698
_task: "core.TaskBase" = attr.ib()

pydra/engine/task.py

Lines changed: 0 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
ShellSpec,
5959
ShellOutSpec,
6060
ContainerSpec,
61-
SingularitySpec,
6261
attr_fields,
6362
)
6463
from .helpers import (
@@ -707,86 +706,6 @@ def _prepare_bindings(self):
707706
SUPPORTED_COPY_MODES = FileSet.CopyMode.any - FileSet.CopyMode.symlink
708707

709708

710-
class SingularityTask(ContainerTask):
711-
"""Extend shell command task for containerized execution with Singularity."""
712-
713-
init = False
714-
715-
def __init__(
716-
self,
717-
name=None,
718-
audit_flags: AuditFlag = AuditFlag.NONE,
719-
cache_dir=None,
720-
input_spec: ty.Optional[SpecInfo] = None,
721-
messenger_args=None,
722-
messengers=None,
723-
output_spec: ty.Optional[SpecInfo] = None,
724-
rerun=False,
725-
strip=False,
726-
**kwargs,
727-
):
728-
"""
729-
Initialize this task.
730-
731-
Parameters
732-
----------
733-
name : :obj:`str`
734-
Name of this task.
735-
audit_flags : :obj:`pydra.utils.messenger.AuditFlag`
736-
Auditing configuration
737-
cache_dir : :obj:`os.pathlike`
738-
Cache directory
739-
input_spec : :obj:`pydra.engine.specs.SpecInfo`
740-
Specification of inputs.
741-
messenger_args :
742-
TODO
743-
messengers :
744-
TODO
745-
output_spec : :obj:`pydra.engine.specs.BaseSpec`
746-
Specification of inputs.
747-
strip : :obj:`bool`
748-
TODO
749-
750-
"""
751-
if not self.init:
752-
if input_spec is None:
753-
input_spec = SpecInfo(
754-
name="Inputs", fields=[], bases=(SingularitySpec,)
755-
)
756-
super().__init__(
757-
name=name,
758-
input_spec=input_spec,
759-
output_spec=output_spec,
760-
audit_flags=audit_flags,
761-
messengers=messengers,
762-
messenger_args=messenger_args,
763-
cache_dir=cache_dir,
764-
strip=strip,
765-
rerun=rerun,
766-
**kwargs,
767-
)
768-
self.init = True
769-
770-
@property
771-
def container_args(self):
772-
"""Get container-specific CLI arguments."""
773-
if is_lazy(self.inputs):
774-
raise Exception("can't return container_args, self.inputs has LazyFields")
775-
self.container_check("singularity")
776-
if self.state:
777-
raise NotImplementedError
778-
779-
cargs = ["singularity", "exec"]
780-
781-
if self.inputs.container_xargs is not None:
782-
cargs.extend(self.inputs.container_xargs)
783-
784-
cargs.extend(self.binds("-B"))
785-
cargs.extend(["--pwd", str(self.output_cpath)])
786-
cargs.append(self.inputs.image)
787-
return cargs
788-
789-
790709
def split_cmd(cmd: str):
791710
"""Splits a shell command line into separate arguments respecting quotes
792711

pydra/engine/tests/test_environments.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from pathlib import Path
22

3-
from ..environments import Native, Docker
3+
from ..environments import Native, Docker, Singularity
44
from ..task import ShellCommandTask
55
from ..submitter import Submitter
66
from ..specs import (
77
ShellSpec,
88
SpecInfo,
99
File,
1010
)
11-
from .utils import no_win, need_docker
11+
from .utils import no_win, need_docker, need_singularity
1212

1313
import attr
1414

@@ -109,6 +109,72 @@ def test_docker_1_subm(tmp_path, plugin):
109109
assert env_res == shelly_call.result().output.__dict__
110110

111111

112+
@no_win
113+
@need_singularity
114+
def test_singularity_1(tmp_path):
115+
"""singularity env: simple command, no arguments"""
116+
newcache = lambda x: makedir(tmp_path, x)
117+
118+
cmd = ["whoami"]
119+
sing = Singularity(image="docker://alpine")
120+
shelly = ShellCommandTask(
121+
name="shelly", executable=cmd, cache_dir=newcache("shelly")
122+
)
123+
assert shelly.cmdline == " ".join(cmd)
124+
env_res = sing.execute(shelly)
125+
126+
shelly_env = ShellCommandTask(
127+
name="shelly",
128+
executable=cmd,
129+
cache_dir=newcache("shelly_env"),
130+
environment=sing,
131+
)
132+
shelly_env()
133+
assert env_res == shelly_env.output_ == shelly_env.result().output.__dict__
134+
135+
shelly_call = ShellCommandTask(
136+
name="shelly", executable=cmd, cache_dir=newcache("shelly_call")
137+
)
138+
shelly_call(environment=sing)
139+
assert env_res == shelly_call.output_ == shelly_call.result().output.__dict__
140+
141+
142+
@no_win
143+
@need_singularity
144+
def test_singularity_1_subm(tmp_path, plugin):
145+
"""docker env with submitter: simple command, no arguments"""
146+
newcache = lambda x: makedir(tmp_path, x)
147+
148+
cmd = ["whoami"]
149+
sing = Singularity(image="docker://alpine")
150+
shelly = ShellCommandTask(
151+
name="shelly", executable=cmd, cache_dir=newcache("shelly")
152+
)
153+
assert shelly.cmdline == " ".join(cmd)
154+
env_res = sing.execute(shelly)
155+
156+
shelly_env = ShellCommandTask(
157+
name="shelly",
158+
executable=cmd,
159+
cache_dir=newcache("shelly_env"),
160+
environment=sing,
161+
)
162+
with Submitter(plugin=plugin) as sub:
163+
shelly_env(submitter=sub)
164+
assert env_res == shelly_env.result().output.__dict__
165+
166+
shelly_call = ShellCommandTask(
167+
name="shelly", executable=cmd, cache_dir=newcache("shelly_call")
168+
)
169+
with Submitter(plugin=plugin) as sub:
170+
shelly_call(submitter=sub, environment=sing)
171+
for key in [
172+
"stdout",
173+
"return_code",
174+
]: # singularity gives info about cashed image in stderr
175+
assert env_res[key] == shelly_call.result().output.__dict__[key]
176+
177+
112178
def create_shelly_inputfile(tempdir, filename, name, executable):
113179
"""creating a task with a simple input_spec"""
114180
my_input_spec = SpecInfo(

0 commit comments

Comments
 (0)