Skip to content

Commit 9a4f9d1

Browse files
GlassOfWhiskeymr-c
andauthored
Added graph loading feature (#146)
Co-authored-by: Michael R. Crusoe <[email protected]>
1 parent 661564d commit 9a4f9d1

File tree

5 files changed

+120
-9
lines changed

5 files changed

+120
-9
lines changed

cwl_utils/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ class SubstitutionError(Exception):
2424

2525
class WorkflowException(Exception):
2626
pass
27+
28+
29+
class GraphTargetMissingException(WorkflowException):
30+
"""When a $graph is encountered and there is no target and no main/#main."""

cwl_utils/parser/__init__.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
from schema_salad.exceptions import ValidationException
99
from schema_salad.utils import yaml_no_ts
1010

11-
from . import cwl_v1_0 as cwl_v1_0
12-
from . import cwl_v1_1 as cwl_v1_1
13-
from . import cwl_v1_2 as cwl_v1_2
11+
from ..errors import GraphTargetMissingException
12+
from . import cwl_v1_0, cwl_v1_1, cwl_v1_2
1413

1514
LoadingOptions = Union[
1615
cwl_v1_0.LoadingOptions, cwl_v1_1.LoadingOptions, cwl_v1_2.LoadingOptions
@@ -38,6 +37,18 @@
3837
_Loader = Union[cwl_v1_0._Loader, cwl_v1_1._Loader, cwl_v1_2._Loader]
3938

4039

40+
def _get_id_from_graph(yaml: MutableMapping[str, Any], id_: Optional[str]) -> Any:
41+
if id_ is None:
42+
id_ = "main"
43+
for el in yaml["$graph"]:
44+
if el["id"].lstrip("#") == id_:
45+
return el
46+
raise GraphTargetMissingException(
47+
"Tool file contains graph of multiple objects, must specify "
48+
"one of #%s" % ", #".join(el["id"] for el in yaml["$graph"])
49+
)
50+
51+
4152
def cwl_version(yaml: Any) -> Any:
4253
"""Return the cwlVersion of a YAML object.
4354
@@ -64,49 +75,61 @@ def load_document_by_uri(
6475
"""Load a CWL object from a URI or a path."""
6576
if isinstance(path, str):
6677
uri = urlparse(path)
78+
id_ = uri.fragment or None
6779
if not uri.scheme or uri.scheme == "file":
6880
real_path = Path(unquote_plus(uri.path)).resolve().as_uri()
6981
else:
7082
real_path = path
7183
else:
7284
real_path = path.resolve().as_uri()
85+
id_ = path.resolve().name.split("#")[1] if "#" in path.resolve().name else None
7386

7487
baseuri = str(real_path)
7588

7689
if loadingOptions is None:
7790
loadingOptions = cwl_v1_2.LoadingOptions(fileuri=baseuri)
7891

7992
doc = loadingOptions.fetcher.fetch_text(real_path)
80-
return load_document_by_string(doc, baseuri, loadingOptions)
93+
return load_document_by_string(doc, baseuri, loadingOptions, id_)
8194

8295

8396
def load_document(
8497
doc: Any,
8598
baseuri: Optional[str] = None,
8699
loadingOptions: Optional[LoadingOptions] = None,
100+
id_: Optional[str] = None,
87101
) -> Any:
88102
"""Load a CWL object from a serialized YAML string or a YAML object."""
89103
if baseuri is None:
90104
baseuri = cwl_v1_0.file_uri(os.getcwd()) + "/"
91105
if isinstance(doc, str):
92-
return load_document_by_string(doc, baseuri, loadingOptions)
93-
return load_document_by_yaml(doc, baseuri, loadingOptions)
106+
return load_document_by_string(doc, baseuri, loadingOptions, id_)
107+
return load_document_by_yaml(doc, baseuri, loadingOptions, id_)
94108

95109

96110
def load_document_by_string(
97-
string: str, uri: str, loadingOptions: Optional[LoadingOptions] = None
111+
string: str,
112+
uri: str,
113+
loadingOptions: Optional[LoadingOptions] = None,
114+
id_: Optional[str] = None,
98115
) -> Any:
99116
"""Load a CWL object from a serialized YAML string."""
100117
yaml = yaml_no_ts()
101118
result = yaml.load(string)
102-
return load_document_by_yaml(result, uri, loadingOptions)
119+
return load_document_by_yaml(result, uri, loadingOptions, id_)
103120

104121

105122
def load_document_by_yaml(
106-
yaml: Any, uri: str, loadingOptions: Optional[LoadingOptions] = None
123+
yaml: Any,
124+
uri: str,
125+
loadingOptions: Optional[LoadingOptions] = None,
126+
id_: Optional[str] = None,
107127
) -> Any:
108128
"""Load a CWL object from a YAML object."""
109129
version = cwl_version(yaml)
130+
if "$graph" in yaml:
131+
yaml = _get_id_from_graph(yaml, id_)
132+
yaml["cwlVersion"] = version
110133
if version == "v1.0":
111134
result = cwl_v1_0.load_document_by_yaml(
112135
yaml, uri, cast(Optional[cwl_v1_0.LoadingOptions], loadingOptions)

testdata/echo-tool-packed.cwl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
cwlVersion: v1.2
2+
$graph:
3+
- class: CommandLineTool
4+
id: first
5+
inputs:
6+
in:
7+
type: Any
8+
outputs:
9+
out:
10+
type: string
11+
outputBinding:
12+
glob: out.txt
13+
loadContents: true
14+
outputEval: $(self[0].contents)
15+
baseCommand: [ echo, first ]
16+
stdout: out.txt
17+
- class: CommandLineTool
18+
id: main
19+
inputs:
20+
in:
21+
type: Any
22+
inputBinding: {}
23+
outputs:
24+
out:
25+
type: string
26+
outputBinding:
27+
glob: out.txt
28+
loadContents: true
29+
outputEval: $(self[0].contents)
30+
baseCommand: echo
31+
stdout: out.txt

testdata/js-expr-req-wf.cwl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
cwlVersion: v1.2
2+
$graph:
3+
- id: tool
4+
class: CommandLineTool
5+
requirements:
6+
InlineJavascriptRequirement:
7+
expressionLib:
8+
- "function foo() { return 2; }"
9+
inputs: []
10+
outputs:
11+
out: stdout
12+
arguments: [echo, $(foo())]
13+
stdout: whatever.txt
14+
15+
- id: wf
16+
class: Workflow
17+
requirements:
18+
InlineJavascriptRequirement:
19+
expressionLib:
20+
- "function bar() { return 1; }"
21+
inputs: []
22+
outputs:
23+
out:
24+
type: File
25+
outputSource: tool/out
26+
steps:
27+
tool:
28+
run: "#tool"
29+
in: {}
30+
out: [out]

tests/test_parser.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Test the load and save functions for CWL."""
22
from pathlib import Path
33

4+
from pytest import raises
45
from ruamel.yaml.main import YAML
56

67
import cwl_utils.parser.latest as latest
8+
from cwl_utils.errors import GraphTargetMissingException
79
from cwl_utils.parser import (
810
cwl_v1_2,
911
cwl_version,
@@ -86,3 +88,24 @@ def test_shortname() -> None:
8688
assert cwl_v1_2.shortname("http://example.com/foo#bar") == "bar"
8789
assert cwl_v1_2.shortname("http://example.com/#foo/bar") == "bar"
8890
assert cwl_v1_2.shortname("http://example.com/foo#bar/baz") == "baz"
91+
92+
93+
def test_get_id_from_graph() -> None:
94+
"""Test loading an explicit id of a CWL document with $graph property."""
95+
uri = Path(HERE / "../testdata/echo-tool-packed.cwl").resolve().as_uri()
96+
cwl_obj = load_document_by_uri(uri + "#main")
97+
assert cwl_obj.id == uri + "#main"
98+
99+
100+
def test_get_default_id_from_graph() -> None:
101+
"""Test that loading the default id of a CWL document with $graph property returns the `#main` id."""
102+
uri = Path(HERE / "../testdata/echo-tool-packed.cwl").resolve().as_uri()
103+
cwl_obj = load_document_by_uri(uri)
104+
assert cwl_obj.id == uri + "#main"
105+
106+
107+
def test_get_default_id_from_graph_without_main() -> None:
108+
"""Test that loading the default id of a CWL document with $graph property and no `#main` id throws an error."""
109+
with raises(GraphTargetMissingException):
110+
uri = Path(HERE / "../testdata/js-expr-req-wf.cwl").resolve().as_uri()
111+
load_document_by_uri(uri)

0 commit comments

Comments
 (0)