Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/backend-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ editor. Runtime configuration changes and process failure may require a restart.
unrelated files keep their source directories in place.
Function-directory moves are preflighted and rolled back if
the model move fails. Saving a deleted folder also removes child functions.
Empty directory cleanup checks each directory after removing its children, so
a deleted folder does not remain solely because it contained empty subdirectories.
- Function source paths are relative to their model entity. Absolute/drive-qualified
paths, traversal, empty segments and symlink escapes are rejected.
- A bare function filename first resolves beside the entity JSON, then in its
Expand Down
4 changes: 2 additions & 2 deletions src/datam8/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ def cleanup_directory(path: Path, up_to: Path) -> None:
"""

# remove children
for dir_path, dir_names, file_names in path.walk(top_down=False):
if len(dir_names) == 0 and len(file_names) == 0:
for dir_path, _, _ in path.walk(top_down=False):
if not any(dir_path.iterdir()):
delete_path(dir_path)

# remove empty parents
Expand Down
27 changes: 27 additions & 0 deletions tests/test_020_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,36 @@
UuidCases,
)

from datam8.utils import cleanup_directory
from datam8.utils import hasher as hashutils


@pytest.mark.parametrize("folder", ["Root", "Core/Nested"])
def test_cleanup_removes_parent_after_empty_descendants(tmp_path, folder):
target = tmp_path / "Model" / folder
(target / "Empty" / "Nested").mkdir(parents=True)
retained = tmp_path / "Model" / "Neighbour" / "keep.txt"
retained.parent.mkdir(parents=True)
retained.write_text("keep", encoding="utf-8")

cleanup_directory(target, tmp_path)

assert not target.exists()
assert retained.read_text(encoding="utf-8") == "keep"


def test_cleanup_preserves_directories_with_remaining_files(tmp_path):
target = tmp_path / "Model" / "Root"
(target / "Empty" / "Nested").mkdir(parents=True)
retained = target / "keep.txt"
retained.write_text("keep", encoding="utf-8")

cleanup_directory(target, tmp_path)

assert not (target / "Empty").exists()
assert retained.read_text(encoding="utf-8") == "keep"


@parametrize_with_cases("algorithm", cases=AlgorithmCases, glob="*_valid")
@parametrize_with_cases("input", cases=HashCases, glob="*_valid")
def test_hasher_hash(input, algorithm):
Expand Down
Loading