We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Error when building subgraph with --target
Hello,

I am trying to use `--target` option with this very simple workflow

```
cwltool --target step2 --debug --print-subgraph wf.cwl > subgraph.cwl
```

However I get an error `TypeError: unhashable type: 'CommentedMap'`

Thanks!
---

## Expected Behavior
**subgraph.cwl**
```
{
    "class": "Workflow",
    "cwlVersion": "v1.2",
    "id": "file:///home/wf.cwl",
    "inputs": [
        {
            "id": "file:///home/wf.cwl#step2_in",
            "type": "string"
        }
    ],
    "outputs": [
        {
            "id": "file:///home/wf.cwl#step2_out",
            "outputSource": "file:///home/wf.cwl#step2/output",
            "type": "File"
        }
    ],
    "steps": [
        {
            "id": "file:///home/wf.cwl#step2",
            "in": [
                {
                    "id": "file:///home/wf.cwl#step2/in",
                    "source": "file:///home/wf.cwl#step2_in"
                }
            ],
            "out": [
                {
                    "id": "file:///home/wf.cwl#step2/output"
                }
            ],
            "run": "file:///home/echo.cwl"
        }
    ]
}
```

## Actual Behavior
Fails with the error as seen in  [Full Traceback](#traceback)

## Workflow Code
**wf.cwl**
```
class: Workflow
cwlVersion: v1.2
inputs:
  - id: step1_in
    type: string
  - id: step2_in
    type: string
outputs:
  - id: step1_out
    type: File
    outputSource: step1/output
  - id: step2_out
    type: File
    outputSource: step2/output
steps:
  - id: step1
    in:
      - id: in
        source: step1_in
    out:
      - id: output
    run: echo.cwl
  - id: step2
    in:
      - id: in
        source: step2_in
    out:
      - id: output
    run: echo.cwl
```
**echo.cwl**
```
cwlVersion: v1.2
class: CommandLineTool
baseCommand: echo
inputs:
  - id: in
    type: string
    inputBinding: {position: 1}
outputs:
  - id: output
    type: stdout
stdout: output_echo.txt
```
**wf.yml**
```
step1_in: step1
step2_in: step2
```

## Full Traceback

<a name="traceback"/>

```
Traceback (most recent call last):
  File "/home/miniconda/envs/cwl_env/lib/python3.8/site-packages/cwltool/main.py", line 1020, in main
    ctool = choose_target(args, tool, loadingContext)
  File "/home/miniconda/envs/cwl_env/lib/python3.8/site-packages/cwltool/main.py", line 768, in choose_target
    extracted = get_subgraph(
  File "/home/miniconda/envs/cwl_env/lib/python3.8/site-packages/cwltool/subgraph.py", line 83, in get_subgraph
    declare_node(nodes, out, None)
  File "/home/miniconda/envs/cwl_env/lib/python3.8/site-packages/cwltool/subgraph.py", line 41, in declare_node
    if nodeid in nodes:
TypeError: unhashable type: 'CommentedMap' 
```
When I am printing out the `nodeid` that is passed to `declare_node` function I see `CommentedMap` instead of `string`:
```
ordereddict([('id', 'file:///home/wf.cwl#step1/output')])
```
After I changed this lines 96-97 in [subgraph.py](https://github.com/common-workflow-language/cwltool/blob/main/cwltool/subgraph.py)
```
#declare_node(nodes, out, None)
declare_node(nodes, out["id"], None)
#nodes[out].up.append(st["id"])
nodes[out["id"]].up.append(st["id"])
```
I got the `subgraph.cwl` as shown above

## Your Environment
* cwltool version:  3.0.20201203173111



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `cwltool/subgraph.py`
Content:
```
1 import urllib
2 from collections import namedtuple
3 from typing import (
4     Any,
5     Dict,
6     List,
7     Mapping,
8     MutableMapping,
9     MutableSequence,
10     Optional,
11     Set,
12     Tuple,
13     cast,
14 )
15 
16 from ruamel.yaml.comments import CommentedMap
17 
18 from .utils import CWLObjectType, aslist
19 from .workflow import Workflow, WorkflowStep
20 
21 Node = namedtuple("Node", ("up", "down", "type"))
22 UP = "up"
23 DOWN = "down"
24 INPUT = "input"
25 OUTPUT = "output"
26 STEP = "step"
27 
28 
29 def subgraph_visit(
30     current: str,
31     nodes: MutableMapping[str, Node],
32     visited: Set[str],
33     direction: str,
34 ) -> None:
35 
36     if current in visited:
37         return
38     visited.add(current)
39 
40     if direction == DOWN:
41         d = nodes[current].down
42     if direction == UP:
43         d = nodes[current].up
44     for c in d:
45         subgraph_visit(c, nodes, visited, direction)
46 
47 
48 def declare_node(nodes: Dict[str, Node], nodeid: str, tp: Optional[str]) -> Node:
49     if nodeid in nodes:
50         n = nodes[nodeid]
51         if n.type is None:
52             nodes[nodeid] = Node(n.up, n.down, tp)
53     else:
54         nodes[nodeid] = Node([], [], tp)
55     return nodes[nodeid]
56 
57 
58 def find_step(steps: List[WorkflowStep], stepid: str) -> Optional[CWLObjectType]:
59     for st in steps:
60         if st.tool["id"] == stepid:
61             return st.tool
62     return None
63 
64 
65 def get_subgraph(roots: MutableSequence[str], tool: Workflow) -> CommentedMap:
66     if tool.tool["class"] != "Workflow":
67         raise Exception("Can only extract subgraph from workflow")
68 
69     nodes: Dict[str, Node] = {}
70 
71     for inp in tool.tool["inputs"]:
72         declare_node(nodes, inp["id"], INPUT)
73 
74     for out in tool.tool["outputs"]:
75         declare_node(nodes, out["id"], OUTPUT)
76         for i in aslist(out.get("outputSource", [])):
77             # source is upstream from output (dependency)
78             nodes[out["id"]].up.append(i)
79             # output is downstream from source
80             declare_node(nodes, i, None)
81             nodes[i].down.append(out["id"])
82 
83     for st in tool.tool["steps"]:
84         step = declare_node(nodes, st["id"], STEP)
85         for i in st["in"]:
86             if "source" not in i:
87                 continue
88             for src in aslist(i["source"]):
89                 # source is upstream from step (dependency)
90                 step.up.append(src)
91                 # step is downstream from source
92                 declare_node(nodes, src, None)
93                 nodes[src].down.append(st["id"])
94         for out in st["out"]:
95             # output is downstream from step
96             step.down.append(out)
97             # step is upstream from output
98             declare_node(nodes, out, None)
99             nodes[out].up.append(st["id"])
100 
101     # Find all the downstream nodes from the starting points
102     visited_down: Set[str] = set()
103     for r in roots:
104         if nodes[r].type == OUTPUT:
105             subgraph_visit(r, nodes, visited_down, UP)
106         else:
107             subgraph_visit(r, nodes, visited_down, DOWN)
108 
109     # Now make sure all the nodes are connected to upstream inputs
110     visited: Set[str] = set()
111     rewire: Dict[str, Tuple[str, CWLObjectType]] = {}
112     for v in visited_down:
113         visited.add(v)
114         if nodes[v].type in (STEP, OUTPUT):
115             for u in nodes[v].up:
116                 if u in visited_down:
117                     continue
118                 if nodes[u].type == INPUT:
119                     visited.add(u)
120                 else:
121                     # rewire
122                     df = urllib.parse.urldefrag(u)
123                     rn = str(df[0] + "#" + df[1].replace("/", "_"))
124                     if nodes[v].type == STEP:
125                         wfstep = find_step(tool.steps, v)
126                         if wfstep is not None:
127                             for inp in cast(
128                                 MutableSequence[CWLObjectType], wfstep["inputs"]
129                             ):
130                                 if "source" in inp and u in cast(
131                                     CWLObjectType, inp["source"]
132                                 ):
133                                     rewire[u] = (rn, cast(CWLObjectType, inp["type"]))
134                                     break
135                         else:
136                             raise Exception("Could not find step %s" % v)
137 
138     extracted = CommentedMap()
139     for f in tool.tool:
140         if f in ("steps", "inputs", "outputs"):
141             extracted[f] = []
142             for i in tool.tool[f]:
143                 if i["id"] in visited:
144                     if f == "steps":
145                         for inport in i["in"]:
146                             if "source" not in inport:
147                                 continue
148                             if isinstance(inport["source"], MutableSequence):
149                                 inport["source"] = [
150                                     rewire[s][0]
151                                     for s in inport["source"]
152                                     if s in rewire
153                                 ]
154                             elif inport["source"] in rewire:
155                                 inport["source"] = rewire[inport["source"]][0]
156                     extracted[f].append(i)
157         else:
158             extracted[f] = tool.tool[f]
159 
160     for rv in rewire.values():
161         extracted["inputs"].append({"id": rv[0], "type": rv[1]})
162 
163     return extracted
164 
165 
166 def get_step(tool: Workflow, step_id: str) -> CommentedMap:
167 
168     extracted = CommentedMap()
169 
170     step = find_step(tool.steps, step_id)
171     if step is None:
172         raise Exception(f"Step {step_id} was not found")
173 
174     extracted["steps"] = [step]
175     extracted["inputs"] = []
176     extracted["outputs"] = []
177 
178     for inport in cast(List[CWLObjectType], step["in"]):
179         name = cast(str, inport["id"]).split("#")[-1].split("/")[-1]
180         extracted["inputs"].append({"id": name, "type": "Any"})
181         inport["source"] = name
182         if "linkMerge" in inport:
183             del inport["linkMerge"]
184 
185     for outport in cast(List[str], step["out"]):
186         name = outport.split("#")[-1].split("/")[-1]
187         extracted["outputs"].append(
188             {"id": name, "type": "Any", "outputSource": f"{step_id}/{name}"}
189         )
190 
191     for f in tool.tool:
192         if f not in ("steps", "inputs", "outputs"):
193             extracted[f] = tool.tool[f]
194 
195     return extracted
196 
197 
198 def get_process(tool: Workflow, step_id: str, index: Mapping[str, Any]) -> Any:
199     """Return just a single Process from a Workflow step."""
200     step = find_step(tool.steps, step_id)
201     if step is None:
202         raise Exception(f"Step {step_id} was not found")
203 
204     run = step["run"]
205 
206     if isinstance(run, str):
207         return index[run]
208     else:
209         return run
210
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/cwltool/subgraph.py b/cwltool/subgraph.py
--- a/cwltool/subgraph.py
+++ b/cwltool/subgraph.py
@@ -92,6 +92,8 @@
                 declare_node(nodes, src, None)
                 nodes[src].down.append(st["id"])
         for out in st["out"]:
+            if isinstance(out, Mapping) and "id" in out:
+                out = out["id"]
             # output is downstream from step
             step.down.append(out)
             # step is upstream from output

{"golden_diff": "diff --git a/cwltool/subgraph.py b/cwltool/subgraph.py\n--- a/cwltool/subgraph.py\n+++ b/cwltool/subgraph.py\n@@ -92,6 +92,8 @@\n                 declare_node(nodes, src, None)\n                 nodes[src].down.append(st[\"id\"])\n         for out in st[\"out\"]:\n+            if isinstance(out, Mapping) and \"id\" in out:\n+                out = out[\"id\"]\n             # output is downstream from step\n             step.down.append(out)\n             # step is upstream from output\n", "issue": "Error when building subgraph with --target\nHello,\r\n\r\nI am trying to use `--target` option with this very simple workflow\r\n\r\n```\r\ncwltool --target step2 --debug --print-subgraph wf.cwl > subgraph.cwl\r\n```\r\n\r\nHowever I get an error `TypeError: unhashable type: 'CommentedMap'`\r\n\r\nThanks!\r\n---\r\n\r\n## Expected Behavior\r\n**subgraph.cwl**\r\n```\r\n{\r\n    \"class\": \"Workflow\",\r\n    \"cwlVersion\": \"v1.2\",\r\n    \"id\": \"file:///home/wf.cwl\",\r\n    \"inputs\": [\r\n        {\r\n            \"id\": \"file:///home/wf.cwl#step2_in\",\r\n            \"type\": \"string\"\r\n        }\r\n    ],\r\n    \"outputs\": [\r\n        {\r\n            \"id\": \"file:///home/wf.cwl#step2_out\",\r\n            \"outputSource\": \"file:///home/wf.cwl#step2/output\",\r\n            \"type\": \"File\"\r\n        }\r\n    ],\r\n    \"steps\": [\r\n        {\r\n            \"id\": \"file:///home/wf.cwl#step2\",\r\n            \"in\": [\r\n                {\r\n                    \"id\": \"file:///home/wf.cwl#step2/in\",\r\n                    \"source\": \"file:///home/wf.cwl#step2_in\"\r\n                }\r\n            ],\r\n            \"out\": [\r\n                {\r\n                    \"id\": \"file:///home/wf.cwl#step2/output\"\r\n                }\r\n            ],\r\n            \"run\": \"file:///home/echo.cwl\"\r\n        }\r\n    ]\r\n}\r\n```\r\n\r\n## Actual Behavior\r\nFails with the error as seen in  [Full Traceback](#traceback)\r\n\r\n## Workflow Code\r\n**wf.cwl**\r\n```\r\nclass: Workflow\r\ncwlVersion: v1.2\r\ninputs:\r\n  - id: step1_in\r\n    type: string\r\n  - id: step2_in\r\n    type: string\r\noutputs:\r\n  - id: step1_out\r\n    type: File\r\n    outputSource: step1/output\r\n  - id: step2_out\r\n    type: File\r\n    outputSource: step2/output\r\nsteps:\r\n  - id: step1\r\n    in:\r\n      - id: in\r\n        source: step1_in\r\n    out:\r\n      - id: output\r\n    run: echo.cwl\r\n  - id: step2\r\n    in:\r\n      - id: in\r\n        source: step2_in\r\n    out:\r\n      - id: output\r\n    run: echo.cwl\r\n```\r\n**echo.cwl**\r\n```\r\ncwlVersion: v1.2\r\nclass: CommandLineTool\r\nbaseCommand: echo\r\ninputs:\r\n  - id: in\r\n    type: string\r\n    inputBinding: {position: 1}\r\noutputs:\r\n  - id: output\r\n    type: stdout\r\nstdout: output_echo.txt\r\n```\r\n**wf.yml**\r\n```\r\nstep1_in: step1\r\nstep2_in: step2\r\n```\r\n\r\n## Full Traceback\r\n\r\n<a name=\"traceback\"/>\r\n\r\n```\r\nTraceback (most recent call last):\r\n  File \"/home/miniconda/envs/cwl_env/lib/python3.8/site-packages/cwltool/main.py\", line 1020, in main\r\n    ctool = choose_target(args, tool, loadingContext)\r\n  File \"/home/miniconda/envs/cwl_env/lib/python3.8/site-packages/cwltool/main.py\", line 768, in choose_target\r\n    extracted = get_subgraph(\r\n  File \"/home/miniconda/envs/cwl_env/lib/python3.8/site-packages/cwltool/subgraph.py\", line 83, in get_subgraph\r\n    declare_node(nodes, out, None)\r\n  File \"/home/miniconda/envs/cwl_env/lib/python3.8/site-packages/cwltool/subgraph.py\", line 41, in declare_node\r\n    if nodeid in nodes:\r\nTypeError: unhashable type: 'CommentedMap' \r\n```\r\nWhen I am printing out the `nodeid` that is passed to `declare_node` function I see `CommentedMap` instead of `string`:\r\n```\r\nordereddict([('id', 'file:///home/wf.cwl#step1/output')])\r\n```\r\nAfter I changed this lines 96-97 in [subgraph.py](https://github.com/common-workflow-language/cwltool/blob/main/cwltool/subgraph.py)\r\n```\r\n#declare_node(nodes, out, None)\r\ndeclare_node(nodes, out[\"id\"], None)\r\n#nodes[out].up.append(st[\"id\"])\r\nnodes[out[\"id\"]].up.append(st[\"id\"])\r\n```\r\nI got the `subgraph.cwl` as shown above\r\n\r\n## Your Environment\r\n* cwltool version:  3.0.20201203173111\r\n\r\n\n", "before_files": [{"content": "import urllib\nfrom collections import namedtuple\nfrom typing import (\n    Any,\n    Dict,\n    List,\n    Mapping,\n    MutableMapping,\n    MutableSequence,\n    Optional,\n    Set,\n    Tuple,\n    cast,\n)\n\nfrom ruamel.yaml.comments import CommentedMap\n\nfrom .utils import CWLObjectType, aslist\nfrom .workflow import Workflow, WorkflowStep\n\nNode = namedtuple(\"Node\", (\"up\", \"down\", \"type\"))\nUP = \"up\"\nDOWN = \"down\"\nINPUT = \"input\"\nOUTPUT = \"output\"\nSTEP = \"step\"\n\n\ndef subgraph_visit(\n    current: str,\n    nodes: MutableMapping[str, Node],\n    visited: Set[str],\n    direction: str,\n) -> None:\n\n    if current in visited:\n        return\n    visited.add(current)\n\n    if direction == DOWN:\n        d = nodes[current].down\n    if direction == UP:\n        d = nodes[current].up\n    for c in d:\n        subgraph_visit(c, nodes, visited, direction)\n\n\ndef declare_node(nodes: Dict[str, Node], nodeid: str, tp: Optional[str]) -> Node:\n    if nodeid in nodes:\n        n = nodes[nodeid]\n        if n.type is None:\n            nodes[nodeid] = Node(n.up, n.down, tp)\n    else:\n        nodes[nodeid] = Node([], [], tp)\n    return nodes[nodeid]\n\n\ndef find_step(steps: List[WorkflowStep], stepid: str) -> Optional[CWLObjectType]:\n    for st in steps:\n        if st.tool[\"id\"] == stepid:\n            return st.tool\n    return None\n\n\ndef get_subgraph(roots: MutableSequence[str], tool: Workflow) -> CommentedMap:\n    if tool.tool[\"class\"] != \"Workflow\":\n        raise Exception(\"Can only extract subgraph from workflow\")\n\n    nodes: Dict[str, Node] = {}\n\n    for inp in tool.tool[\"inputs\"]:\n        declare_node(nodes, inp[\"id\"], INPUT)\n\n    for out in tool.tool[\"outputs\"]:\n        declare_node(nodes, out[\"id\"], OUTPUT)\n        for i in aslist(out.get(\"outputSource\", [])):\n            # source is upstream from output (dependency)\n            nodes[out[\"id\"]].up.append(i)\n            # output is downstream from source\n            declare_node(nodes, i, None)\n            nodes[i].down.append(out[\"id\"])\n\n    for st in tool.tool[\"steps\"]:\n        step = declare_node(nodes, st[\"id\"], STEP)\n        for i in st[\"in\"]:\n            if \"source\" not in i:\n                continue\n            for src in aslist(i[\"source\"]):\n                # source is upstream from step (dependency)\n                step.up.append(src)\n                # step is downstream from source\n                declare_node(nodes, src, None)\n                nodes[src].down.append(st[\"id\"])\n        for out in st[\"out\"]:\n            # output is downstream from step\n            step.down.append(out)\n            # step is upstream from output\n            declare_node(nodes, out, None)\n            nodes[out].up.append(st[\"id\"])\n\n    # Find all the downstream nodes from the starting points\n    visited_down: Set[str] = set()\n    for r in roots:\n        if nodes[r].type == OUTPUT:\n            subgraph_visit(r, nodes, visited_down, UP)\n        else:\n            subgraph_visit(r, nodes, visited_down, DOWN)\n\n    # Now make sure all the nodes are connected to upstream inputs\n    visited: Set[str] = set()\n    rewire: Dict[str, Tuple[str, CWLObjectType]] = {}\n    for v in visited_down:\n        visited.add(v)\n        if nodes[v].type in (STEP, OUTPUT):\n            for u in nodes[v].up:\n                if u in visited_down:\n                    continue\n                if nodes[u].type == INPUT:\n                    visited.add(u)\n                else:\n                    # rewire\n                    df = urllib.parse.urldefrag(u)\n                    rn = str(df[0] + \"#\" + df[1].replace(\"/\", \"_\"))\n                    if nodes[v].type == STEP:\n                        wfstep = find_step(tool.steps, v)\n                        if wfstep is not None:\n                            for inp in cast(\n                                MutableSequence[CWLObjectType], wfstep[\"inputs\"]\n                            ):\n                                if \"source\" in inp and u in cast(\n                                    CWLObjectType, inp[\"source\"]\n                                ):\n                                    rewire[u] = (rn, cast(CWLObjectType, inp[\"type\"]))\n                                    break\n                        else:\n                            raise Exception(\"Could not find step %s\" % v)\n\n    extracted = CommentedMap()\n    for f in tool.tool:\n        if f in (\"steps\", \"inputs\", \"outputs\"):\n            extracted[f] = []\n            for i in tool.tool[f]:\n                if i[\"id\"] in visited:\n                    if f == \"steps\":\n                        for inport in i[\"in\"]:\n                            if \"source\" not in inport:\n                                continue\n                            if isinstance(inport[\"source\"], MutableSequence):\n                                inport[\"source\"] = [\n                                    rewire[s][0]\n                                    for s in inport[\"source\"]\n                                    if s in rewire\n                                ]\n                            elif inport[\"source\"] in rewire:\n                                inport[\"source\"] = rewire[inport[\"source\"]][0]\n                    extracted[f].append(i)\n        else:\n            extracted[f] = tool.tool[f]\n\n    for rv in rewire.values():\n        extracted[\"inputs\"].append({\"id\": rv[0], \"type\": rv[1]})\n\n    return extracted\n\n\ndef get_step(tool: Workflow, step_id: str) -> CommentedMap:\n\n    extracted = CommentedMap()\n\n    step = find_step(tool.steps, step_id)\n    if step is None:\n        raise Exception(f\"Step {step_id} was not found\")\n\n    extracted[\"steps\"] = [step]\n    extracted[\"inputs\"] = []\n    extracted[\"outputs\"] = []\n\n    for inport in cast(List[CWLObjectType], step[\"in\"]):\n        name = cast(str, inport[\"id\"]).split(\"#\")[-1].split(\"/\")[-1]\n        extracted[\"inputs\"].append({\"id\": name, \"type\": \"Any\"})\n        inport[\"source\"] = name\n        if \"linkMerge\" in inport:\n            del inport[\"linkMerge\"]\n\n    for outport in cast(List[str], step[\"out\"]):\n        name = outport.split(\"#\")[-1].split(\"/\")[-1]\n        extracted[\"outputs\"].append(\n            {\"id\": name, \"type\": \"Any\", \"outputSource\": f\"{step_id}/{name}\"}\n        )\n\n    for f in tool.tool:\n        if f not in (\"steps\", \"inputs\", \"outputs\"):\n            extracted[f] = tool.tool[f]\n\n    return extracted\n\n\ndef get_process(tool: Workflow, step_id: str, index: Mapping[str, Any]) -> Any:\n    \"\"\"Return just a single Process from a Workflow step.\"\"\"\n    step = find_step(tool.steps, step_id)\n    if step is None:\n        raise Exception(f\"Step {step_id} was not found\")\n\n    run = step[\"run\"]\n\n    if isinstance(run, str):\n        return index[run]\n    else:\n        return run\n", "path": "cwltool/subgraph.py"}], "after_files": [{"content": "import urllib\nfrom collections import namedtuple\nfrom typing import (\n    Any,\n    Dict,\n    List,\n    Mapping,\n    MutableMapping,\n    MutableSequence,\n    Optional,\n    Set,\n    Tuple,\n    cast,\n)\n\nfrom ruamel.yaml.comments import CommentedMap\n\nfrom .utils import CWLObjectType, aslist\nfrom .workflow import Workflow, WorkflowStep\n\nNode = namedtuple(\"Node\", (\"up\", \"down\", \"type\"))\nUP = \"up\"\nDOWN = \"down\"\nINPUT = \"input\"\nOUTPUT = \"output\"\nSTEP = \"step\"\n\n\ndef subgraph_visit(\n    current: str,\n    nodes: MutableMapping[str, Node],\n    visited: Set[str],\n    direction: str,\n) -> None:\n\n    if current in visited:\n        return\n    visited.add(current)\n\n    if direction == DOWN:\n        d = nodes[current].down\n    if direction == UP:\n        d = nodes[current].up\n    for c in d:\n        subgraph_visit(c, nodes, visited, direction)\n\n\ndef declare_node(nodes: Dict[str, Node], nodeid: str, tp: Optional[str]) -> Node:\n    if nodeid in nodes:\n        n = nodes[nodeid]\n        if n.type is None:\n            nodes[nodeid] = Node(n.up, n.down, tp)\n    else:\n        nodes[nodeid] = Node([], [], tp)\n    return nodes[nodeid]\n\n\ndef find_step(steps: List[WorkflowStep], stepid: str) -> Optional[CWLObjectType]:\n    for st in steps:\n        if st.tool[\"id\"] == stepid:\n            return st.tool\n    return None\n\n\ndef get_subgraph(roots: MutableSequence[str], tool: Workflow) -> CommentedMap:\n    if tool.tool[\"class\"] != \"Workflow\":\n        raise Exception(\"Can only extract subgraph from workflow\")\n\n    nodes: Dict[str, Node] = {}\n\n    for inp in tool.tool[\"inputs\"]:\n        declare_node(nodes, inp[\"id\"], INPUT)\n\n    for out in tool.tool[\"outputs\"]:\n        declare_node(nodes, out[\"id\"], OUTPUT)\n        for i in aslist(out.get(\"outputSource\", [])):\n            # source is upstream from output (dependency)\n            nodes[out[\"id\"]].up.append(i)\n            # output is downstream from source\n            declare_node(nodes, i, None)\n            nodes[i].down.append(out[\"id\"])\n\n    for st in tool.tool[\"steps\"]:\n        step = declare_node(nodes, st[\"id\"], STEP)\n        for i in st[\"in\"]:\n            if \"source\" not in i:\n                continue\n            for src in aslist(i[\"source\"]):\n                # source is upstream from step (dependency)\n                step.up.append(src)\n                # step is downstream from source\n                declare_node(nodes, src, None)\n                nodes[src].down.append(st[\"id\"])\n        for out in st[\"out\"]:\n            if isinstance(out, Mapping) and \"id\" in out:\n                out = out[\"id\"]\n            # output is downstream from step\n            step.down.append(out)\n            # step is upstream from output\n            declare_node(nodes, out, None)\n            nodes[out].up.append(st[\"id\"])\n\n    # Find all the downstream nodes from the starting points\n    visited_down: Set[str] = set()\n    for r in roots:\n        if nodes[r].type == OUTPUT:\n            subgraph_visit(r, nodes, visited_down, UP)\n        else:\n            subgraph_visit(r, nodes, visited_down, DOWN)\n\n    # Now make sure all the nodes are connected to upstream inputs\n    visited: Set[str] = set()\n    rewire: Dict[str, Tuple[str, CWLObjectType]] = {}\n    for v in visited_down:\n        visited.add(v)\n        if nodes[v].type in (STEP, OUTPUT):\n            for u in nodes[v].up:\n                if u in visited_down:\n                    continue\n                if nodes[u].type == INPUT:\n                    visited.add(u)\n                else:\n                    # rewire\n                    df = urllib.parse.urldefrag(u)\n                    rn = str(df[0] + \"#\" + df[1].replace(\"/\", \"_\"))\n                    if nodes[v].type == STEP:\n                        wfstep = find_step(tool.steps, v)\n                        if wfstep is not None:\n                            for inp in cast(\n                                MutableSequence[CWLObjectType], wfstep[\"inputs\"]\n                            ):\n                                if \"source\" in inp and u in cast(\n                                    CWLObjectType, inp[\"source\"]\n                                ):\n                                    rewire[u] = (rn, cast(CWLObjectType, inp[\"type\"]))\n                                    break\n                        else:\n                            raise Exception(\"Could not find step %s\" % v)\n\n    extracted = CommentedMap()\n    for f in tool.tool:\n        if f in (\"steps\", \"inputs\", \"outputs\"):\n            extracted[f] = []\n            for i in tool.tool[f]:\n                if i[\"id\"] in visited:\n                    if f == \"steps\":\n                        for inport in i[\"in\"]:\n                            if \"source\" not in inport:\n                                continue\n                            if isinstance(inport[\"source\"], MutableSequence):\n                                inport[\"source\"] = [\n                                    rewire[s][0]\n                                    for s in inport[\"source\"]\n                                    if s in rewire\n                                ]\n                            elif inport[\"source\"] in rewire:\n                                inport[\"source\"] = rewire[inport[\"source\"]][0]\n                    extracted[f].append(i)\n        else:\n            extracted[f] = tool.tool[f]\n\n    for rv in rewire.values():\n        extracted[\"inputs\"].append({\"id\": rv[0], \"type\": rv[1]})\n\n    return extracted\n\n\ndef get_step(tool: Workflow, step_id: str) -> CommentedMap:\n\n    extracted = CommentedMap()\n\n    step = find_step(tool.steps, step_id)\n    if step is None:\n        raise Exception(f\"Step {step_id} was not found\")\n\n    extracted[\"steps\"] = [step]\n    extracted[\"inputs\"] = []\n    extracted[\"outputs\"] = []\n\n    for inport in cast(List[CWLObjectType], step[\"in\"]):\n        name = cast(str, inport[\"id\"]).split(\"#\")[-1].split(\"/\")[-1]\n        extracted[\"inputs\"].append({\"id\": name, \"type\": \"Any\"})\n        inport[\"source\"] = name\n        if \"linkMerge\" in inport:\n            del inport[\"linkMerge\"]\n\n    for outport in cast(List[str], step[\"out\"]):\n        name = outport.split(\"#\")[-1].split(\"/\")[-1]\n        extracted[\"outputs\"].append(\n            {\"id\": name, \"type\": \"Any\", \"outputSource\": f\"{step_id}/{name}\"}\n        )\n\n    for f in tool.tool:\n        if f not in (\"steps\", \"inputs\", \"outputs\"):\n            extracted[f] = tool.tool[f]\n\n    return extracted\n\n\ndef get_process(tool: Workflow, step_id: str, index: Mapping[str, Any]) -> Any:\n    \"\"\"Return just a single Process from a Workflow step.\"\"\"\n    step = find_step(tool.steps, step_id)\n    if step is None:\n        raise Exception(f\"Step {step_id} was not found\")\n\n    run = step[\"run\"]\n\n    if isinstance(run, str):\n        return index[run]\n    else:\n        return run\n", "path": "cwltool/subgraph.py"}]}