Skip to content
Merged
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
23 changes: 23 additions & 0 deletions src/ndi/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ def __init__(self, document_type: Union[str, dict, "ndi_document"] = "base", **k
if isinstance(document_type, dict):
# Loading from existing properties
self._document_properties = deepcopy(document_type)
self._normalize_depends_on()
elif isinstance(document_type, ndi_document):
# Copy from another ndi_document
self._document_properties = deepcopy(document_type.document_properties)
elif DIDDocument is not None and isinstance(document_type, DIDDocument):
# Convert from DIDDocument
self._document_properties = deepcopy(document_type.document_properties)
self._normalize_depends_on()
else:
# Create new from schema
self._document_properties = self.read_blank_definition(document_type)
Expand All @@ -92,6 +94,27 @@ def __init__(self, document_type: Union[str, dict, "ndi_document"] = "base", **k
for key, value in kwargs.items():
self._set_nested_property(key, value)

def _normalize_depends_on(self):
"""Ensure depends_on and files.file_info are always lists.

MATLAB's jsonencode converts single-element cell arrays to scalars,
so documents downloaded from the cloud (or created by MATLAB) may
have ``depends_on`` or ``files.file_info`` as a bare dict instead
of a list. Normalizing here guarantees that downstream code
(including DID-python's ``doc_to_sql`` / ``_serialize_depends_on``
and all NDI code that iterates over ``file_info``) always sees
a list.
"""
dep = self._document_properties.get("depends_on")
if isinstance(dep, dict):
self._document_properties["depends_on"] = [dep]

files = self._document_properties.get("files")
if isinstance(files, dict):
fi = files.get("file_info")
if isinstance(fi, dict):
files["file_info"] = [fi]

@property
def document_properties(self) -> dict:
"""The document's properties as a nested dictionary."""
Expand Down
45 changes: 45 additions & 0 deletions tests/test_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,51 @@ def test_find_dependencies(self, temp_session):
assert deps[0].id == parent_ido.id


class TestDatabaseDependsSQLite:
"""Test that depends_on is correctly stored in SQLite doc_data table."""

def test_depends_on_bare_dict_stored_in_doc_data(self, temp_session):
"""Test that a bare dict depends_on (MATLAB-style) is stored in doc_data."""
db = ndi_database(temp_session)

parent_ido = ndi_ido()
parent = ndi_document(
{
"base": {
"id": parent_ido.id,
"datestamp": timestamp(),
"name": "parent",
"session_id": "",
},
"document_class": {"class_name": "base", "superclasses": []},
}
)
db.add(parent)

child_ido = ndi_ido()
# Use bare dict (MATLAB-style) for depends_on
child = ndi_document(
{
"base": {
"id": child_ido.id,
"datestamp": timestamp(),
"name": "child",
"session_id": "",
},
"document_class": {"class_name": "base", "superclasses": []},
"depends_on": {"name": "parent_doc", "value": parent_ido.id},
}
)
db.add(child)

# Verify depends_on was stored in doc_data by querying with depends_on
from ndi.query import ndi_query

results = db.search(ndi_query("").depends_on("parent_doc", parent_ido.id))
assert len(results) == 1
assert results[0].id == child_ido.id


class TestDatabasePaths:
"""Test ndi_database path properties."""

Expand Down
31 changes: 31 additions & 0 deletions tests/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,37 @@ def test_dependency_with_deps(self):
assert "element_id" in names
assert len(deps) == 2

def test_depends_on_bare_dict_normalized_to_list(self):
"""Test that a bare dict depends_on (MATLAB-style) is normalized to a list."""
props = {
"base": {"id": "id", "datestamp": "", "session_id": ""},
"document_class": {"class_name": "base", "superclasses": []},
"depends_on": {"name": "element_id", "value": "abc123"},
}
doc = ndi_document(props)
# Should be normalized to a list
assert isinstance(doc.document_properties["depends_on"], list)
assert len(doc.document_properties["depends_on"]) == 1
assert doc.dependency_value("element_id") == "abc123"

def test_file_info_bare_dict_normalized_to_list(self):
"""Test that a bare dict file_info (MATLAB-style) is normalized to a list."""
props = {
"base": {"id": "id", "datestamp": "", "session_id": ""},
"document_class": {"class_name": "base", "superclasses": []},
"files": {
"file_info": {
"name": "data.bin",
"locations": {"location": "/tmp/data.bin"},
}
},
}
doc = ndi_document(props)
fi = doc.document_properties["files"]["file_info"]
assert isinstance(fi, list)
assert len(fi) == 1
assert fi[0]["name"] == "data.bin"

def test_dependency_value(self):
"""Test dependency_value returns correct value."""
props = {
Expand Down
Loading