Skip to content

Commit 55f0dd9

Browse files
authored
Merge pull request #869 from common-workflow-language/validate_quiet
massively improve input object template generator
2 parents 1a83925 + 4712c1c commit 55f0dd9

File tree

1 file changed

+128
-47
lines changed

1 file changed

+128
-47
lines changed

cwltool/main.py

Lines changed: 128 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python
2+
"""Entry point for cwltool."""
23
from __future__ import absolute_import, print_function
34

45
import argparse
@@ -14,7 +15,8 @@
1415
import sys
1516

1617
from typing import (IO, Any, Callable, Dict, Iterable, List, Mapping,
17-
MutableMapping, Optional, TextIO, Tuple, Union, cast)
18+
MutableMapping, MutableSequence, Optional, TextIO, Tuple,
19+
Union, cast)
1820
from typing_extensions import Text # pylint: disable=unused-import
1921
# move to a regular typing import when Python 3.3-3.6 is no longer supported
2022
import pkg_resources # part of setuptools
@@ -86,56 +88,137 @@ def _signal_handler(signum, _):
8688
sys.exit(signum)
8789

8890

89-
def generate_example_input(inptype):
90-
# type: (Union[Text, Dict[Text, Any]]) -> Any
91+
def generate_example_input(inptype, # type: Any
92+
default # type: Optional[Any]
93+
): # type: (...) -> Tuple[Any, Text]
94+
"""Converts a single input schema into an example."""
95+
example = None
96+
comment = u""
9197
defaults = {u'null': 'null',
9298
u'Any': 'null',
9399
u'boolean': False,
94100
u'int': 0,
95101
u'long': 0,
96102
u'float': 0.1,
97103
u'double': 0.1,
98-
u'string': 'default_string',
99-
u'File': {'class': 'File',
100-
'path': 'default/file/path'},
101-
u'Directory': {'class': 'Directory',
102-
'path': 'default/directory/path'}
104+
u'string': 'a_string',
105+
u'File': yaml.comments.CommentedMap([
106+
('class', 'File'), ('path', 'a/file/path')]),
107+
u'Directory': yaml.comments.CommentedMap([
108+
('class', 'Directory'), ('path', 'a/directory/path')])
103109
} # type: Dict[Text, Any]
104-
if (not isinstance(inptype, string_types) and
105-
not isinstance(inptype, collections.Mapping)
106-
and isinstance(inptype, collections.MutableSet)):
107-
if len(inptype) == 2 and 'null' in inptype:
110+
if isinstance(inptype, collections.MutableSequence):
111+
optional = False
112+
if 'null' in inptype:
108113
inptype.remove('null')
109-
return generate_example_input(inptype[0])
110-
# TODO: indicate that this input is optional
111-
raise Exception("multi-types other than optional not yet supported"
112-
" for generating example input objects: "
113-
"{}".format(inptype))
114-
if isinstance(inptype, collections.Mapping) and 'type' in inptype:
114+
optional = True
115+
if len(inptype) == 1:
116+
example, comment = generate_example_input(inptype[0], default)
117+
if optional:
118+
if comment:
119+
comment = u"{} (optional)".format(comment)
120+
else:
121+
comment = u"optional"
122+
else:
123+
example = yaml.comments.CommentedSeq()
124+
for index, entry in enumerate(inptype):
125+
value, e_comment = generate_example_input(entry, default)
126+
example.append(value)
127+
example.yaml_add_eol_comment(e_comment, index)
128+
if optional:
129+
comment = u"optional"
130+
elif isinstance(inptype, collections.Mapping) and 'type' in inptype:
115131
if inptype['type'] == 'array':
116-
return [generate_example_input(inptype['items'])]
117-
if inptype['type'] == 'enum':
118-
return 'valid_enum_value'
119-
# TODO: list valid values in a comment
120-
if inptype['type'] == 'record':
121-
record = {}
132+
if len(inptype['items']) == 1 and 'type' in inptype['items'][0] \
133+
and inptype['items'][0]['type'] == 'enum':
134+
# array of just an enum then list all the options
135+
example = inptype['items'][0]['symbols']
136+
if 'name' in inptype['items'][0]:
137+
comment = u'array of type "{}".'.format(inptype['items'][0]['name'])
138+
else:
139+
value, comment = generate_example_input(inptype['items'], None)
140+
comment = u"array of " + comment
141+
if len(inptype['items']) == 1:
142+
example = [value]
143+
else:
144+
example = value
145+
if default:
146+
example = default
147+
elif inptype['type'] == 'enum':
148+
if default:
149+
example = default
150+
elif 'default' in inptype:
151+
example = inptype['default']
152+
elif len(inptype['symbols']) == 1:
153+
example = inptype['symbols'][0]
154+
else:
155+
example = '{}_enum_value'.format(inptype.get('name', 'valid'))
156+
comment = u'enum; valid values: "{}"'.format(
157+
'", "'.join(inptype['symbols']))
158+
elif inptype['type'] == 'record':
159+
example = yaml.comments.CommentedMap()
160+
if 'name' in inptype:
161+
comment = u'"{}" record type.'.format(inptype['name'])
122162
for field in inptype['fields']:
123-
record[shortname(field['name'])] = generate_example_input(
124-
field['type'])
125-
return record
126-
elif isinstance(inptype, string_types):
127-
return defaults.get(Text(inptype), 'custom_type')
128-
# TODO: support custom types, complex arrays
129-
return None
130-
163+
value, f_comment = generate_example_input(field['type'], None)
164+
example.insert(0, shortname(field['name']), value, f_comment)
165+
elif 'default' in inptype:
166+
example = inptype['default']
167+
comment = u'default value of type "{}".'.format(inptype['type'])
168+
else:
169+
example = defaults.get(inptype['type'], Text(inptype))
170+
comment = u'type "{}".'.format(inptype['type'])
171+
else:
172+
if not default:
173+
example = defaults.get(Text(inptype), Text(inptype))
174+
comment = u'type "{}"'.format(inptype)
175+
else:
176+
example = default
177+
comment = u'default value of type "{}".'.format(inptype)
178+
return example, comment
179+
180+
def realize_input_schema(input_types, # type: MutableSequence[Dict[Text, Any]]
181+
schema_defs # type: Dict[Text, Any]
182+
): # type: (...) -> MutableSequence[Dict[Text, Any]]
183+
"""Replace references to named typed with the actual types."""
184+
for index, entry in enumerate(input_types):
185+
if isinstance(entry, string_types):
186+
if '#' in entry:
187+
_, input_type_name = entry.split('#')
188+
else:
189+
input_type_name = entry
190+
if input_type_name in schema_defs:
191+
entry = input_types[index] = schema_defs[input_type_name]
192+
if isinstance(entry, collections.Mapping):
193+
if isinstance(entry['type'], string_types) and '#' in entry['type']:
194+
_, input_type_name = entry['type'].split('#')
195+
if input_type_name in schema_defs:
196+
input_types[index]['type'] = realize_input_schema(
197+
schema_defs[input_type_name], schema_defs)
198+
if isinstance(entry['type'], collections.MutableSequence):
199+
input_types[index]['type'] = realize_input_schema(
200+
entry['type'], schema_defs)
201+
if isinstance(entry['type'], collections.Mapping):
202+
input_types[index]['type'] = realize_input_schema(
203+
[input_types[index]['type']], schema_defs)
204+
if entry['type'] == 'array':
205+
items = entry['items'] if \
206+
not isinstance(entry['items'], string_types) else [entry['items']]
207+
input_types[index]['items'] = realize_input_schema(items, schema_defs)
208+
if entry['type'] == 'record':
209+
input_types[index]['fields'] = realize_input_schema(
210+
entry['fields'], schema_defs)
211+
return input_types
131212

