Repository: common-workflow-language/cwltool

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




Discussion:

Resolving patch:
diff --git a/cwltool/subgraph.py b/cwltool/subgraph.py
--- a/cwltool/subgraph.py
+++ b/cwltool/subgraph.py
@@ -92,6 +92,8 @@ def get_subgraph(roots: MutableSequence[str], tool: Workflow) -> CommentedMap:
                 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


Test patch:
diff --git a/tests/subgraph/1432.cwl b/tests/subgraph/1432.cwl
new file mode 100644
--- /dev/null
+++ b/tests/subgraph/1432.cwl
@@ -0,0 +1,29 @@
+class: Workflow
+cwlVersion: v1.2
+inputs:
+  - id: step1_in
+    type: string
+  - id: step2_in
+    type: string
+outputs:
+  - id: step1_out
+    type: string
+    outputSource: step1/out
+  - id: step2_out
+    type: string
+    outputSource: step2/out
+steps:
+  - id: step1
+    in:
+      - id: inp
+        source: step1_in
+    out:
+      - id: out
+    run: ../echo.cwl
+  - id: step2
+    in:
+      - id: inp
+        source: step2_in
+    out:
+      - id: out
+    run: ../echo.cwl
diff --git a/tests/subgraph/extract_step2_1432.json b/tests/subgraph/extract_step2_1432.json
new file mode 100644
--- /dev/null
+++ b/tests/subgraph/extract_step2_1432.json
@@ -0,0 +1,35 @@
+{
+    "class": "Workflow",
+    "cwlVersion": "v1.2",
+    "id": "subgraph/1432.cwl",
+    "inputs": [
+        {
+            "id": "subgraph/1432.cwl#step2_in",
+            "type": "string"
+        }
+    ],
+    "outputs": [
+        {
+            "id": "subgraph/1432.cwl#step2_out",
+            "outputSource": "subgraph/1432.cwl#step2/out",
+            "type": "string"
+        }
+    ],
+    "steps": [
+        {
+            "id": "subgraph/1432.cwl#step2",
+            "in": [
+                {
+                    "id": "subgraph/1432.cwl#step2/inp",
+                    "source": "subgraph/1432.cwl#step2_in"
+                }
+            ],
+            "out": [
+                {
+                    "id": "subgraph/1432.cwl#step2/out"
+                }
+            ],
+            "run": "echo.cwl"
+        }
+    ]
+}
diff --git a/tests/test_subgraph.py b/tests/test_subgraph.py
--- a/tests/test_subgraph.py
+++ b/tests/test_subgraph.py
@@ -12,7 +12,20 @@
 from .util import get_data
 
 
+def clean(val: Any, path: str) -> Any:
+    """Remove the path prefix from an string values."""
+    if isinstance(val, str):
+        if val.startswith(path):
+            return val[len(path) + 1 :]
+    if isinstance(val, dict):
+        return {k: clean(v, path) for k, v in val.items()}
+    if isinstance(val, list):
+        return [clean(v, path) for v in val]
+    return val
+
+
 def test_get_subgraph() -> None:
+    """Compare known correct subgraphs to generated subgraphs."""
     loadingContext = LoadingContext({"construct_tool_object": default_make_tool})
     wf = Path(get_data("tests/subgraph/count-lines1-wf.cwl")).as_uri()
     loadingContext.do_update = False
@@ -20,16 +33,6 @@ def test_get_subgraph() -> None:
 
     sg = Path(get_data("tests/subgraph")).as_uri()
 
-    def clean(val: Any) -> Any:
-        if isinstance(val, str):
-            if val.startswith(sg):
-                return val[len(sg) + 1 :]
-        if isinstance(val, dict):
-            return {k: clean(v) for k, v in val.items()}
-        if isinstance(val, list):
-            return [clean(v) for v in val]
-        return val
-
     for a in (
         "file1",
         "file2",
@@ -47,7 +50,22 @@ def clean(val: Any) -> Any:
         assert isinstance(tool, Workflow)
         extracted = get_subgraph([wf + "#" + a], tool)
         with open(get_data("tests/subgraph/extract_" + a + ".json")) as f:
-            assert json.load(f) == clean(convert_to_dict(extracted))
+            assert json.load(f) == clean(convert_to_dict(extracted), sg)
+
+
+def test_get_subgraph_long_out_form() -> None:
+    """Compare subgraphs generatation when 'out' is in the long form."""
+    loadingContext = LoadingContext({"construct_tool_object": default_make_tool})
+    wf = Path(get_data("tests/subgraph/1432.cwl")).as_uri()
+    loadingContext.do_update = False
+    tool = load_tool(wf, loadingContext)
+
+    sg = Path(get_data("tests/")).as_uri()
+
+    assert isinstance(tool, Workflow)
+    extracted = get_subgraph([wf + "#step2"], tool)
+    with open(get_data("tests/subgraph/extract_step2_1432.json")) as f:
+        assert json.load(f) == clean(convert_to_dict(extracted), sg)
 
 
 def test_get_step() -> None:
@@ -59,16 +77,6 @@ def test_get_step() -> None:
 
     sg = Path(get_data("tests/subgraph")).as_uri()
 
-    def clean(val: Any) -> Any:
-        if isinstance(val, str):
-            if val.startswith(sg):
-                return val[len(sg) + 1 :]
-        if isinstance(val, dict):
-            return {k: clean(v) for k, v in val.items()}
-        if isinstance(val, list):
-            return [clean(v) for v in val]
-        return val
-
     for a in (
         "step1",
         "step2",
@@ -78,4 +86,4 @@ def clean(val: Any) -> Any:
     ):
         extracted = get_step(tool, wf + "#" + a)
         with open(get_data("tests/subgraph/single_" + a + ".json")) as f:
-            assert json.load(f) == clean(convert_to_dict(extracted))
+            assert json.load(f) == clean(convert_to_dict(extracted), sg)
diff --git a/tests/test_validate_js.py b/tests/test_validate_js.py
--- a/tests/test_validate_js.py
+++ b/tests/test_validate_js.py
@@ -1,5 +1,3 @@
-from typing import Any
-
 import pytest
 from schema_salad.avro.schema import Names
 from schema_salad.utils import yaml_no_ts