Skip to content

Commit 56d89d6

Browse files
authored
Merge branch 'master' into sfc
2 parents c9207a1 + 9f3b9e7 commit 56d89d6

File tree

153 files changed

+65422
-105
lines changed

Some content is hidden

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

153 files changed

+65422
-105
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Generated during tests
2+
pytestdebug.log
3+
tmp/
4+
15
# Python temps
26
__pycache__/
37
*.py[cod]

.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/argparser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ def generate_parser(
799799
records: List[str],
800800
input_required: bool = True,
801801
) -> argparse.ArgumentParser:
802+
toolparser.description = tool.tool.get("doc", None)
802803
toolparser.add_argument("job_order", nargs="?", help="Job input json file")
803804
namemap["job_order"] = "job_order"
804805

cwltool/checker.py

Lines changed: 108 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,93 @@ 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]], parm_id: str) -> bool:
401+
source_step = param_to_step.get(parm_id)
402+
if source_step is not None:
403+
if source_step.get("when") is not None:
404+
return True
405+
return False

cwltool/command_line_tool.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,17 @@ def job(
162162
yield job
163163

164164

165+
class AbstractOperation(Process):
166+
def job(
167+
self,
168+
job_order, # type: Mapping[str, str]
169+
output_callbacks, # type: Callable[[Any, Any], Any]
170+
runtimeContext, # type: RuntimeContext
171+
):
172+
# type: (...) -> Generator[ExpressionTool.ExpressionJob, None, None]
173+
raise WorkflowException("Abstract operation cannot be executed.")
174+
175+
165176
def remove_path(f): # type: (Dict[str, Any]) -> None
166177
if "path" in f:
167178
del f["path"]

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/docker.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Enables Docker software containers via the {dx-,u,}docker runtimes."""
22

33
import datetime
4+
import csv
45
import os
56
import re
67
import shutil
78
import sys
89
import tempfile
910
import threading
1011
from distutils import spawn
11-
from io import open # pylint: disable=redefined-builtin
12+
from io import open, StringIO # pylint: disable=redefined-builtin
1213
from typing import Dict, List, MutableMapping, Optional, Set, Tuple
1314

1415
import requests
@@ -222,11 +223,20 @@ def get_from_requirements(
222223
def append_volume(runtime, source, target, writable=False):
223224
# type: (List[str], str, str, bool) -> None
224225
"""Add binding arguments to the runtime list."""
225-
runtime.append(
226-
"--volume={}:{}:{}".format(
227-
docker_windows_path_adjust(source), target, "rw" if writable else "ro"
228-
)
229-
)
226+
options = [
227+
"type=bind",
228+
"source=" + source,
229+
"target=" + target,
230+
]
231+
if not writable:
232+
options.append("readonly")
233+
output = StringIO()
234+
csv.writer(output).writerow(options)
235+
mount_arg = output.getvalue().strip()
236+
runtime.append("--mount={}".format(mount_arg))
237+
# Unlike "--volume", "--mount" will fail if the volume doesn't already exist.
238+
if not os.path.exists(source):
239+
os.mkdir(source)
230240

231241
def add_file_or_directory_volume(
232242
self, runtime: List[str], volume: MapperEnt, host_outdir_tgt: Optional[str]

cwltool/executors.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,22 @@
77
import threading
88
from abc import ABCMeta, abstractmethod
99
from threading import Lock
10-
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
10+
from typing import (
11+
Any,
12+
Dict,
13+
Iterable,
14+
List,
15+
Optional,
16+
Set,
17+
Tuple,
18+
Union,
19+
MutableMapping,
20+
)
1121

1222
import psutil
1323

1424
from schema_salad.validate import ValidationException
25+
from schema_salad.sourceline import SourceLine
1526

1627
from .command_line_tool import CallbackJob
1728
from .context import RuntimeContext, getdefault
@@ -68,6 +79,14 @@ def execute(
6879
if not runtime_context.basedir:
6980
raise WorkflowException("Must provide 'basedir' in runtimeContext")
7081

82+
def check_for_abstract_op(tool: MutableMapping[str, Any]) -> None:
83+
if tool["class"] == "Operation":
84+
raise SourceLine(tool, "class", WorkflowException).makeError(
85+
"Workflow has unrunnable abstract Operation"
86+
)
87+
88+
process.visit(check_for_abstract_op)
89+
7190
finaloutdir = None # Type: Optional[str]
7291
original_outdir = runtime_context.outdir
7392
if isinstance(original_outdir, str):

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.

0 commit comments

Comments
 (0)