Skip to content

Commit 3497d75

Browse files
Merge pull request #86 from VH-Lab/claude/did-python-matlab-sync-vlzx8g
Close the fail-open in check_files' step 3 (DID-matlab#200)
2 parents 5df213c + 0630e0c commit 3497d75

3 files changed

Lines changed: 186 additions & 4 deletions

File tree

‎src/did/did_matlab_python_bridge.yaml‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ classes:
4040
- name: database
4141
type: class
4242
matlab_path: "+did/database.m"
43-
matlab_last_sync_hash: "ec1e733"
43+
matlab_last_sync_hash: "9b36124"
4444
status: ported
4545
python_path: "did/database.py"
4646
python_class: "Database"
@@ -59,6 +59,14 @@ classes:
5959
MissingOptionalDependencyWarning gated on the
6060
DID_FORCE_VALIDATION_WARNINGS environment variable.
6161
62+
2026-09-09: bumped through DID-matlab 9b36124 (PR #200). Only
63+
check_files needed a change -- the fail-open on a required file
64+
declared in file_list with nothing bound to it in file_info, same
65+
shape as PR #182's step-1 fail-open, one step down. See the checkfiles
66+
method entry for the port. The rest of PR #200 was MATLAB-only: a
67+
try/catch that read file_info before file_list under one arm, which
68+
Python does not share.
69+
6270
Reviewed 2026-08-29 through MATLAB c0a9507, nothing to port. Two
6371
MATLAB fixes landed in this window and Python has neither defect:
6472
@@ -768,6 +776,20 @@ classes:
768776
meant a coverage check could not tell that isfilenamematch and
769777
canfindonefile were bridged at all. Split 2026-08-28.
770778
779+
2026-09-09 (DID-matlab#200 / #199): step 3 used to loop over the
780+
actual file_info entries and fall through to is_valid = True when a
781+
required name had none -- the same fail-open PR #182 closed for
782+
step 1, one step further down. Ported here: check_files now
783+
separates the two absences, keeping the file_list message for a
784+
name missing from the list and adding a new one for a name that IS
785+
in the file_list with nothing bound for it (naming the file and
786+
file_info). The other half of DID-matlab#200, an independent read
787+
of file_list before file_info, is not a bug on the Python side --
788+
_validate_files already reads the two fields independently and did
789+
not share MATLAB's try/catch shape -- so no code change beyond
790+
check_files was needed. Coverage:
791+
tests/test_validation.py:TestFileValidationDiagnosis.
792+
771793
- name: isfilenamematch
772794
kind: static
773795
python_name: is_filename_match

‎src/did/validate.py‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,15 @@ def check_files(
640640
641641
Returns ``(is_valid, error_message)``. Mirrors MATLAB
642642
``database.checkfiles``.
643+
644+
Steps 1 and 3 report DIFFERENT ABSENCES. A schema-required name can be
645+
missing from the document's file_list (step 1), or it can be there in the
646+
file_list -- declared, correctly -- with nothing bound to it in file_info
647+
(step 3). The second is the common one, and step 3 used to fall out of its
648+
empty match loop without raising: a required file was absent and the
649+
document was accepted anyway. That is the same fail-open MATLAB PR #182
650+
closed for step 1, one step further down, and DID-matlab#199 / #200 closed
651+
for step 3.
643652
"""
644653
expected_unique = sorted(set(expected_names))
645654
actual_unique = sorted(set(actual_file_names))
@@ -665,9 +674,23 @@ def check_files(
665674
if not _is_truthy(expected_value):
666675
continue
667676
item_name = expected_names[index]
668-
for file_index, actual in enumerate(actual_file_names):
669-
if not is_filename_match(item_name, actual):
670-
continue
677+
matches = [
678+
file_index
679+
for file_index, actual in enumerate(actual_file_names)
680+
if is_filename_match(item_name, actual)
681+
]
682+
if not matches:
683+
# The name IS in the document's file_list -- step 1 above
684+
# established that -- but nothing is bound to it in file_info, so
685+
# there is no file to look for. Say that, and say it about
686+
# file_info: the file_list is correct here, and blaming it is what
687+
# cost the debugging time in DID-matlab#199.
688+
return False, (
689+
f'Required file "{item_name}" is declared in the file_list of '
690+
f"{doc_name} but no file is bound to that name "
691+
f"(files.file_info has no entry for it)"
692+
)
693+
for file_index in matches:
671694
entry = files[file_index] if file_index < len(files) else {}
672695
locations = entry.get("locations") if isinstance(entry, dict) else None
673696
if not can_find_one_file(locations):

‎tests/test_validation.py‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,140 @@ def test_the_url_survives_the_round_trip(self):
404404
entries = entries if isinstance(entries, list) else [entries]
405405
locations.extend(item.get("location") for item in entries)
406406
self.assertIn(self.URL, locations)
407+
408+
409+
class TestFileValidationDiagnosis(_DatabaseTestCase):
410+
"""WHICH FIELD a file-validation failure blames.
411+
412+
check_files' step 3 used to loop over the actual file_info entries: a
413+
required name declared in the file_list with nothing bound to it in
414+
file_info matched nothing and fell through to is_valid = True. That is
415+
the same fail-open MATLAB PR #182 closed for step 1, one step further
416+
down; DID-matlab#199 closed it for step 3. Python's _validate_files
417+
already read file_info and file_list independently -- MATLAB's own bug
418+
from that PR was a try/catch shape Python does not share -- but the
419+
fail-open in check_files itself was shared, so the port is that fix.
420+
"""
421+
422+
def _doc_with_file_info(self, file_info, file_list=None):
423+
"""A demoFile document whose files section is set by the caller.
424+
425+
demoFile declares filename1.ext and filename2.ext, both
426+
mustbenotempty.
427+
"""
428+
doc = Document("demoFile", **{"demoFile.value": 1})
429+
doc.document_properties["files"]["file_info"] = file_info
430+
if file_list is not None:
431+
doc.document_properties["files"]["file_list"] = file_list
432+
return doc
433+
434+
def _assert_refused(self, doc):
435+
with self.assertRaises(ValidationError) as caught:
436+
self.db.add_docs([doc])
437+
self.assertEqual(
438+
caught.exception.identifier,
439+
"DID:Database:ValidationFiles",
440+
f"refused for the wrong reason: {caught.exception}",
441+
)
442+
return str(caught.exception)
443+
444+
def test_empty_file_info_blames_the_unbound_file_not_the_file_list(self):
445+
# The reported case. file_list declares both names and is correct;
446+
# nothing is bound to them. file_info = [] used to fall out of the
447+
# empty match loop and be accepted (Python) or throw and get the
448+
# file_list blamed (MATLAB).
449+
doc = self._doc_with_file_info([])
450+
451+
msg = self._assert_refused(doc)
452+
453+
self.assertIn(
454+
"filename1.ext",
455+
msg,
456+
"the message must name the file that is not bound",
457+
)
458+
self.assertIn(
459+
"file_info",
460+
msg,
461+
"the message must name the field that is actually empty",
462+
)
463+
self.assertNotIn(
464+
"from the file_list",
465+
msg,
466+
(
467+
"the file_list declares the file and is correct, so the "
468+
f"message must not send the reader there: {msg}"
469+
),
470+
)
471+
472+
def test_name_missing_from_file_list_names_the_absent_name(self):
473+
# A document whose file_list is genuinely incomplete: it holds
474+
# filename1.ext and not filename2.ext. The absent name is
475+
# filename2.ext, and that is what the message must say.
476+
doc = self._doc_with_file_info([], file_list=["filename1.ext"])
477+
478+
msg = self._assert_refused(doc)
479+
480+
self.assertIn(
481+
"file_list",
482+
msg,
483+
"a name absent from the file_list IS a file_list problem",
484+
)
485+
self.assertIn(
486+
"filename2.ext",
487+
msg,
488+
"the message must name the file that is absent from the list",
489+
)
490+
self.assertNotIn(
491+
"filename1.ext",
492+
msg,
493+
(
494+
"filename1.ext is in the file_list; only filename2.ext is "
495+
f"absent from it: {msg}"
496+
),
497+
)
498+
499+
def test_document_with_no_files_field_still_validates(self):
500+
# Most classes declare no files at all, and their documents have no
501+
# files section. That must keep working.
502+
doc = Document("demoA", **{"demoA.value": 1})
503+
self.assertNotIn(
504+
"files",
505+
doc.document_properties,
506+
"precondition: a demoA document has no files section",
507+
)
508+
self.db.add_docs([doc])
509+
self.assertIsNotNone(self.db.get_docs(doc.id()))
510+
511+
def test_declared_optional_files_need_not_be_bound(self):
512+
# demoSeries declares both of its files mustbenotempty = 0, so a
513+
# document that binds neither is valid: the file_list is correct
514+
# and nothing it declares is required.
515+
doc = Document("demoSeries", **{"demoSeries.value": 1})
516+
doc.document_properties["files"]["file_info"] = []
517+
self.db.add_docs([doc])
518+
self.assertIsNotNone(self.db.get_docs(doc.id()))
519+
520+
def test_bound_required_files_validate(self):
521+
# The other side of the tightening: a document that binds what its
522+
# schema requires still validates. Validation does no file I/O, so
523+
# the locations need not exist yet.
524+
doc = Document("demoFile", **{"demoFile.value": 1})
525+
doc.add_file("filename1.ext", "placeholder", ingest=0, delete_original=0)
526+
doc.add_file("filename2.ext", "placeholder", ingest=0, delete_original=0)
527+
self.db.add_docs([doc])
528+
self.assertIsNotNone(self.db.get_docs(doc.id()))
529+
530+
def test_check_files_directly_reports_unbound_required_file(self):
531+
# The unit-level counterpart, calling check_files without a database.
532+
is_valid, message = check_files(
533+
expected_names=["filename1.ext", "filename2.ext"],
534+
must_have_value=[1, 1],
535+
actual_file_names=[],
536+
doc_name="demoFile doc X",
537+
files=[],
538+
actual_file_list=["filename1.ext", "filename2.ext"],
539+
)
540+
self.assertFalse(is_valid)
541+
self.assertIn("filename1.ext", message)
542+
self.assertIn("file_info", message)
543+
self.assertNotIn("from the file_list", message)

0 commit comments

Comments
 (0)