Skip to content

Commit d32d569

Browse files
authored
Conditionals (#1222)
CWLtool will parse and load v1.2.0-dev1 documents with conditional features `when` and `pickValue` Workflow checker checks expressions before warning about unconnected inputs. Run CWL 1.2 conformance tests on travis v1.2.0-dev1 schema is db3b07c718e51b3d13d22763b52ff90ec60edd8b
1 parent a3d565b commit d32d569

File tree

136 files changed

+65117
-98
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

136 files changed

+65117
-98
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ jobs:
3434
env:
3535
- version=v1.1
3636
script: ${TRAVIS_BUILD_DIR}/travis.bash
37+
- python: "3.8"
38+
name: "CWL v1.2 conformance tests"
39+
env:
40+
- version=v1.2.0-dev1
41+
script: ${TRAVIS_BUILD_DIR}/travis.bash
3742
- python: "3.7"
3843
script: RELEASE_SKIP=head ${TRAVIS_BUILD_DIR}/release-test.sh
3944
script: tox

MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ include cwltool/schemas/v1.1.0-dev1/*.yml
2424
include cwltool/schemas/v1.1.0-dev1/*.md
2525
include cwltool/schemas/v1.1.0-dev1/salad/schema_salad/metaschema/*.yml
2626
include cwltool/schemas/v1.1.0-dev1/salad/schema_salad/metaschema/*.md
27+
include cwltool/schemas/v1.2.0-dev1/*.yml
28+
include cwltool/schemas/v1.2.0-dev1/*.md
29+
include cwltool/schemas/v1.2.0-dev1/salad/schema_salad/metaschema/*.yml
30+
include cwltool/schemas/v1.2.0-dev1/salad/schema_salad/metaschema/*.md
2731
include cwltool/cwlNodeEngine.js
2832
include cwltool/cwlNodeEngineJSConsole.js
2933
include cwltool/cwlNodeEngineWithContext.js

cwltool/checker.py

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,10 @@ def static_checker(
166166
for parm in src_parms:
167167
src_dict[parm["id"]] = parm
168168

169-
step_inputs_val = check_all_types(src_dict, step_inputs, "source")
170-
workflow_outputs_val = check_all_types(src_dict, workflow_outputs, "outputSource")
169+
step_inputs_val = check_all_types(src_dict, step_inputs, "source", param_to_step)
170+
workflow_outputs_val = check_all_types(
171+
src_dict, workflow_outputs, "outputSource", param_to_step
172+
)
171173

172174
warnings = step_inputs_val["warning"] + workflow_outputs_val["warning"]
173175
exceptions = step_inputs_val["exception"] + workflow_outputs_val["exception"]
@@ -212,18 +214,21 @@ def static_checker(
212214
"%s\n%s" % (msg1, bullets([msg3, msg4, msg5], " "))
213215
)
214216
elif sink.get("not_connected"):
215-
msg = SourceLine(sink, "type").makeError(
216-
"'%s' is not an input parameter of %s, expected %s"
217-
% (
218-
shortname(sink["id"]),
219-
param_to_step[sink["id"]]["run"],
220-
", ".join(
221-
shortname(s["id"])
222-
for s in param_to_step[sink["id"]]["inputs"]
223-
if not s.get("not_connected")
224-
),
217+
if not sink.get("used_by_step"):
218+
msg = SourceLine(sink, "type").makeError(
219+
"'%s' is not an input parameter of %s, expected %s"
220+
% (
221+
shortname(sink["id"]),
222+
param_to_step[sink["id"]]["run"],
223+
", ".join(
224+
shortname(s["id"])
225+
for s in param_to_step[sink["id"]]["inputs"]
226+
if not s.get("not_connected")
227+
),
228+
)
225229
)
226-
)
230+
else:
231+
msg = ""
227232
else:
228233
msg = (
229234
SourceLine(src, "type").makeError(
@@ -241,11 +246,17 @@ def static_checker(
241246
" source has linkMerge method %s" % linkMerge
242247
)
243248

244-
warning_msgs.append(msg)
249+
if warning.message is not None:
250+
msg += "\n" + SourceLine(sink).makeError(" " + warning.message)
251+
252+
if msg:
253+
warning_msgs.append(msg)
254+
245255
for exception in exceptions:
246256
src = exception.src
247257
sink = exception.sink
248258
linkMerge = exception.linkMerge
259+
extra_message = exception.message
249260
msg = (
250261
SourceLine(src, "type").makeError(
251262
"Source '%s' of type %s is incompatible"
@@ -257,6 +268,9 @@ def static_checker(
257268
% (shortname(sink["id"]), json_dumps(sink["type"]))
258269
)
259270
)
271+
if extra_message is not None:
272+
msg += "\n" + SourceLine(sink).makeError(" " + extra_message)
273+
260274
if linkMerge is not None:
261275
msg += "\n" + SourceLine(sink).makeError(
262276
" source has linkMerge method %s" % linkMerge
@@ -278,19 +292,19 @@ def static_checker(
278292
exception_msgs.append(msg)
279293

280294
all_warning_msg = strip_dup_lineno("\n".join(warning_msgs))
281-
all_exception_msg = strip_dup_lineno("\n".join(exception_msgs))
295+
all_exception_msg = strip_dup_lineno("\n" + "\n".join(exception_msgs))
282296

283-
if warnings:
297+
if all_warning_msg:
284298
_logger.warning("Workflow checker warning:\n%s", all_warning_msg)
285299
if exceptions:
286300
raise validate.ValidationException(all_exception_msg)
287301

288302

289-
SrcSink = namedtuple("SrcSink", ["src", "sink", "linkMerge"])
303+
SrcSink = namedtuple("SrcSink", ["src", "sink", "linkMerge", "message"])
290304

291305

292-
def check_all_types(src_dict, sinks, sourceField):
293-
# type: (Dict[str, Any], List[Dict[str, Any]], str) -> Dict[str, List[SrcSink]]
306+
def check_all_types(src_dict, sinks, sourceField, param_to_step):
307+
# type: (Dict[str, Any], List[Dict[str, Any]], str, Dict[str, Dict[str, Any]]) -> Dict[str, List[SrcSink]]
294308
"""
295309
Given a list of sinks, check if their types match with the types of their sources.
296310
@@ -299,21 +313,94 @@ def check_all_types(src_dict, sinks, sourceField):
299313
validation = {"warning": [], "exception": []} # type: Dict[str, List[SrcSink]]
300314
for sink in sinks:
301315
if sourceField in sink:
316+
302317
valueFrom = sink.get("valueFrom")
318+
pickValue = sink.get("pickValue")
319+
320+
extra_message = None
321+
if pickValue is not None:
322+
extra_message = "pickValue is: %s" % pickValue
323+
303324
if isinstance(sink[sourceField], MutableSequence):
304-
srcs_of_sink = [src_dict[parm_id] for parm_id in sink[sourceField]]
305325
linkMerge = sink.get(
306326
"linkMerge",
307327
("merge_nested" if len(sink[sourceField]) > 1 else None),
308328
)
329+
330+
if pickValue in ["first_non_null", "only_non_null"]:
331+
linkMerge = None
332+
333+
srcs_of_sink = [] # type: List[Any]
334+
for parm_id in sink[sourceField]:
335+
srcs_of_sink += [src_dict[parm_id]]
336+
if (
337+
is_conditional_step(param_to_step, parm_id)
338+
and pickValue is None
339+
):
340+
validation["warning"].append(
341+
SrcSink(
342+
src_dict[parm_id],
343+
sink,
344+
linkMerge,
345+
message="Source is from conditional step, but pickValue is not used",
346+
)
347+
)
309348
else:
310349
parm_id = sink[sourceField]
311350
srcs_of_sink = [src_dict[parm_id]]
312351
linkMerge = None
352+
353+
if pickValue is not None:
354+
validation["warning"].append(
355+
SrcSink(
356+
src_dict[parm_id],
357+
sink,
358+
linkMerge,
359+
message="pickValue is used but only a single input source is declared",
360+
)
361+
)
362+
363+
if is_conditional_step(param_to_step, parm_id):
364+
src_typ = srcs_of_sink[0]["type"]
365+
snk_typ = sink["type"]
366+
367+
if not isinstance(src_typ, list):
368+
src_typ = [src_typ]
369+
if "null" not in src_typ:
370+
src_typ = ["null"] + src_typ
371+
372+
if (
373+
"null" not in snk_typ
374+
): # Given our type names this works even if not a list
375+
validation["warning"].append(
376+
SrcSink(
377+
src_dict[parm_id],
378+
sink,
379+
linkMerge,
380+
message="Source is from conditional step and may produce `null`",
381+
)
382+
)
383+
384+
srcs_of_sink[0]["type"] = src_typ
385+
313386
for src in srcs_of_sink:
314387
check_result = check_types(src, sink, linkMerge, valueFrom)
315388
if check_result == "warning":
316-
validation["warning"].append(SrcSink(src, sink, linkMerge))
389+
validation["warning"].append(
390+
SrcSink(src, sink, linkMerge, message=extra_message)
391+
)
317392
elif check_result == "exception":
318-
validation["exception"].append(SrcSink(src, sink, linkMerge))
393+
validation["exception"].append(
394+
SrcSink(src, sink, linkMerge, message=extra_message)
395+
)
396+
319397
return validation
398+
399+
400+
def is_conditional_step(param_to_step: Dict[str, Dict[str, Any]],
401+
parm_id: str) -> bool:
402+
source_step = param_to_step.get(parm_id)
403+
if source_step is not None:
404+
if source_step.get("when") is not None:
405+
return True
406+
return False

cwltool/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def __init__(self, kwargs: Optional[Dict[str, Any]] = None) -> None:
132132
self.eval_timeout = 20 # type: float
133133
self.postScatterEval = (
134134
None
135-
) # type: Optional[Callable[[MutableMapping[str, Any]], Dict[str, Any]]]
135+
) # type: Optional[Callable[[MutableMapping[str, Any]], Optional[MutableMapping[str, Any]]]]
136136
self.on_error = "stop" # type: str
137137
self.strict_memory_limit = False # type: bool
138138

cwltool/job.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,9 @@ def shouldquote(x: Any) -> bool:
433433
_logger.info("[job %s] completed %s", self.name, processStatus)
434434

435435
if _logger.isEnabledFor(logging.DEBUG):
436-
_logger.debug("[job %s] %s", self.name, json_dumps(outputs, indent=4))
436+
_logger.debug(
437+
"[job %s] outputs %s", self.name, json_dumps(outputs, indent=4)
438+
)
437439

438440
if self.generatemapper is not None and runtimeContext.secret_store is not None:
439441
# Delete any runtime-generated files containing secrets.

cwltool/provenance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,7 @@ def used_artefacts(
864864

865865
def generate_output_prov(
866866
self,
867-
final_output: Union[Dict[str, Any], List[Dict[str, Any]]],
867+
final_output: Union[MutableMapping[str, Any], List[Dict[str, Any]]],
868868
process_run_id: Optional[str],
869869
name: Optional[str],
870870
) -> None:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- $import: Process.yml
2+
- $import: CommandLineTool.yml

0 commit comments

Comments
 (0)