Repository: ResearchObject/ro-crate-py
Pull request #65 (2021-05-20 15:30:52)

Delete entities
First draft. See #62.


Patch:
diff --git a/rocrate/model/entity.py b/rocrate/model/entity.py
index 601b2f1..87cffe2 100644
--- a/rocrate/model/entity.py
+++ b/rocrate/model/entity.py
@@ -133,3 +133,6 @@ class Entity(object):
         except AttributeError:
             pass
         self['datePublished'] = value
+
+    def delete(self):
+        self.crate.delete(self)
diff --git a/rocrate/rocrate.py b/rocrate/rocrate.py
index 20a83c2..5c3969d 100644
--- a/rocrate/rocrate.py
+++ b/rocrate/rocrate.py
@@ -31,6 +31,7 @@ from pathlib import Path
 from urllib.parse import urljoin
 
 from .model import contextentity
+from .model.entity import Entity
 from .model.root_dataset import RootDataset
 from .model.file import File
 from .model.dataset import Dataset
@@ -431,6 +432,41 @@ class ROCrate():
             self.__entity_map[key] = e
         return entities[0] if len(entities) == 1 else entities
 
+    def delete(self, *entities):
+        """\
+        Delete one or more entities from this RO-Crate.
+
+        Note that the crate could be left in an inconsistent state as a result
+        of calling this method, since neither entities pointing to the deleted
+        ones nor entities pointed to by the deleted ones are modified.
+        """
+        for e in entities:
+            if not isinstance(e, Entity):
+                e = self.dereference(e)
+            if not e:
+                continue
+            if e is self.root_dataset:
+                raise ValueError("cannot delete the root data entity")
+            if e is self.metadata:
+                raise ValueError("cannot delete the metadata entity")
+            if e is self.preview:
+                self.default_entities.remove(e)
+                self.preview = None
+            elif hasattr(e, "write"):
+                try:
+                    self.data_entities.remove(e)
+                except ValueError:
+                    pass
+                self.root_dataset["hasPart"] = [_ for _ in self.root_dataset["hasPart"] or [] if _ != e]
+                if not self.root_dataset["hasPart"]:
+                    del self.root_dataset._jsonld["hasPart"]
+            else:
+                try:
+                    self.contextual_entities.remove(e)
+                except ValueError:
+                    pass
+            self.__entity_map.pop(e.canonical_id(), None)
+
     # TODO
     # def fetch_all(self):
         # fetch all files defined in the crate


Test patch:
diff --git a/test/test_model.py b/test/test_model.py
index f1f0573..aaf5169 100644
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -23,6 +23,7 @@ from rocrate.rocrate import ROCrate
 from rocrate.model.file import File
 from rocrate.model.computationalworkflow import ComputationalWorkflow
 from rocrate.model.person import Person
+from rocrate.model.preview import Preview
 
 
 RAW_REPO_URL = "https://raw.githubusercontent.com/ResearchObject/ro-crate-py"
@@ -152,3 +153,82 @@ def test_update(test_data_dir, tmpdir, helpers):
     crate.write_crate(out_path)
     json_entities = helpers.read_json_entities(out_path)
     helpers.check_wf_crate(json_entities, wf.id)
+
+
+def test_delete(test_data_dir):
+    crate = ROCrate()
+    # fundamental entities
+    with pytest.raises(ValueError):
+        crate.delete(crate.root_dataset)
+    with pytest.raises(ValueError):
+        crate.delete(crate.metadata)
+    # preview
+    preview = crate.add(Preview(crate))
+    assert preview in crate.default_entities
+    crate.delete(preview)
+    assert preview not in crate.default_entities
+    assert crate.preview is None
+    # data entities
+    file1 = crate.add_file(test_data_dir / "sample_file.txt")
+    file2 = crate.add_file(test_data_dir / "sample_cwl_wf.cwl")
+    for f in file1, file2:
+        assert f in crate.root_dataset["hasPart"]
+        assert f in crate.data_entities
+    crate.delete(file1)
+    assert file1 not in crate.data_entities
+    assert file1 not in crate.root_dataset["hasPart"]
+    crate.delete(file2)
+    assert file2 not in crate.data_entities
+    assert crate.root_dataset["hasPart"] is None
+    crate.delete(file2)  # no-op
+    # contextual entities
+    john = crate.add(Person(crate, '#john', {'name': 'John Doe'}))
+    assert john in crate.contextual_entities
+    crate.delete(john)
+    assert john not in crate.contextual_entities
+    crate.delete(john)  # no-op
+
+
+# FIXME: what to do with refs is still WIP
+def test_delete_refs(test_data_dir, tmpdir, helpers):
+    def_path = "test/test1/sort-and-change-case-test.yml"
+    crate = ROCrate(test_data_dir / 'ro-crate-galaxy-sortchangecase')
+    suite = crate.dereference("#test1")
+    definition = crate.dereference(def_path)
+    assert suite.definition is definition
+    crate.delete(definition)
+    assert suite.definition is not definition  # so far, so good
+    assert suite.definition == str(def_path)  # should probably be set to None
+    crate.write_crate("/tmp/crate_out")
+    # check json output
+    out_path = tmpdir / "ro_crate_out"
+    crate.write_crate(out_path)
+    json_entities = helpers.read_json_entities(out_path)
+    assert def_path not in json_entities  # good
+    assert json_entities["#test1"]["definition"]["@id"] == def_path  # not good
+    # the test suite should not be in the crate at all
+    # even better, the write should fail in such an inconsistent state
+    # or perhaps such an inconsistent state should not be allowed at all
+
+
+def test_delete_by_id(test_data_dir):
+    crate = ROCrate()
+    path = test_data_dir / "sample_file.txt"
+    f = crate.add_file(path)  # with this signature, the file's id will be its basename
+    assert f in crate.data_entities
+    assert f in crate.root_dataset["hasPart"]
+    assert f.id == path.name
+    crate.delete(path.name)
+    assert f not in crate.data_entities
+    assert crate.root_dataset["hasPart"] is None
+
+
+def test_self_delete(test_data_dir):
+    crate = ROCrate()
+    path = test_data_dir / "sample_file.txt"
+    f = crate.add_file(path)  # with this signature, the file's id will be its basename
+    assert f in crate.data_entities
+    assert f in crate.root_dataset["hasPart"]
+    f.delete()
+    assert f not in crate.data_entities
+    assert crate.root_dataset["hasPart"] is None