Skip to content

Commit de84925

Browse files
committed
debugged test_environments (except singularity tests)
1 parent 7418345 commit de84925

File tree

6 files changed

+86
-77
lines changed

6 files changed

+86
-77
lines changed

pydra/design/shell.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,15 @@ def _validate_path_template(self, attribute, value):
216216
f"path_template ({value!r}) can only be provided when no default "
217217
f"({self.default!r}) is provided"
218218
)
219+
if value and not is_fileset_or_union(self.type):
220+
raise ValueError(
221+
f"path_template ({value!r}) can only be provided when type is a FileSet, "
222+
f"or union thereof, not {self.type!r}"
223+
)
219224

220225
@keep_extension.validator
221226
def _validate_keep_extension(self, attribute, value):
222-
if value and self.path_template is not None:
227+
if value and self.path_template is None:
223228
raise ValueError(
224229
f"keep_extension ({value!r}) can only be provided when path_template "
225230
f"is provided"

pydra/engine/environments.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
if ty.TYPE_CHECKING:
1111
from pydra.engine.core import Task
1212
from pydra.engine.specs import ShellDef
13-
from pydra.design import shell
1413

1514

1615
class Environment:
@@ -94,7 +93,7 @@ def bind(self, loc, mode="ro"):
9493
return f"{loc_abs}:{self.root}{loc_abs}:{mode}"
9594

9695
def _get_bindings(
97-
self, definition: "ShellDef", root: str | None = None
96+
self, task: "Task", root: str | None = None
9897
) -> tuple[dict[str, tuple[str, str]], dict[str, tuple[Path, ...]]]:
9998
"""Return bindings necessary to run task in an alternative root.
10099
@@ -111,27 +110,32 @@ def _get_bindings(
111110
bindings: dict
112111
Mapping from paths in the host environment to the target environment
113112
"""
113+
from pydra.design import shell
114+
114115
bindings: dict[str, tuple[str, str]] = {}
115116
input_updates: dict[str, tuple[Path, ...]] = {}
116117
if root is None:
117118
return bindings
118119
fld: shell.arg
119-
for fld in list_fields(definition):
120+
for fld in list_fields(task.definition):
120121
if TypeParser.contains_type(FileSet, fld.type):
121-
fileset: FileSet | None = definition[fld.name]
122-
if fileset is None:
122+
fileset: FileSet | None = task.inputs[fld.name]
123+
if not fileset:
123124
continue
124125
if not isinstance(fileset, (os.PathLike, FileSet)):
125126
raise NotImplementedError(
126-
"Generating environment bindings for nested FileSets is not "
127-
"supported yet"
127+
f"No support for generating bindings for {type(fileset)} types "
128+
f"({fileset})"
128129
)
129130
copy = fld.copy_mode == FileSet.CopyMode.copy
130131

131132
host_path, env_path = fileset.parent, Path(f"{root}{fileset.parent}")
132133

133134
# Default to mounting paths as read-only, but respect existing modes
134-
bindings[host_path] = (env_path, "rw" if copy else "ro")
135+
bindings[host_path] = (
136+
env_path,
137+
"rw" if copy or isinstance(fld, shell.outarg) else "ro",
138+
)
135139

136140
# Provide updated in-container paths to the command to be run. If a
137141
# fs-object, which resolves to a single path, just pass in the name of
@@ -152,9 +156,7 @@ class Docker(Container):
152156
def execute(self, task: "Task[ShellDef]") -> dict[str, ty.Any]:
153157
docker_img = f"{self.image}:{self.tag}"
154158
# mounting all input locations
155-
mounts, input_updates = self._get_bindings(
156-
definition=task.definition, root=self.root
157-
)
159+
mounts, input_updates = self._get_bindings(task=task, root=self.root)
158160

159161
docker_args = [
160162
"docker",
@@ -193,9 +195,7 @@ class Singularity(Container):
193195
def execute(self, task: "Task[ShellDef]") -> dict[str, ty.Any]:
194196
singularity_img = f"{self.image}:{self.tag}"
195197
# mounting all input locations
196-
mounts, input_updates = self._get_bindings(
197-
definition=task.definition, root=self.root
198-
)
198+
mounts, input_updates = self._get_bindings(task=task, root=self.root)
199199

200200
# todo adding xargsy etc
201201
singularity_args = [

pydra/engine/helpers_file.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def template_update_single(
157157
input_values: dict[str, ty.Any] = None,
158158
output_dir: Path | None = None,
159159
spec_type: str = "input",
160-
):
160+
) -> Path | None:
161161
"""Update a single template from the input_spec or output_spec
162162
based on the value from inputs_dict
163163
(checking the types of the fields, that have "output_file_template)"
@@ -198,9 +198,9 @@ def template_update_single(
198198
if output_dir and value is not None:
199199
# should be converted to str, it is also used for input fields that should be str
200200
if type(value) is list:
201-
return [str(output_dir / Path(val).name) for val in value]
201+
return [output_dir / val.name for val in value]
202202
else:
203-
return str(output_dir / Path(value).name)
203+
return output_dir / value.name
204204
else:
205205
return None
206206

@@ -243,7 +243,7 @@ def _template_formatting(field, definition, input_values):
243243
formatted = _string_template_formatting(
244244
field, template, definition, input_values
245245
)
246-
return formatted
246+
return Path(formatted)
247247

248248

249249
def _string_template_formatting(field, template, definition, input_values):
@@ -252,6 +252,14 @@ def _string_template_formatting(field, template, definition, input_values):
252252
inp_fields = re.findall(r"{\w+}", template)
253253
inp_fields_fl = re.findall(r"{\w+:[0-9.]+f}", template)
254254
inp_fields += [re.sub(":[0-9.]+f", "", el) for el in inp_fields_fl]
255+
256+
# FIXME: This would be a better solution, and would allow you to explicitly specify
257+
# whether you want to use the extension of the input file or not, by referencing
258+
# the "ext" attribute of the input file. However, this would require a change in the
259+
# way the element formatting is done
260+
#
261+
# inp_fields = set(re.findall(r"{(\w+)(?:\.\w+)?(?::[0-9.]+f)?}", template))
262+
255263
if len(inp_fields) == 0:
256264
return template
257265

pydra/engine/specs.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ def _check_rules(self):
521521
if is_lazy(value):
522522
continue
523523

524-
if value is attrs.NOTHING:
524+
if value is attrs.NOTHING and not getattr(field, "path_template", False):
525525
errors.append(f"Mandatory field {field.name!r} is not set")
526526

527527
# Collect alternative fields associated with this field.
@@ -1043,10 +1043,9 @@ def _command_args(
10431043
output_dir = Path.cwd()
10441044
self._check_resolved()
10451045
inputs = attrs_values(self)
1046-
modified_inputs = template_update(self, output_dir=output_dir)
1046+
inputs.update(template_update(self, output_dir=output_dir))
10471047
if input_updates:
10481048
inputs.update(input_updates)
1049-
inputs.update(modified_inputs)
10501049
pos_args = [] # list for (position, command arg)
10511050
positions_provided = []
10521051
for field in list_fields(self):

pydra/engine/tests/test_environments.py

Lines changed: 30 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def newcache(x):
4343
outputs = shelly(cache_dir=newcache("shelly-exec"))
4444
assert drop_stderr(env_outputs) == drop_stderr(attrs_values(outputs))
4545

46-
outputs = shelly(environment=Native())
46+
outputs = shelly(environment=Native(), cache_dir=newcache("shelly-call"))
4747
assert env_outputs == attrs_values(outputs)
4848

4949
with Submitter(cache_dir=newcache("shelly-submitter"), environment=Native()) as sub:
@@ -59,11 +59,11 @@ def test_docker_1(tmp_path):
5959
def newcache(x):
6060
makedir(tmp_path, x)
6161

62-
cmd = ["whoami"]
62+
cmd = "whoami"
6363
docker = Docker(image="busybox")
6464
Shelly = shell.define(cmd)
6565
shelly = Shelly()
66-
assert shelly.cmdline == " ".join(cmd)
66+
assert shelly.cmdline == cmd
6767

6868
shelly_job = Task(
6969
definition=shelly,
@@ -267,7 +267,7 @@ def newcache(x):
267267
f.write("hello ")
268268

269269
shelly = shelly_with_input_factory(filename=filename, executable="cat")
270-
outputs_dict = docker.execute(shelly)
270+
outputs_dict = docker.execute(make_job(shelly, tmp_path, "shelly"))
271271

272272
with Submitter(environment=docker, cache_dir=newcache("shell_sub")) as sub:
273273
results = sub(shelly)
@@ -340,7 +340,7 @@ def newcache(x):
340340
outputs = shelly.split(file=filename)(
341341
environment=docker, cache_dir=newcache("shelly_call")
342342
)
343-
assert [s.strip for s in outputs.stdout] == ["hello", "hi"]
343+
assert [s.strip() for s in outputs.stdout] == ["hello", "hi"]
344344

345345

346346
def shelly_outputfile_factory(filename, executable="cp"):
@@ -351,16 +351,20 @@ def shelly_outputfile_factory(filename, executable="cp"):
351351
shell.arg(
352352
name="file_orig",
353353
type=File,
354-
position=2,
354+
position=1,
355355
help="new file",
356356
argstr="",
357357
),
358-
shell.arg(
358+
],
359+
outputs=[
360+
shell.outarg(
359361
name="file_copy",
360-
type=str,
361-
output_file_template="{file_orig}_copy",
362+
type=File,
363+
path_template="{file_orig}_copy",
362364
help="output file",
363365
argstr="",
366+
position=2,
367+
keep_extension=True,
364368
),
365369
],
366370
)
@@ -390,11 +394,10 @@ def newcache(x):
390394
result = sub(shelly)
391395
assert Path(result.outputs.file_copy) == result.output_dir / "file_copy.txt"
392396

393-
outputs = shelly(environment=Native(), cache_dir=newcache("shelly_call"))
394-
assert (
395-
Path(outputs.file_copy)
396-
== newcache("shelly_call") / shelly._checksum / "file_copy.txt"
397-
)
397+
call_cache = newcache("shelly_call")
398+
399+
outputs = shelly(environment=Native(), cache_dir=call_cache)
400+
assert Path(outputs.file_copy) == call_cache / shelly._checksum / "file_copy.txt"
398401

399402

400403
def test_shell_fileout_st(tmp_path):
@@ -414,34 +417,21 @@ def newcache(x):
414417

415418
filename = [filename_1, filename_2]
416419

417-
shelly = shelly_outputfile_factory(
418-
tempdir=tmp_path, filename=None, name="shelly_sub"
419-
)
420+
shelly = shelly_outputfile_factory(filename=None)
420421
with Submitter(environment=Native(), cache_dir=newcache("shelly")) as sub:
421422
results = sub(shelly.split(file_orig=filename))
422423

423-
assert results.outputs.file_copy == [
424-
File(results.output_dir / "file_1_copy.txt"),
425-
File(results.output_dir / "file_2_copy.txt"),
424+
assert [f.name for f in results.outputs.file_copy] == [
425+
"file_1_copy.txt",
426+
"file_2_copy.txt",
426427
]
427428

428429
call_cache = newcache("shelly_call")
429430

430431
outputs = shelly.split(file_orig=filename)(
431432
environment=Native(), cache_dir=call_cache
432433
)
433-
assert outputs.file_copy == [
434-
File(
435-
call_cache
436-
/ attrs.evolve(shelly, file_orig=filename_1)._checksum
437-
/ "file_1_copy.txt"
438-
),
439-
File(
440-
call_cache
441-
/ attrs.evolve(shelly, file_orig=filename_1)._checksum
442-
/ "file_1_copy.txt"
443-
),
444-
]
434+
assert [f.name for f in outputs.file_copy] == ["file_1_copy.txt", "file_2_copy.txt"]
445435

446436

447437
@no_win
@@ -459,15 +449,11 @@ def newcache(x):
459449
with open(filename, "w") as f:
460450
f.write("hello ")
461451

462-
shelly_sub = shelly_outputfile_factory(
463-
tempdir=tmp_path, filename=filename, name="shelly_sub"
464-
)
465-
shelly_sub.environment = docker_env
466-
shelly_sub()
467-
assert (
468-
Path(shelly_sub.result().output.file_copy)
469-
== shelly_sub.output_dir / "file_copy.txt"
470-
)
452+
shelly = shelly_outputfile_factory(filename=filename)
453+
454+
with Submitter(environment=docker_env, cache_dir=newcache("shelly")) as sub:
455+
results = sub(shelly)
456+
assert results.outputs.file_copy == File(results.output_dir / "file_copy.txt")
471457

472458

473459
@no_win
@@ -495,15 +481,7 @@ def newcache(x):
495481

496482
with Submitter(environment=docker_env, cache_dir=newcache("shelly_sub")) as sub:
497483
results = sub(shelly.split(file_orig=filename))
498-
assert results.outputs.file_copy == [
499-
File(
500-
results.output_dir
501-
/ attrs.evolve(shelly, file_orig=filename_1)._checksum
502-
/ "file_1_copy.txt"
503-
),
504-
File(
505-
results.output_dir
506-
/ attrs.evolve(shelly, file_orig=filename_2)._checksum
507-
/ "file_2_copy.txt"
508-
),
484+
assert [f.name for f in results.outputs.file_copy] == [
485+
"file_1_copy.txt",
486+
"file_2_copy.txt",
509487
]

pydra/utils/typing.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,10 +1056,29 @@ def non_optional_type(type_: type) -> type:
10561056
return type_
10571057

10581058

1059-
def is_fileset_or_union(type_: type) -> bool:
1060-
"""Check if the type is a FileSet or a Union containing a FileSet"""
1059+
def is_fileset_or_union(type_: type, allow_none: bool | None = None) -> bool:
1060+
"""Check if the type is a FileSet or a Union containing a FileSet
1061+
1062+
Parameters
1063+
----------
1064+
type_ : type
1065+
the type to check
1066+
allow_none : bool, optional
1067+
whether to allow None as a valid type, by default None. If None, then None
1068+
is not allowed at the outer layer, but is allowed within a Union
1069+
1070+
Returns
1071+
-------
1072+
is_fileset : bool
1073+
whether the type is a FileSet or a Union containing a FileSet
1074+
"""
1075+
if type_ is None and allow_none:
1076+
return True
10611077
if is_union(type_):
1062-
return any(is_fileset_or_union(t) for t in ty.get_args(type_))
1078+
return any(
1079+
is_fileset_or_union(t, allow_none=allow_none or allow_none is None)
1080+
for t in ty.get_args(type_)
1081+
)
10631082
elif not inspect.isclass(type_):
10641083
return False
10651084
return issubclass(type_, core.FileSet)

0 commit comments

Comments
 (0)