Skip to content

Commit 710e414

Browse files
committed
Evaluate hints and resources at the workflow level, using the correct inputs.
1 parent 0e2ced5 commit 710e414

File tree

4 files changed

+168
-0
lines changed

4 files changed

+168
-0
lines changed

cwltool/workflow.py

+68
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
aslist,
3636
)
3737
from .workflow_job import WorkflowJob
38+
from cwl_utils import expression
3839

3940

4041
def default_make_tool(
@@ -156,6 +157,73 @@ def job(
156157
output_callbacks: Optional[OutputCallbackType],
157158
runtimeContext: RuntimeContext,
158159
) -> JobsGeneratorType:
160+
# TODO: This is not very efficient and could be improved.
161+
#
162+
# See issue #1330. We needed to evaluate the requirements
163+
# at the workflow level, so that it was not re-evaluated at
164+
# each step level (since a command-line tool, for instance,
165+
# won't have the inputs from args, but instead will have the
166+
# empty/default inputs of a workflow step).
167+
#
168+
# The solution below evaluates the requirements and hints for
169+
# the workflow (parent), keeping track of the name of the
170+
# requirements and hints. For each step of the workflow and of
171+
# the embedded tool (command-line or expression tools) it will
172+
# then evaluate the requirements or hints that have the same
173+
# name - even though they may be re-evaluated at the step
174+
# level (e.g. a workflow defines a requirement resource that
175+
# uses inputs.threads_max, and a command-line tool of the same
176+
# workflow also defines a requirement with the same name, but
177+
# using the command-line tool input values).
178+
#
179+
# This prevents evaluation at the step level (i.e. the values
180+
# were already loaded earlier).
181+
def _fix_hints_and_requirements(
182+
hints_or_requirements: List[CWLObjectType],
183+
requirements_or_hints_to_evaluate: List[str],
184+
) -> None:
185+
"""Fix hints and requirements of a workflow.
186+
187+
Internal function to iterate the hints or requirements
188+
of steps provided and evaluate the ones that exist in
189+
the parent process.
190+
"""
191+
for hint_or_requirement in hints_or_requirements:
192+
for key, value in hint_or_requirement.items():
193+
if key in requirements_or_hints_to_evaluate:
194+
hint_or_requirement[key] = expression.do_eval(
195+
ex=value,
196+
jobinput=job_order,
197+
requirements=self.requirements,
198+
outdir=runtimeContext.outdir,
199+
tmpdir=runtimeContext.tmpdir,
200+
resources={},
201+
context=None,
202+
timeout=runtimeContext.eval_timeout,
203+
)
204+
205+
for attr_key in ["hints", "requirements"]:
206+
parent_entries = []
207+
for hint_or_requirement in getattr(self, attr_key):
208+
for key, value in hint_or_requirement.items():
209+
hint_or_requirement[key] = expression.do_eval(
210+
ex=value,
211+
jobinput=job_order,
212+
requirements=self.requirements,
213+
outdir=runtimeContext.outdir,
214+
tmpdir=runtimeContext.tmpdir,
215+
resources={},
216+
context=None,
217+
timeout=runtimeContext.eval_timeout,
218+
)
219+
parent_entries.append(key)
220+
221+
for step in self.steps:
222+
_fix_hints_and_requirements(getattr(step, attr_key), parent_entries)
223+
_fix_hints_and_requirements(
224+
getattr(step.embedded_tool, attr_key), parent_entries
225+
)
226+
159227
builder = self._init_job(job_order, runtimeContext)
160228

161229
if runtimeContext.research_obj is not None:

tests/test_reqs_hints.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Test for Requirements and Hints in cwltool."""
2+
import json
3+
from io import StringIO
4+
5+
from cwltool.main import main
6+
from .util import get_data
7+
8+
9+
def test_workflow_reqs_are_evaluated_earlier_default_args() -> None:
10+
"""Test that a Workflow process will evaluate the requirements earlier.
11+
12+
Uses the default input values.
13+
14+
This means that workflow steps, such as Expression and Command Line Tools
15+
can both use resources without re-evaluating expressions. This is useful
16+
when you have an expression that, for instance, dynamically decides
17+
how many threads/cpus to use.
18+
19+
Issue: https://github.com/common-workflow-language/cwltool/issues/1330
20+
"""
21+
stream = StringIO()
22+
23+
assert (
24+
main(
25+
[get_data("tests/wf/1330.cwl")],
26+
stdout=stream,
27+
)
28+
== 0
29+
)
30+
31+
out = json.loads(stream.getvalue())
32+
assert out["out"] == "2\n"
33+
34+
35+
def test_workflow_reqs_are_evaluated_earlier_provided_inputs() -> None:
36+
"""Test that a Workflow process will evaluate the requirements earlier.
37+
38+
Passes inputs via a job file.
39+
40+
This means that workflow steps, such as Expression and Command Line Tools
41+
can both use resources without re-evaluating expressions. This is useful
42+
when you have an expression that, for instance, dynamically decides
43+
how many threads/cpus to use.
44+
45+
Issue: https://github.com/common-workflow-language/cwltool/issues/1330
46+
"""
47+
stream = StringIO()
48+
49+
assert (
50+
main(
51+
[get_data("tests/wf/1330.cwl"), get_data("tests/wf/1330.json")],
52+
stdout=stream,
53+
)
54+
== 0
55+
)
56+
57+
out = json.loads(stream.getvalue())
58+
assert out["out"] == "1\n"

tests/wf/1330.cwl

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Original file: tests/eager-eval-reqs-hints/wf-reqs.cwl
2+
# From: https://github.com/common-workflow-language/cwl-v1.2/pull/195
3+
cwlVersion: v1.2
4+
class: Workflow
5+
6+
requirements:
7+
ResourceRequirement:
8+
coresMax: $(inputs.threads_max)
9+
10+
inputs:
11+
threads_max:
12+
type: int
13+
default: 2
14+
15+
steps:
16+
one:
17+
in: []
18+
run:
19+
class: CommandLineTool
20+
inputs:
21+
other_input:
22+
type: int
23+
default: 8
24+
baseCommand: echo
25+
arguments: [ $(runtime.cores) ]
26+
stdout: out.txt
27+
outputs:
28+
out:
29+
type: string
30+
outputBinding:
31+
glob: out.txt
32+
loadContents: true
33+
outputEval: $(self[0].contents)
34+
out: [out]
35+
36+
outputs:
37+
out:
38+
type: string
39+
outputSource: one/out

tests/wf/1330.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"threads_max": 1
3+
}

0 commit comments

Comments
 (0)