132213
def generate_input_template(tool):
133214
# type: (Process) -> Dict[Text, Any]
134-
template = {}
135-
for inp in tool.tool["inputs"]:
215+
"""Generate an example input object for the given CWL process."""
216+
template = yaml.comments.CommentedMap()
217+
for inp in realize_input_schema(tool.tool["inputs"], tool.schemaDefs):
136218
name = shortname(inp["id"])
137-
inptype = inp["type"]
138-
template[name] = generate_example_input(inptype)
219+
value, comment = generate_example_input(
220+
inp['type'], inp.get('default', None))
221+
template.insert(0, name, value, comment)
139222
return template
140223

141224
def load_job_order(args, # type: argparse.Namespace
@@ -454,20 +537,14 @@ def main(argsl=None, # type: List[str]
454537
res.close()
455538
else:
456539
use_standard_schema("v1.0")
457-
#call function from provenance.py if the provenance flag is enabled.
458540
if args.provenance:
459541
if not args.compute_checksum:
460542
_logger.error("--provenance incompatible with --no-compute-checksum")
461543
return 1
462-
463544
runtimeContext.research_obj = ResearchObject(
464-
temp_prefix_ro=args.tmpdir_prefix,
465-
# Optionals, might be None
466-
orcid=args.orcid,
545+
temp_prefix_ro=args.tmpdir_prefix, orcid=args.orcid,
467546
full_name=args.cwl_full_name)
468547

469-
470-
471548
if loadingContext is None:
472549
loadingContext = LoadingContext(vars(args))
473550
else:
@@ -528,13 +605,17 @@ def main(argsl=None, # type: List[str]
528605
tool = make_tool(document_loader, avsc_names,
529606
metadata, uri, loadingContext)
530607
if args.make_template:
531-
yaml.safe_dump(generate_input_template(tool), sys.stdout,
532-
default_flow_style=False, indent=4,
533-
block_seq_indent=2)
608+
def my_represent_none(self, data): # pylint: disable=unused-argument
609+
"""Force clean representation of 'null'."""
610+
return self.represent_scalar(u'tag:yaml.org,2002:null', u'null')
611+
yaml.RoundTripRepresenter.add_representer(type(None), my_represent_none)
612+
yaml.round_trip_dump(
613+
generate_input_template(tool), sys.stdout,
614+
default_flow_style=False, indent=4, block_seq_indent=2)
534615
return 0
535616

536617
if args.validate:
537-
_logger.info("Tool definition is valid")
618+
print("{} is valid CWL.".format(args.workflow))
538619
return 0
539620

540621
if args.print_rdf:

0 commit comments

Comments
 (0)