Skip to content

Commit fa240d9

Browse files
authored
Merge pull request #705 from djarecka/env
Env
2 parents 6889577 + e4644a5 commit fa240d9

14 files changed

+366
-133
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repos:
99
- id: check-yaml
1010
- id: check-added-large-files
1111
- repo: https://github.com/psf/black
12-
rev: 23.7.0
12+
rev: 23.9.1
1313
hooks:
1414
- id: black
1515
- repo: https://github.com/codespell-project/codespell

pydra/engine/environments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def execute(self, task, root="/mnt/pydra"):
5555
# Skips over any inputs in task.cache_dir
5656
# Needs to include `out_file`s when not relative to working dir
5757
# Possibly a `TargetFile` type to distinguish between `File` and `str`?
58-
mounts = task.get_inputs_in_root(root=root)
58+
mounts = task.get_bindings(root=root)
5959

6060
# todo adding xargsy etc
6161
docker_args = ["docker", "run", "-v", self.bind(task.cache_dir, "rw")]

pydra/engine/helpers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ def make_klass(spec):
261261
type=tp,
262262
**kwargs,
263263
)
264-
type_checker = TypeParser[newfield.type](newfield.type)
264+
checker_label = f"'{name}' field of {spec.name}"
265+
type_checker = TypeParser[newfield.type](newfield.type, label=checker_label)
265266
if newfield.type in (MultiInputObj, MultiInputFile):
266267
converter = attr.converters.pipe(ensure_list, type_checker)
267268
elif newfield.type in (MultiOutputObj, MultiOutputFile):
@@ -652,7 +653,11 @@ def argstr_formatting(argstr, inputs, value_updates=None):
652653
for fld in inp_fields:
653654
fld_name = fld[1:-1] # extracting the name form {field_name}
654655
fld_value = inputs_dict[fld_name]
655-
if fld_value is attr.NOTHING:
656+
fld_attr = getattr(attrs.fields(type(inputs)), fld_name)
657+
if fld_value is attr.NOTHING or (
658+
fld_value is False
659+
and TypeParser.matches_type(fld_attr.type, ty.Union[Path, bool])
660+
):
656661
# if value is NOTHING, nothing should be added to the command
657662
val_dict[fld_name] = ""
658663
else:

