3232from dandi .validate ._types import (
3333 ORIGIN_INTERNAL_DANDI ,
3434 ORIGIN_VALIDATION_DANDI ,
35+ ORIGIN_VALIDATION_DANDI_LAYOUT ,
36+ MissingFileContent ,
3537 Origin ,
3638 OriginType ,
3739 Scope ,
@@ -91,9 +93,18 @@ def get_validation_errors(
9193 self ,
9294 schema_version : str | None = None ,
9395 devel_debug : bool = False ,
96+ missing_file_content : MissingFileContent | None = None ,
9497 ) -> list [ValidationResult ]:
9598 """
9699 Attempt to validate the file and return a list of errors encountered
100+
101+ Parameters
102+ ----------
103+ missing_file_content : MissingFileContent | None
104+ When set (not None), the file's content is known to be unavailable
105+ (e.g. broken symlink). Validators should adjust behaviour
106+ accordingly — for ``only-non-data`` they should skip
107+ content-dependent checks and emit a WARNING.
97108 """
98109 ...
99110
@@ -116,6 +127,7 @@ def get_validation_errors(
116127 self ,
117128 schema_version : str | None = None ,
118129 devel_debug : bool = False ,
130+ missing_file_content : MissingFileContent | None = None ,
119131 ) -> list [ValidationResult ]:
120132 with open (self .filepath ) as f :
121133 meta = yaml_load (f , typ = "safe" )
@@ -185,7 +197,14 @@ def get_validation_errors(
185197 self ,
186198 schema_version : str | None = None ,
187199 devel_debug : bool = False ,
200+ missing_file_content : MissingFileContent | None = None ,
188201 ) -> list [ValidationResult ]:
202+ # When file content is unavailable and policy is only-non-data,
203+ # skip metadata extraction (which requires reading the file) and
204+ # only do path-based validation via subclass hooks.
205+ if missing_file_content == MissingFileContent .only_non_data :
206+ return []
207+
189208 current_version = get_schema_version ()
190209 if schema_version is None :
191210 schema_version = current_version
@@ -511,81 +530,101 @@ def get_validation_errors(
511530 self ,
512531 schema_version : str | None = None ,
513532 devel_debug : bool = False ,
533+ missing_file_content : MissingFileContent | None = None ,
514534 ) -> list [ValidationResult ]:
515535 """
516536 Validate NWB asset
517537
518538 If ``schema_version`` was provided, we only validate basic metadata,
519- and completely skip validation using nwbinspector.inspect_nwbfile
520- """
521- # Avoid heavy import by importing within function:
522- from nwbinspector import Importance , inspect_nwbfile , load_config
539+ and completely skip validation using nwbinspector.inspect_nwbfile.
523540
524- # Avoid heavy import by importing within function:
525- from dandi .pynwb_utils import validate as pynwb_validate
541+ If ``missing_file_content`` is ``only-non-data``, content-dependent
542+ validators (pynwb, nwbinspector) are skipped and only path-layout
543+ validation is performed.
544+ """
545+ errors : list [ValidationResult ] = []
526546
527- errors : list [ValidationResult ] = pynwb_validate (
528- self .filepath , devel_debug = devel_debug
529- )
530- if schema_version is not None :
531- errors .extend (
532- super ().get_validation_errors (
533- schema_version = schema_version , devel_debug = devel_debug
547+ if missing_file_content == MissingFileContent .only_non_data :
548+ # Skip content-dependent validators; emit a warning
549+ errors .append (
550+ ValidationResult (
551+ id = "DANDI.FILE_CONTENT_MISSING_PARTIAL" ,
552+ origin = ORIGIN_VALIDATION_DANDI_LAYOUT ,
553+ severity = Severity .WARNING ,
554+ scope = Scope .FILE ,
555+ path = self .filepath ,
556+ dandiset_path = self .dandiset_path ,
557+ message = (
558+ "File content is not available; "
559+ "skipping content-dependent validators "
560+ "(pynwb, nwbinspector). Only path layout is validated."
561+ ),
534562 )
535563 )
536564 else :
537- # make sure that we have some basic metadata fields we require
538- try :
539- origin_validation_nwbinspector = Origin (
540- type = OriginType .VALIDATION ,
541- validator = Validator .nwbinspector ,
542- validator_version = str (_get_nwb_inspector_version ()),
565+ # Avoid heavy import by importing within function:
566+ from nwbinspector import Importance , inspect_nwbfile , load_config
567+
568+ # Avoid heavy import by importing within function:
569+ from dandi .pynwb_utils import validate as pynwb_validate
570+
571+ errors .extend (pynwb_validate (self .filepath , devel_debug = devel_debug ))
572+ if schema_version is not None :
573+ errors .extend (
574+ super ().get_validation_errors (
575+ schema_version = schema_version , devel_debug = devel_debug
576+ )
543577 )
578+ else :
579+ # make sure that we have some basic metadata fields we require
580+ try :
581+ origin_validation_nwbinspector = Origin (
582+ type = OriginType .VALIDATION ,
583+ validator = Validator .nwbinspector ,
584+ validator_version = str (_get_nwb_inspector_version ()),
585+ )
544586
545- for error in inspect_nwbfile (
546- nwbfile_path = self .filepath ,
547- skip_validate = True ,
548- config = load_config (filepath_or_keyword = "dandi" ),
549- importance_threshold = Importance .BEST_PRACTICE_VIOLATION ,
550- # we might want to switch to a lower threshold once nwbinspector
551- # upstream reporting issues are clarified:
552- # https://github.com/dandi/dandi-cli/pull/1162#issuecomment-1322238896
553- # importance_threshold=Importance.BEST_PRACTICE_SUGGESTION,
554- ):
555- severity = NWBI_IMPORTANCE_TO_DANDI_SEVERITY [error .importance .name ]
556- kw : Any = {}
557- if error .location :
558- kw ["within_asset_paths" ] = {
559- error .file_path : error .location ,
560- }
561- errors .append (
562- ValidationResult (
563- origin = origin_validation_nwbinspector ,
564- severity = severity ,
565- id = f"NWBI.{ error .check_function_name } " ,
566- scope = Scope .FILE ,
567- origin_result = error ,
568- path = Path (error .file_path ),
569- message = error .message ,
570- # Assuming multiple sessions per multiple subjects,
571- # otherwise nesting level might differ
572- dataset_path = Path (error .file_path ).parent .parent , # TODO
573- dandiset_path = Path (error .file_path ).parent , # TODO
574- ** kw ,
587+ for error in inspect_nwbfile (
588+ nwbfile_path = self .filepath ,
589+ skip_validate = True ,
590+ config = load_config (filepath_or_keyword = "dandi" ),
591+ importance_threshold = Importance .BEST_PRACTICE_VIOLATION ,
592+ ):
593+ severity = NWBI_IMPORTANCE_TO_DANDI_SEVERITY [
594+ error .importance .name
595+ ]
596+ kw : Any = {}
597+ if error .location :
598+ kw ["within_asset_paths" ] = {
599+ error .file_path : error .location ,
600+ }
601+ errors .append (
602+ ValidationResult (
603+ origin = origin_validation_nwbinspector ,
604+ severity = severity ,
605+ id = f"NWBI.{ error .check_function_name } " ,
606+ scope = Scope .FILE ,
607+ origin_result = error ,
608+ path = Path (error .file_path ),
609+ message = error .message ,
610+ dataset_path = Path (error .file_path ).parent .parent ,
611+ dandiset_path = Path (error .file_path ).parent ,
612+ ** kw ,
613+ )
575614 )
615+ except Exception as e :
616+ if devel_debug :
617+ raise
618+ # TODO: might reraise instead of making it into an error
619+ return _pydantic_errors_to_validation_results (
620+ [e ], self .filepath , scope = Scope .FILE
576621 )
577- except Exception as e :
578- if devel_debug :
579- raise
580- # TODO: might reraise instead of making it into an error
581- return _pydantic_errors_to_validation_results (
582- [e ], self .filepath , scope = Scope .FILE
583- )
584622
585623 # Avoid circular imports by importing within function:
586624 from .bids import NWBBIDSAsset
587625 from ..organize import validate_organized_path
588626
627+ # Path-layout validation always runs (doesn't need content)
589628 if not isinstance (self , NWBBIDSAsset ) and self .dandiset_path is not None :
590629 errors .extend (
591630 validate_organized_path (self .path , self .filepath , self .dandiset_path )
0 commit comments