Skip to content

Commit b3c4e67

Browse files
committed
Add CWL v1.0.x to CWL v1.1 conversion
All but three of the CWL v1.0 conformance tests still pass after conversion. To do: support $import / $include
1 parent e06bf4e commit b3c4e67

File tree

2 files changed

+119
-6
lines changed

2 files changed

+119
-6
lines changed

cwlupgrader/main.py

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""Transforms draft-3 CWL documents into v1.0 as idiomatically as possible."""
33

44
from __future__ import print_function
5-
from collections import Mapping, MutableMapping, Sequence
5+
from collections import Mapping, MutableMapping, MutableSequence, Sequence
66
import sys
77
import copy
88
from typing import (Any, Dict, List, Optional, # pylint:disable=unused-import
@@ -18,16 +18,33 @@ def main(args=None): # type: (Optional[List[str]]) -> int
1818
for path in args:
1919
with open(path) as entry:
2020
document = ruamel.yaml.safe_load(entry)
21-
if ('cwlVersion' in document
22-
and (document['cwlVersion'] == 'cwl:draft-3'
23-
or document['cwlVersion'] == 'draft-3')):
24-
document = draft3_to_v1_0(document)
21+
if 'cwlVersion' in document:
22+
if (document['cwlVersion'] == 'cwl:draft-3'
23+
or document['cwlVersion'] == 'draft-3'):
24+
document = draft3_to_v1_0(document)
25+
elif document['cwlVersion'] == 'v1.0':
26+
document = v1_0_to_v1_1(document)
2527
else:
2628
print("Skipping non draft-3 CWL document", file=sys.stderr)
2729
print(ruamel.yaml.round_trip_dump(
2830
document, default_flow_style=False))
2931
return 0
3032

33+
def v1_0_to_v1_1(document): # type: (Dict[Text, Any]) -> Dict
34+
"""CWL v1.0.x to v1.1 transformation loop."""
35+
# TODO: handle $import, see imported-hint.cwl schemadef-tool.cwl schemadef-wf.cwl
36+
_v1_0_to_v1_1(document)
37+
if isinstance(document, MutableMapping):
38+
for key, value in document.items():
39+
if isinstance(value, MutableMapping):
40+
document[key] = _v1_0_to_v1_1(value)
41+
elif isinstance(value, list):
42+
for index, entry in enumerate(value):
43+
if isinstance(entry, MutableMapping):
44+
value[index] = _v1_0_to_v1_1(entry)
45+
document['cwlVersion'] = 'v1.1'
46+
return sort_v1_0(document)
47+
3148

3249
def draft3_to_v1_0(document): # type: (Dict[Text, Any]) -> Dict
3350
"""Transformation loop."""
@@ -67,6 +84,102 @@ def _draft3_to_v1_0(document):
6784

6885
return document
6986

87+
WORKFLOW_INPUT_INPUTBINDING = \
88+
"{}[cwl-upgrader_v1_0_to_v1_1] Original input had the follow (unused) inputBinding element: {}"
89+
90+
V1_0_TO_V1_1_REWRITE = {
91+
"http://commonwl.org/cwltool#WorkReuse": "WorkReuse",
92+
"http://arvados.org/cwl#ReuseRequirement": "WorkReuse",
93+
"http://commonwl.org/cwltool#TimeLimit": "ToolTimeLimit",
94+
"http://commonwl.org/cwltool#NetworkAccess": "NetworkAccess",
95+
"http://commonwl.org/cwltool#InplaceUpdateRequirement": "InplaceUpdateRequirement",
96+
"http://commonwl.org/cwltool#LoadListingRequirement": "LoadListingRequirement"
97+
}
98+
99+
100+
def _v1_0_to_v1_1(document):
101+
# type: (MutableMapping[Text, Any]) -> MutableMapping[Text, Any]
102+
"""Inner loop for transforming draft-3 to v1.0."""
103+
if "class" in document:
104+
if document['class'] == 'Workflow':
105+
upgrade_v1_0_hints_and_reqs(document)
106+
cleanup_v1_0_input_bindings(document)
107+
steps = document['steps']
108+
if isinstance(steps, MutableSequence):
109+
for entry in steps:
110+
upgrade_v1_0_hints_and_reqs(entry)
111+
elif isinstance(steps, MutableMapping):
112+
for step_name in steps:
113+
upgrade_v1_0_hints_and_reqs(steps[step_name])
114+
elif document['class'] == 'CommandLineTool':
115+
upgrade_v1_0_hints_and_reqs(document)
116+
network_access = has_hint_or_req(document, "NetworkAccess")
117+
listing = has_hint_or_req(document, "LoadListingRequirement")
118+
if 'hints' in document:
119+
if isinstance(document['hints'], MutableSequence):
120+
if not network_access:
121+
document['hints'].append({"class": "NetworkAcess", "networkAccess": True})
122+
if not listing:
123+
document['hints'].append({"class": "LoadListingRequirement", "loadListing": "deep_listing"})
124+
elif isinstance(document['hints'], MutableMapping):
125+
if not network_access:
126+
document['hints']["NetworkAcess"] = {"networkAccess": True}
127+
if not listing:
128+
document['hints']["LoadListingRequirement"] = {"loadListing": "deep_listing"}
129+
elif document['class'] == 'ExpressionTool':
130+
cleanup_v1_0_input_bindings(document)
131+
return document
132+
133+
134+
def cleanup_v1_0_input_bindings(document):
135+
"""In v1.1 only loadContents is allow in inputs for a Workflow or ExpressionTool."""
136+
def cleanup(inp):
137+
"""Serialize non loadContents fields and add that to the doc."""
138+
if 'inputBinding' in inp:
139+
bindings = inp["inputBinding"]
140+
for field in list(bindings.keys()):
141+
if field != "loadContents":
142+
prefix = '' if 'doc' not in inp else '{}\n'.format(inp['doc'])
143+
inp['doc'] = WORKFLOW_INPUT_INPUTBINDING.format(prefix, field)
144+
del bindings[field]
145+
if not bindings:
146+
del inp['inputBinding']
147+
inputs = document['inputs']
148+
if isinstance(inputs, MutableSequence):
149+
for entry in inputs:
150+
cleanup(entry)
151+
elif isinstance(inputs, MutableMapping):
152+
for input_name in inputs:
153+
cleanup(inputs[input_name])
154+
155+
156+
def upgrade_v1_0_hints_and_reqs(document):
157+
for extra in ("requirements", "hints"):
158+
if extra in document:
159+
if isinstance(document[extra], MutableMapping):
160+
for req_name in document[extra]:
161+
if req_name in V1_0_TO_V1_1_REWRITE:
162+
extra[V1_0_TO_V1_1_REWRITE[req_name]] = extra.pop(req_name)
163+
elif isinstance(document[extra], MutableSequence):
164+
for entry in document[extra]:
165+
if entry['class'] in V1_0_TO_V1_1_REWRITE:
166+
entry['class'] = V1_0_TO_V1_1_REWRITE[entry['id']]
167+
else:
168+
raise Exception("{} section must be either a list of dictionaries or a dictionary of dictionaries!: {}").format(extra, document[extra])
169+
170+
def has_hint_or_req(document, name):
171+
"""Detects an existing named hint or requirement."""
172+
for extra in ("requirements", "hints"):
173+
if extra in document:
174+
if isinstance(document[extra], MutableMapping):
175+
if name in document[extra]:
176+
return True
177+
elif isinstance(document[extra], MutableSequence):
178+
for entry in document[extra]:
179+
if entry['class'] == name:
180+
return True
181+
return False
182+
70183

71184
def workflow_clean(document): # type: (MutableMapping[Text, Any]) -> None
72185
"""Transform draft-3 style Workflows to more idiomatic v1.0"""

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
package_dir={'cwlupgrader.tests': 'tests'},
2525
install_requires=[
2626
'setuptools',
27-
'ruamel.yaml >= 0.14.12, < 0.15',
27+
'ruamel.yaml >= 0.14.12, <= 0.15.97',
2828
'typing'],
2929
entry_points={
3030
'console_scripts': ["cwl-upgrader = cwlupgrader.main:main"]

0 commit comments

Comments
 (0)