pydra/engine/helpers_file.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def template_update(inputs, output_dir, state_ind=None, map_copyfiles=None):
120120
field
121121
for field in attr_fields(inputs)
122122
if field.metadata.get("output_file_template")
123+
and getattr(inputs, field.name) is not False
123124
and all(
124125
getattr(inputs, required_field) is not attr.NOTHING
125126
for required_field in field.metadata.get("requires", ())
@@ -150,25 +151,19 @@ def template_update_single(
150151
# if input_dict_st with state specific value is not available,
151152
# the dictionary will be created from inputs object
152153
from ..utils.typing import TypeParser # noqa
153-
from pydra.engine.specs import LazyField
154-
155-
VALID_TYPES = (str, ty.Union[str, bool], Path, ty.Union[Path, bool], LazyField)
154+
from pydra.engine.specs import LazyField, OUTPUT_TEMPLATE_TYPES
156155

157156
if inputs_dict_st is None:
158157
inputs_dict_st = attr.asdict(inputs, recurse=False)
159158

160159
if spec_type == "input":
161160
inp_val_set = inputs_dict_st[field.name]
162-
if inp_val_set is not attr.NOTHING and not TypeParser.is_instance(
163-
inp_val_set, VALID_TYPES
164-
):
165-
raise TypeError(
166-
f"'{field.name}' field has to be a Path instance or a bool, but {inp_val_set} set"
167-
)
168161
if isinstance(inp_val_set, bool) and field.type in (Path, str):
169162
raise TypeError(
170163
f"type of '{field.name}' is Path, consider using Union[Path, bool]"
171164
)
165+
if inp_val_set is not attr.NOTHING and not isinstance(inp_val_set, LazyField):
166+
inp_val_set = TypeParser(ty.Union[OUTPUT_TEMPLATE_TYPES])(inp_val_set)
172167
elif spec_type == "output":
173168
if not TypeParser.contains_type(FileSet, field.type):
174169
raise TypeError(
@@ -178,22 +173,23 @@ def template_update_single(
178173
else:
179174
raise TypeError(f"spec_type can be input or output, but {spec_type} provided")
180175
# for inputs that the value is set (so the template is ignored)
181-
if spec_type == "input" and isinstance(inputs_dict_st[field.name], (str, Path)):
182-
return inputs_dict_st[field.name]
183-
elif spec_type == "input" and inputs_dict_st[field.name] is False:
184-
# if input fld is set to False, the fld shouldn't be used (setting NOTHING)
185-
return attr.NOTHING
186-
else: # inputs_dict[field.name] is True or spec_type is output
187-
value = _template_formatting(field, inputs, inputs_dict_st)
188-
# changing path so it is in the output_dir
189-
if output_dir and value is not attr.NOTHING:
190-
# should be converted to str, it is also used for input fields that should be str
191-
if type(value) is list:
192-
return [str(output_dir / Path(val).name) for val in value]
193-
else:
194-
return str(output_dir / Path(value).name)
195-
else:
176+
if spec_type == "input":
177+
if isinstance(inp_val_set, (Path, list)):
178+
return inp_val_set
179+
if inp_val_set is False:
180+
# if input fld is set to False, the fld shouldn't be used (setting NOTHING)
196181
return attr.NOTHING
182+
# inputs_dict[field.name] is True or spec_type is output
183+
value = _template_formatting(field, inputs, inputs_dict_st)
184+
# changing path so it is in the output_dir
185+
if output_dir and value is not attr.NOTHING:
186+
# should be converted to str, it is also used for input fields that should be str
187+
if type(value) is list:
188+
return [str(output_dir / Path(val).name) for val in value]
189+
else:
190+
return str(output_dir / Path(value).name)
191+
else:
192+
return attr.NOTHING
197193

198194

199195
def _template_formatting(field, inputs, inputs_dict_st):
@@ -204,16 +200,27 @@ def _template_formatting(field, inputs, inputs_dict_st):
204200
Allowing for multiple input values used in the template as longs as
205201
there is no more than one file (i.e. File, PathLike or string with extensions)
206202
"""
207-
from .specs import MultiInputObj, MultiOutputFile
208-
209203
# if a template is a function it has to be run first with the inputs as the only arg
210204
template = field.metadata["output_file_template"]
211205
if callable(template):
212206
template = template(inputs)
213207

214208
# as default, we assume that keep_extension is True
215-
keep_extension = field.metadata.get("keep_extension", True)
209+
if isinstance(template, (tuple, list)):
210+
formatted = [
211+
_string_template_formatting(field, t, inputs, inputs_dict_st)
212+
for t in template
213+
]
214+
else:
215+
assert isinstance(template, str)
216+
formatted = _string_template_formatting(field, template, inputs, inputs_dict_st)
217+
return formatted
218+
216219

220+
def _string_template_formatting(field, template, inputs, inputs_dict_st):
221+
from .specs import MultiInputObj, MultiOutputFile
222+
223+
keep_extension = field.metadata.get("keep_extension", True)
217224
inp_fields = re.findall(r"{\w+}", template)
218225
inp_fields_fl = re.findall(r"{\w+:[0-9.]+f}", template)
219226
inp_fields += [re.sub(":[0-9.]+f", "", el) for el in inp_fields_fl]

pydra/engine/specs.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ class MultiOutputType:
4646
MultiOutputObj = ty.Union[list, object, MultiOutputType]
4747
MultiOutputFile = ty.Union[File, ty.List[File], MultiOutputType]
4848

49+
OUTPUT_TEMPLATE_TYPES = (
50+
Path,
51+
ty.List[Path],
52+
ty.Union[Path, bool],
53+
ty.Union[ty.List[Path], bool],
54+
ty.List[ty.List[Path]],
55+
)
56+
4957

5058
@attr.s(auto_attribs=True, kw_only=True)
5159
class SpecInfo:
@@ -343,6 +351,8 @@ def check_metadata(self):
343351
Also sets the default values when available and needed.
344352
345353
"""
354+
from ..utils.typing import TypeParser
355+
346356
supported_keys = {
347357
"allowed_values",
348358
"argstr",
@@ -361,6 +371,7 @@ def check_metadata(self):
361371
"formatter",
362372
"_output_type",
363373
}
374+
364375
for fld in attr_fields(self, exclude_names=("_func", "_graph_checksums")):
365376
mdata = fld.metadata
366377
# checking keys from metadata
@@ -377,16 +388,13 @@ def check_metadata(self):
377388
)
378389
# assuming that fields with output_file_template shouldn't have default
379390
if mdata.get("output_file_template"):
380-
if fld.type not in (
381-
Path,
382-
ty.Union[Path, bool],
383-
str,
384-
ty.Union[str, bool],
391+
if not any(
392+
TypeParser.matches_type(fld.type, t) for t in OUTPUT_TEMPLATE_TYPES
385393
):
386394
raise TypeError(
387-
f"Type of '{fld.name}' should be either pathlib.Path or "
388-
f"typing.Union[pathlib.Path, bool] (not {fld.type}) because "
389-
f"it has a value for output_file_template ({mdata['output_file_template']!r})"
395+
f"Type of '{fld.name}' should be one of {OUTPUT_TEMPLATE_TYPES} "
396+
f"(not {fld.type}) because it has a value for output_file_template "
397+
f"({mdata['output_file_template']!r})"
390398
)
391399
if fld.default not in [attr.NOTHING, True, False]:
392400
raise AttributeError(
@@ -443,7 +451,8 @@ def collect_additional_outputs(self, inputs, output_dir, outputs):
443451
input_value = getattr(inputs, fld.name, attr.NOTHING)
444452
if input_value is not attr.NOTHING:
445453
if TypeParser.contains_type(FileSet, fld.type):
446-
input_value = TypeParser(fld.type).coerce(input_value)
454+
label = f"output field '{fld.name}' of {self}"
455+
input_value = TypeParser(fld.type, label=label).coerce(input_value)
447456
additional_out[fld.name] = input_value
448457
elif (
449458
fld.default is None or fld.default == attr.NOTHING

pydra/engine/task.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
<https://colab.research.google.com/drive/1RRV1gHbGJs49qQB1q1d5tQEycVRtuhw6>`__
3939
4040
"""
41+
from __future__ import annotations
42+
4143
import platform
4244
import re
4345
import attr
@@ -62,7 +64,6 @@
6264
)
6365
from .helpers import (
6466
ensure_list,
65-
execute,
6667
position_sort,
6768
argstr_formatting,
6869
output_from_inputfields,
@@ -354,7 +355,7 @@ def get_bindings(self, root: str | None = None) -> dict[str, tuple[str, str]]:
354355
if root is None:
355356
return {}
356357
else:
357-
self._check_inputs(root=root)
358+
self._prepare_bindings(root=root)
358359
return self.bindings
359360

360361
def command_args(self, root=None):
@@ -574,29 +575,7 @@ def cmdline(self):
574575
def _run_task(self, environment=None):
575576
if environment is None:
576577
environment = self.environment
577-
578-
if (
579-
environment == "old"
580-
): # TODO this is just temporarily for testing, remove this part
581-
if isinstance(self, ContainerTask):
582-
args = self.container_args + self.command_args()
583-
else:
584-
args = self.command_args()
585-
if args:
586-
# removing empty strings
587-
args = [str(el) for el in args if el not in ["", " "]]
588-
keys = ["return_code", "stdout", "stderr"]
589-
values = execute(args, strip=self.strip)
590-
self.output_ = dict(zip(keys, values))
591-
if self.output_["return_code"]:
592-
msg = f"Error running '{self.name}' task with {args}:"
593-
if self.output_["stderr"]:
594-
msg += "\n\nstderr:\n" + self.output_["stderr"]
595-
if self.output_["stdout"]:
596-
msg += "\n\nstdout:\n" + self.output_["stdout"]
597-
raise RuntimeError(msg)
598-
else:
599-
self.output_ = environment.execute(self)
578+
self.output_ = environment.execute(self)
600579

601580
def _prepare_bindings(self, root: str):
602581
"""Prepare input files to be passed to the task
@@ -721,7 +700,7 @@ def bind_paths(self):
721700
mount points: dict
722701
mapping from local path to tuple of container path + mode
723702
"""
724-
self._check_inputs()
703+
self._prepare_bindings()
725704
return {**self.bindings, **{self.output_dir: (self.output_cpath, "rw")}}
726705

727706
def binds(self, opt):
@@ -736,7 +715,7 @@ def binds(self, opt):
736715
bargs.extend([opt, f"{lpath}:{cpath}:{mode}"])
737716
return bargs
738717

739-
def _check_inputs(self):
718+
def _prepare_bindings(self):
740719
fields = attr_fields(self.inputs)
741720
for fld in fields:
742721
if TypeParser.contains_type(FileSet, fld.type):

pydra/engine/tests/test_dockertask.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ def test_docker_1_nosubm():
1717
no submitter
1818
"""
1919
cmd = "whoami"
20-
docky = DockerTask(name="docky", executable=cmd, image="busybox", environment="old")
21-
assert docky.inputs.image == "busybox"
22-
assert docky.inputs.container == "docker"
23-
assert (
24-
docky.cmdline
25-
== f"docker run --rm -v {docky.output_dir}:/output_pydra:rw -w /output_pydra {docky.inputs.image} {cmd}"
20+
docky = ShellCommandTask(
21+
name="docky", executable=cmd, environment=Docker(image="busybox")
2622
)
23+
assert docky.environment.image == "busybox"
24+
assert docky.environment.tag == "latest"
25+
assert isinstance(docky.environment, Docker)
26+
assert docky.cmdline == cmd
2727

2828
res = docky()
2929
assert res.output.stdout == "root\n"

pydra/engine/tests/test_environments.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,6 @@ def test_native_1(tmp_path):
4646
shelly_subm(submitter=sub, environment=Native())
4747
assert env_res == shelly_subm.result().output.__dict__
4848

49-
# TODO: should be removed at the end
50-
shelly_old = ShellCommandTask(
51-
name="shelly_old", executable=cmd, cache_dir=tmp_path, environment="old"
52-
)
53-
shelly_old()
54-
assert env_res == shelly_old.result().output.__dict__
55-
5649

5750
@no_win
5851
@need_docker

0 commit comments

Comments
 (0)