Repository: ResearchObject/ro-crate-py
Pull request #151 (2023-05-29 13:35:23)

Add add_tree method
Adds an `add_tree` method to recursively add a directory along with its contents to the JSON-LD metadata (`add_dataset` already adds contents recursively to the crate's payload), listing directory contents in the `hasPart` of the `Dataset` instances.


Patch:
diff --git a/README.md b/README.md
index 0774b61..92a5311 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,9 @@ You can also add whole directories. A directory in RO-Crate is represented by th
 logs = crate.add_dataset("exp/logs")
 ```
 
+Note that the above adds all files and directories contained in `"exp/logs"` recursively to the crate, but only the top-level `"exp/logs"` dataset itself is listed in the metadata file (there is no requirement to represent every file and folder in the JSON-LD). To also add files and directory recursively to the metadata, use `add_tree` (but note that it only works on local directory trees).
+
+
 #### Appending elements to property values
 
 What ro-crate-py entities actually store is their JSON representation:
diff --git a/rocrate/rocrate.py b/rocrate/rocrate.py
index e209495..76f9105 100644
--- a/rocrate/rocrate.py
+++ b/rocrate/rocrate.py
@@ -20,6 +20,7 @@ import errno
 import uuid
 import zipfile
 import atexit
+import os
 import shutil
 import tempfile
 
@@ -323,6 +324,21 @@ class ROCrate():
 
     add_directory = add_dataset
 
+    def add_tree(self, source, dest_path=None, properties=None):
+        if not source:
+            raise ValueError("source must refer to an existing local directory")
+        top = self.add_dataset(source, dest_path=dest_path)
+        dest_path = Path(top.id).as_posix()
+        for e in os.scandir(source):
+            dest = dest_path / Path(e.path).relative_to(source)
+            if e.is_file():
+                file_ = self.add_file(source=e.path, dest_path=dest)
+                top.append_to("hasPart", file_)
+            if e.is_dir():
+                dir_ = self.add_tree(e.path, dest_path=dest)
+                top.append_to("hasPart", dir_)
+        return top
+
     def add(self, *entities):
         """\
         Add one or more entities to this RO-Crate.


Test patch:
diff --git a/test/test_write.py b/test/test_write.py
index 7f41f1b..b2f264e 100644
--- a/test/test_write.py
+++ b/test/test_write.py
@@ -393,3 +393,42 @@ def test_no_zip_in_zip(test_data_dir, tmpdir):
         zf.extractall(out_path)
 
     assert not (out_path / zip_name).exists()
+
+
+def test_add_tree(test_data_dir, tmpdir):
+    source = test_data_dir / "read_extra"
+    (source / "ro-crate-metadata.json").unlink()
+    crate = ROCrate()
+    crate.add_tree(source)
+    out_path = tmpdir / 'ro_crate_out'
+    crate.write(out_path)
+
+    assert (out_path / "read_extra").is_dir()
+    assert (out_path / "read_extra" / "listed").is_dir()
+    assert (out_path / "read_extra" / "listed" / "listed.txt").is_file()
+    assert (out_path / "read_extra" / "listed" / "not_listed.txt").is_file()
+    assert (out_path / "read_extra" / "not_listed").is_dir()
+    assert (out_path / "read_extra" / "not_listed" / "not_listed.txt").is_file()
+    assert (out_path / "read_extra" / "listed.txt").is_file()
+    assert (out_path / "read_extra" / "not_listed.txt").is_file()
+
+    crate = ROCrate(out_path)
+    top = crate.get("read_extra")
+    assert top.type == "Dataset"
+    listed = crate.get("read_extra/listed")
+    assert listed.type == "Dataset"
+    not_listed = crate.get("read_extra/not_listed")
+    assert not_listed.type == "Dataset"
+    listed_txt = crate.get("read_extra/listed.txt")
+    assert listed_txt.type == "File"
+    not_listed_txt = crate.get("read_extra/not_listed.txt")
+    assert not_listed_txt.type == "File"
+    assert set(top["hasPart"]) == {listed, not_listed, listed_txt, not_listed_txt}
+    listed_listed_txt = crate.get("read_extra/listed/listed.txt")
+    listed_not_listed_txt = crate.get("read_extra/listed/not_listed.txt")
+    assert set(listed["hasPart"]) == {listed_listed_txt, listed_not_listed_txt}
+    not_listed_not_listed_txt = crate.get("read_extra/not_listed/not_listed.txt")
+    assert set(not_listed["hasPart"]) == {not_listed_not_listed_txt}
+
+    with pytest.raises(ValueError):
+        crate.add_tree(None, dest_path="foobar")