Repository: ResearchObject/ro-crate-py
Pull request #216 (2025-03-07 15:36:24)

Fix indirect data entities
#161 modified the crate reader to consider entities as data entities if linked not just directly from the root data entity's `hasPart`, but also recursively from the `hasPart` of another data entity. This leads to problems when `hasPart` is used on a `File` to refer to sections of it. For instance, in [ml-predict-pipeline-cwltool-runcrate](https://github.com/ResearchObject/workflow-run-crate/tree/67d6a698beaf88245a8da0038e6033eaabaf87cd/docs/examples/draft/ml-predict-pipeline-cwltool-runcrate) :

```json
{
    "@id": "packed.cwl",
    "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow", "HowTo"],
    "hasPart": [
        {"@id": "packed.cwl#classify_tumor.cwl"},
        {"@id": "packed.cwl#extract_tissue.cwl"}
    ]
},
{
    "@id": "packed.cwl#classify_tumor.cwl",
    "@type": "SoftwareApplication"
}
```

`"packed.cwl#classify_tumor.cwl"` is read in as a data entity. This PR changes `ROCrate.__add_parts` to consider only `hasPart` from `Dataset` entities.


Patch:
diff --git a/rocrate/rocrate.py b/rocrate/rocrate.py
index 906dcf5..cf439a6 100644
--- a/rocrate/rocrate.py
+++ b/rocrate/rocrate.py
@@ -176,7 +176,8 @@ class ROCrate():
                 else:
                     instance = cls(self, source / id_, id_, properties=entity)
             self.add(instance)
-            self.__add_parts(as_list(entity.get("hasPart", [])), entities, source)
+            if instance.type == "Dataset":
+                self.__add_parts(as_list(entity.get("hasPart", [])), entities, source)
 
     def __read_contextual_entities(self, entities):
         type_map = {_.__name__: _ for _ in subclasses(ContextEntity)}


Test patch:
diff --git a/test/test_read.py b/test/test_read.py
index 2eaa0ac..a205a48 100644
--- a/test/test_read.py
+++ b/test/test_read.py
@@ -537,6 +537,9 @@ def test_indirect_data_entity(tmpdir):
     d2_e = crate.dereference("d1/d2")
     assert d2_e
     assert d2_e in crate.data_entities
+    f1_e = crate.dereference("d1/d2/f1")
+    assert f1_e
+    assert f1_e in crate.data_entities
 
 
 @pytest.mark.filterwarnings("ignore")
@@ -597,3 +600,62 @@ def test_from_dict(tmpdir):
     crate.write(out_path)
     with pytest.raises(ValueError):
         ROCrate(metadata, init=True)
+
+
+def test_no_data_entity_link_from_file():
+    metadata = {
+        "@context": "https://w3id.org/ro/crate/1.1/context",
+        "@graph": [
+            {
+                "@id": "ro-crate-metadata.json",
+                "@type": "CreativeWork",
+                "about": {"@id": "./"},
+                "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}
+            },
+            {
+                "@id": "./",
+                "@type": "Dataset",
+                "hasPart": [
+                    {"@id": "d1"},
+                    {"@id": "packed.cwl"}
+                ]
+            },
+            {
+                "@id": "d1",
+                "@type": "Dataset"
+            },
+            {
+                "@id": "packed.cwl",
+                "@type": [
+                    "File",
+                    "SoftwareSourceCode",
+                    "ComputationalWorkflow"
+                ],
+                "hasPart": [
+                    {"@id": "packed.cwl#do_this.cwl"},
+                    {"@id": "packed.cwl#do_that.cwl"}
+                ]
+            },
+            {
+                "@id": "packed.cwl#do_this.cwl",
+                "@type": "SoftwareApplication",
+            },
+            {
+                "@id": "packed.cwl#do_that.cwl",
+                "@type": "SoftwareApplication",
+            }
+        ]
+    }
+    crate = ROCrate(metadata)
+    d1 = crate.dereference("d1")
+    assert d1
+    assert d1 in crate.data_entities
+    assert d1 not in crate.contextual_entities
+    wf = crate.dereference("packed.cwl")
+    assert wf
+    assert wf in crate.data_entities
+    assert wf not in crate.contextual_entities
+    t1 = crate.dereference("packed.cwl#do_this.cwl")
+    assert t1
+    assert t1 not in crate.data_entities
+    assert t1 in crate.contextual_entities