Skip to content

Commit 29d8279

Browse files
authored
--single-process inherit reqs & hints; sub-workflows (#1558)
1 parent 8720230 commit 29d8279

29 files changed

+707
-139
lines changed

cwltool/argparser.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -656,8 +656,10 @@ def arg_parser() -> argparse.ArgumentParser:
656656
default=None,
657657
help="Only executes the underlying Process (CommandLineTool, "
658658
"ExpressionTool, or sub-Workflow) for the given step in a workflow. "
659-
"This will not include any step-level processing: scatter, when, no "
660-
"processing of step-level default, or valueFrom input modifiers. "
659+
"This will not include any step-level processing: 'scatter', 'when'; "
660+
"and there will be no processing of step-level 'default', or 'valueFrom' "
661+
"input modifiers. However, requirements/hints from the step or parent "
662+
"workflow(s) will be inherited as usual."
661663
"The input object must match that Process's inputs.",
662664
)
663665

cwltool/builder.py

+1-21
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
MutableSequence,
1212
Optional,
1313
Set,
14-
Tuple,
1514
Union,
1615
cast,
1716
)
@@ -36,6 +35,7 @@
3635
CONTENT_LIMIT,
3736
CWLObjectType,
3837
CWLOutputType,
38+
HasReqsHints,
3939
aslist,
4040
get_listing,
4141
normalizeFilesDirs,
@@ -134,26 +134,6 @@ def check_format(
134134
)
135135

136136

