From 0e9e2351c22f8016c36392ba99e8bf3a4df24f76 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 19:04:48 +0000 Subject: [PATCH 1/2] Fix depends_on not stored in doc_data for MATLAB-style documents MATLAB's jsonencode converts single-element cell arrays to scalars, so documents downloaded from the cloud (or created by MATLAB) may have depends_on as a bare dict {"name": "x", "value": "y"} instead of a list [{"name": "x", "value": "y"}]. DID-python's _serialize_depends_on rejects non-list depends_on and returns "", leaving field 6 in doc_data blank. Normalize depends_on to always be a list in ndi_document.__init__ when loading from a dict or DIDDocument, so downstream code always sees the correct format. https://claude.ai/code/session_0169wc3G2LGv6cq86MYzo7dp --- src/ndi/document.py | 15 ++++++++++++++ tests/test_database.py | 45 ++++++++++++++++++++++++++++++++++++++++++ tests/test_document.py | 13 ++++++++++++ 3 files changed, 73 insertions(+) diff --git a/src/ndi/document.py b/src/ndi/document.py index 24615f0..717e851 100644 --- a/src/ndi/document.py +++ b/src/ndi/document.py @@ -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) @@ -92,6 +94,19 @@ 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 is always a list of dicts. + + MATLAB's jsonencode converts single-element cell arrays to scalars, + so documents downloaded from the cloud (or created by MATLAB) may + have ``depends_on`` as a bare dict instead of a list. Normalizing + here guarantees that downstream code (including DID-python's + ``doc_to_sql`` / ``_serialize_depends_on``) always sees a list. + """ + dep = self._document_properties.get("depends_on") + if isinstance(dep, dict): + self._document_properties["depends_on"] = [dep] + @property def document_properties(self) -> dict: """The document's properties as a nested dictionary.""" diff --git a/tests/test_database.py b/tests/test_database.py index bec9bc0..6e384b4 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -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.""" diff --git a/tests/test_document.py b/tests/test_document.py index 120c982..ebc8846 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -178,6 +178,19 @@ 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_dependency_value(self): """Test dependency_value returns correct value.""" props = { From 37c1ac9007412eb69eabe4fd833eaf0ec0c14216 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 19:13:42 +0000 Subject: [PATCH 2/2] Normalize files.file_info bare dict to list (same MATLAB issue) MATLAB's jsonencode also converts single-element file_info arrays to bare dicts. Many places in NDI iterate over file_info assuming a list, which would silently iterate over dict keys instead of file entries. Extend the existing _normalize_depends_on to also wrap a bare dict files.file_info into a single-element list. https://claude.ai/code/session_0169wc3G2LGv6cq86MYzo7dp --- src/ndi/document.py | 16 ++++++++++++---- tests/test_document.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/ndi/document.py b/src/ndi/document.py index 717e851..a497020 100644 --- a/src/ndi/document.py +++ b/src/ndi/document.py @@ -95,18 +95,26 @@ def __init__(self, document_type: Union[str, dict, "ndi_document"] = "base", **k self._set_nested_property(key, value) def _normalize_depends_on(self): - """Ensure depends_on is always a list of dicts. + """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`` as a bare dict instead of a list. Normalizing - here guarantees that downstream code (including DID-python's - ``doc_to_sql`` / ``_serialize_depends_on``) always sees a list. + 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.""" diff --git a/tests/test_document.py b/tests/test_document.py index ebc8846..51b6c07 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -191,6 +191,24 @@ def test_depends_on_bare_dict_normalized_to_list(self): 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 = {