137-
class HasReqsHints:
138-
"""Base class for get_requirement()."""
139-
140-
def __init__(self) -> None:
141-
"""Initialize this reqs decorator."""
142-
self.requirements = [] # type: List[CWLObjectType]
143-
self.hints = [] # type: List[CWLObjectType]
144-
145-
def get_requirement(
146-
self, feature: str
147-
) -> Tuple[Optional[CWLObjectType], Optional[bool]]:
148-
for item in reversed(self.requirements):
149-
if item["class"] == feature:
150-
return (item, True)
151-
for item in reversed(self.hints):
152-
if item["class"] == feature:
153-
return (item, False)
154-
return (None, None)
155-
156-
157137
class Builder(HasReqsHints):
158138
def __init__(
159139
self,

cwltool/context.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
from schema_salad.utils import FetcherCallableType
1313
from typing_extensions import TYPE_CHECKING
1414

15-
from .builder import Builder, HasReqsHints
15+
from .builder import Builder
1616
from .mpi import MpiConfig
1717
from .mutation import MutationManager
1818
from .pathmapper import PathMapper
1919
from .secrets import SecretStore
2020
from .software_requirements import DependenciesConfiguration
2121
from .stdfsaccess import StdFsAccess
22-
from .utils import DEFAULT_TMP_PREFIX, CWLObjectType, ResolverType
22+
from .utils import DEFAULT_TMP_PREFIX, CWLObjectType, HasReqsHints, ResolverType
2323

2424
if TYPE_CHECKING:
2525
from .process import Process

cwltool/cwlviewer.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Visualize a CWL workflow."""
2-
import os
32
from pathlib import Path
43
from urllib.parse import urlparse
54

cwltool/job.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from typing_extensions import TYPE_CHECKING
4040

4141
from . import env_to_stdout, run_job
42-
from .builder import Builder, HasReqsHints
42+
from .builder import Builder
4343
from .context import RuntimeContext
4444
from .errors import UnsupportedRequirement, WorkflowException
4545
from .loghandler import _logger
@@ -49,6 +49,7 @@
4949
from .utils import (
5050
CWLObjectType,
5151
CWLOutputType,
52+
HasReqsHints,
5253
DirectoryType,
5354
OutputCallbackType,
5455
bytes2str_in_dicts,

cwltool/main.py

+90-47
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,11 @@
4040
from ruamel.yaml.main import YAML
4141
from schema_salad.exceptions import ValidationException
4242
from schema_salad.ref_resolver import Loader, file_uri, uri_file_path
43-
from schema_salad.sourceline import strip_dup_lineno
43+
from schema_salad.sourceline import cmap, strip_dup_lineno
4444
from schema_salad.utils import ContextType, FetcherCallableType, json_dumps, yaml_no_ts
4545

4646
from . import CWL_CONTENT_TYPES, workflow
4747
from .argparser import arg_parser, generate_parser, get_default_args
48-
from .builder import HasReqsHints
4948
from .context import LoadingContext, RuntimeContext, getdefault
5049
from .cwlrdf import printdot, printrdf
5150
from .errors import ArgumentException, UnsupportedRequirement, WorkflowException
@@ -89,6 +88,7 @@
8988
CWLObjectType,
9089
CWLOutputAtomType,
9190
CWLOutputType,
91+
HasReqsHints,
9292
adjustDirObjs,
9393
normalizeFilesDirs,
9494
processes_to_kill,
@@ -754,69 +754,95 @@ def my_represent_none(
754754
)
755755

756756

757+
def inherit_reqshints(tool: Process, parent: Process) -> None:
758+
"""Copy down requirements and hints from ancestors of a given process."""
759+
for parent_req in parent.requirements:
760+
found = False
761+
for tool_req in tool.requirements:
762+
if parent_req["class"] == tool_req["class"]:
763+
found = True
764+
break
765+
if not found:
766+
tool.requirements.append(parent_req)
767+
for parent_hint in parent.hints:
768+
found = False
769+
for tool_req in tool.requirements:
770+
if parent_hint["class"] == tool_req["class"]:
771+
found = True
772+
break
773+
if not found:
774+
for tool_hint in tool.hints:
775+
if parent_hint["class"] == tool_hint["class"]:
776+
found = True
777+
break
778+
if not found:
779+
tool.hints.append(parent_hint)
780+
781+
757782
def choose_target(
758783
args: argparse.Namespace,
759784
tool: Process,
760-
loadingContext: LoadingContext,
785+
loading_context: LoadingContext,
761786
) -> Optional[Process]:
762787
"""Walk the Workflow, extract the subset matches all the args.targets."""
763-
if loadingContext.loader is None:
764-
raise Exception("loadingContext.loader cannot be None")
788+
if loading_context.loader is None:
789+
raise Exception("loading_context.loader cannot be None")
765790

766791
if isinstance(tool, Workflow):
767792
url = urllib.parse.urlparse(tool.tool["id"])
768793
if url.fragment:
769794
extracted = get_subgraph(
770-
[tool.tool["id"] + "/" + r for r in args.target], tool
795+
[tool.tool["id"] + "/" + r for r in args.target], tool, loading_context
771796
)
772797
else:
773798
extracted = get_subgraph(
774799
[
775-
loadingContext.loader.fetcher.urljoin(tool.tool["id"], "#" + r)
800+
loading_context.loader.fetcher.urljoin(tool.tool["id"], "#" + r)
776801
for r in args.target
777802
],
778803
tool,
804+
loading_context,
779805
)
780806
else:
781807
_logger.error("Can only use --target on Workflows")
782808
return None
783-
if isinstance(loadingContext.loader.idx, MutableMapping):
784-
loadingContext.loader.idx[extracted["id"]] = extracted
785-
tool = make_tool(extracted["id"], loadingContext)
809+
if isinstance(loading_context.loader.idx, MutableMapping):
810+
loading_context.loader.idx[extracted["id"]] = extracted
811+
tool = make_tool(extracted["id"], loading_context)
786812
else:
787-
raise Exception("Missing loadingContext.loader.idx!")
813+
raise Exception("Missing loading_context.loader.idx!")
788814

789815
return tool
790816

791817

792818
def choose_step(
793819
args: argparse.Namespace,
794820
tool: Process,
795-
loadingContext: LoadingContext,
821+
loading_context: LoadingContext,
796822
) -> Optional[Process]:
797823
"""Walk the given Workflow and extract just args.single_step."""
798-
if loadingContext.loader is None:
799-
raise Exception("loadingContext.loader cannot be None")
824+
if loading_context.loader is None:
825+
raise Exception("loading_context.loader cannot be None")
800826

801827
if isinstance(tool, Workflow):
802828
url = urllib.parse.urlparse(tool.tool["id"])
803829
if url.fragment:
804-
extracted = get_step(tool, tool.tool["id"] + "/" + args.single_step)
830+
step_id = tool.tool["id"] + "/" + args.single_step
805831
else:
806-
extracted = get_step(
807-
tool,
808-
loadingContext.loader.fetcher.urljoin(
809-
tool.tool["id"], "#" + args.single_step
810-
),
832+
step_id = loading_context.loader.fetcher.urljoin(
833+
tool.tool["id"], "#" + args.single_step
811834
)
835+
extracted = get_step(tool, step_id, loading_context)
812836
else:
813837
_logger.error("Can only use --single-step on Workflows")
814838
return None
815-
if isinstance(loadingContext.loader.idx, MutableMapping):
816-
loadingContext.loader.idx[extracted["id"]] = extracted
817-
tool = make_tool(extracted["id"], loadingContext)
839+
if isinstance(loading_context.loader.idx, MutableMapping):
840+
loading_context.loader.idx[extracted["id"]] = cast(
841+
Union[CommentedMap, CommentedSeq, str, None], cmap(extracted)
842+
)
843+
tool = make_tool(extracted["id"], loading_context)
818844
else:
819-
raise Exception("Missing loadingContext.loader.idx!")
845+
raise Exception("Missing loading_context.loader.idx!")
820846

821847
return tool
822848

@@ -826,36 +852,33 @@ def choose_process(
826852
tool: Process,
827853
loadingContext: LoadingContext,
828854
) -> Optional[Process]:
829-
"""Walk the given Workflow and extract just args.single_step."""
855+
"""Walk the given Workflow and extract just args.single_process."""
830856
if loadingContext.loader is None:
831857
raise Exception("loadingContext.loader cannot be None")
832858

833859
if isinstance(tool, Workflow):
834860
url = urllib.parse.urlparse(tool.tool["id"])
835861
if url.fragment:
836-
extracted = get_process(
837-
tool,
838-
tool.tool["id"] + "/" + args.single_process,
839-
loadingContext.loader.idx,
840-
)
862+
step_id = tool.tool["id"] + "/" + args.single_process
841863
else:
842-
extracted = get_process(
843-
tool,
844-
loadingContext.loader.fetcher.urljoin(
845-
tool.tool["id"], "#" + args.single_process
846-
),
847-
loadingContext.loader.idx,
864+
step_id = loadingContext.loader.fetcher.urljoin(
865+
tool.tool["id"], "#" + args.single_process
848866
)
867+
extracted, workflow_step = get_process(
868+
tool,
869+
step_id,
870+
loadingContext,
871+
)
849872
else:
850873
_logger.error("Can only use --single-process on Workflows")
851874
return None
852875
if isinstance(loadingContext.loader.idx, MutableMapping):
853876
loadingContext.loader.idx[extracted["id"]] = extracted
854-
tool = make_tool(extracted["id"], loadingContext)
877+
new_tool = make_tool(extracted["id"], loadingContext)
855878
else:
856879
raise Exception("Missing loadingContext.loader.idx!")
857-
858-
return tool
880+
inherit_reqshints(new_tool, workflow_step)
881+
return new_tool
859882

860883

861884
def check_working_directories(
@@ -887,6 +910,33 @@ def check_working_directories(
887910
return None
888911

889912

913+
def print_targets(
914+
tool: Process,
915+
stdout: Union[TextIO, StreamWriter],
916+
loading_context: LoadingContext,
917+
prefix: str = "",
918+
) -> None:
919+
"""Recursively find targets for --subgraph and friends."""
920+
for f in ("outputs", "inputs"):
921+
if tool.tool[f]:
922+
_logger.info("%s %s%s targets:", prefix[:-1], f[0].upper(), f[1:-1])
923+
stdout.write(
924+
" "
925+
+ "\n ".join([f"{prefix}{shortname(t['id'])}" for t in tool.tool[f]])
926+
+ "\n"
927+
)
928+
if "steps" in tool.tool:
929+
_logger.info("%s steps targets:", prefix[:-1])
930+
for t in tool.tool["steps"]:
931+
stdout.write(f" {prefix}{shortname(t['id'])}\n")
932+
run: Union[str, Process] = t["run"]
933+
if isinstance(run, str):
934+
process = make_tool(run, loading_context)
935+
else:
936+
process = run
937+
print_targets(process, stdout, loading_context, shortname(t["id"]) + "/")
938+
939+
890940
def main(
891941
argsl: Optional[List[str]] = None,
892942
args: Optional[argparse.Namespace] = None,
@@ -1084,14 +1134,7 @@ def main(
10841134
return 0
10851135

10861136
if args.print_targets:
1087-
for f in ("outputs", "steps", "inputs"):
1088-
if tool.tool[f]:
1089-
_logger.info("%s%s targets:", f[0].upper(), f[1:-1])
1090-
stdout.write(
1091-
" "
1092-
+ "\n ".join([shortname(t["id"]) for t in tool.tool[f]])
1093-
+ "\n"
1094-
)
1137+
print_targets(tool, stdout, loadingContext)
10951138
return 0
10961139

10971140
if args.target:

cwltool/process.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
from typing_extensions import TYPE_CHECKING
5050

5151
from . import expression
52-
from .builder import INPUT_OBJ_VOCAB, Builder, HasReqsHints
52+
from .builder import INPUT_OBJ_VOCAB, Builder
5353
from .context import LoadingContext, RuntimeContext, getdefault
5454
from .errors import UnsupportedRequirement, WorkflowException
5555
from .loghandler import _logger
@@ -62,6 +62,7 @@
6262
CWLObjectType,
6363
CWLOutputAtomType,
6464
CWLOutputType,
65+
HasReqsHints,
6566
JobsGeneratorType,
6667
OutputCallbackType,
6768
adjustDirObjs,

cwltool/software_requirements.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
from typing_extensions import TYPE_CHECKING
1616

17+
from .utils import HasReqsHints
18+
1719
if TYPE_CHECKING:
18-
from .builder import Builder, HasReqsHints
20+
from .builder import Builder
1921

2022
try:
2123
from galaxy.tool_util import deps
@@ -98,7 +100,7 @@ def build_job_script(self, builder: "Builder", command: List[str]) -> str:
98100
return job_script
99101

100102

101-
def get_dependencies(builder: "HasReqsHints") -> ToolRequirements:
103+
def get_dependencies(builder: HasReqsHints) -> ToolRequirements:
102104
(software_requirement, _) = builder.get_requirement("SoftwareRequirement")
103105
dependencies = [] # type: List[ToolRequirement]
104106
if software_requirement and software_requirement.get("packages"):
@@ -129,7 +131,7 @@ def get_dependencies(builder: "HasReqsHints") -> ToolRequirements:
129131

130132

131133
def get_container_from_software_requirements(
132-
use_biocontainers: bool, builder: "HasReqsHints"
134+
use_biocontainers: bool, builder: HasReqsHints
133135
) -> Optional[str]:
134136
if use_biocontainers:
135137
ensure_galaxy_lib_available()

0 commit comments

Comments
 (0)