@@ -146,7 +146,10 @@ def validate_bids(
146146 "--grouping" ,
147147 "-g" ,
148148 help = "How to group error/warning reporting." ,
149- type = click .Choice (["none" , "path" ], case_sensitive = False ),
149+ type = click .Choice (
150+ ["none" , "path" , "severity" , "id" , "validator" , "standard" , "dandiset" ],
151+ case_sensitive = False ,
152+ ),
150153 default = "none" ,
151154)
152155@click .option ("--ignore" , metavar = "REGEX" , help = "Regex matching error IDs to ignore" )
@@ -209,13 +212,14 @@ def validate(
209212 """
210213 # Auto-detect format from output file extension when --format not given
211214 if output_file is not None and output_format == "human" :
212- output_format = _format_from_ext (output_file )
213- if output_format is None :
215+ detected = _format_from_ext (output_file )
216+ if detected is None :
214217 raise click .UsageError (
215218 "--output requires --format to be set to a structured format "
216219 "(json, json_pp, json_lines, yaml), or use a recognized "
217220 "extension (.json, .jsonl, .yaml, .yml)."
218221 )
222+ output_format = detected
219223
220224 if load and paths :
221225 raise click .UsageError ("--load and positional paths are mutually exclusive." )
@@ -326,23 +330,40 @@ def _exit_if_errors(results: list[ValidationResult]) -> None:
326330 raise SystemExit (1 )
327331
328332
333+ def _group_key (issue : ValidationResult , grouping : str ) -> str :
334+ """Extract the grouping key from a ValidationResult."""
335+ if grouping == "path" :
336+ return issue .purview or "(no path)"
337+ elif grouping == "severity" :
338+ return issue .severity .name if issue .severity is not None else "NONE"
339+ elif grouping == "id" :
340+ return issue .id
341+ elif grouping == "validator" :
342+ return issue .origin .validator .value
343+ elif grouping == "standard" :
344+ return issue .origin .standard .value if issue .origin .standard else "N/A"
345+ elif grouping == "dandiset" :
346+ return str (issue .dandiset_path ) if issue .dandiset_path else "(no dandiset)"
347+ else :
348+ raise NotImplementedError (f"Unsupported grouping: { grouping } " )
349+
350+
329351def _render_human (
330352 issues : list [ValidationResult ],
331353 grouping : str ,
332354) -> None :
333355 """Render validation results in human-readable colored format."""
334- purviews = [i .purview for i in issues ]
335356 if grouping == "none" :
357+ purviews = [i .purview for i in issues ]
336358 display_errors (
337359 purviews ,
338360 [i .id for i in issues ],
339361 cast ("list[Severity]" , [i .severity for i in issues ]),
340362 [i .message for i in issues ],
341363 )
342364 elif grouping == "path" :
343- # The purviews are the paths, if we group by path, we need to de-duplicate.
344- # typing complains if we just take the set, though the code works otherwise.
345- purviews = list (set (purviews ))
365+ # Legacy path grouping: de-duplicate purviews, show per-path
366+ purviews = list (set (i .purview for i in issues ))
346367 for purview in purviews :
347368 applies_to = [i for i in issues if purview == i .purview ]
348369 display_errors (
@@ -352,10 +373,29 @@ def _render_human(
352373 [i .message for i in applies_to ],
353374 )
354375 else :
355- raise NotImplementedError (
356- "The `grouping` parameter values currently supported are 'path' and"
357- " 'none'."
358- )
376+ # Generic grouped rendering with section headers
377+ from collections import OrderedDict
378+
379+ groups : OrderedDict [str , list [ValidationResult ]] = OrderedDict ()
380+ for issue in issues :
381+ key = _group_key (issue , grouping )
382+ groups .setdefault (key , []).append (issue )
383+
384+ for key , group_issues in groups .items ():
385+ header = f"=== { key } ({ pluralize (len (group_issues ), 'issue' )} ) ==="
386+ fg = _get_severity_color (
387+ cast (
388+ "list[Severity]" ,
389+ [i .severity for i in group_issues if i .severity is not None ],
390+ )
391+ )
392+ click .secho (header , fg = fg , bold = True )
393+ for issue in group_issues :
394+ msg = f" [{ issue .id } ] { issue .purview } — { issue .message } "
395+ ifg = _get_severity_color (
396+ [issue .severity ] if issue .severity is not None else []
397+ )
398+ click .secho (msg , fg = ifg )
359399
360400 if not any (r .severity is not None and r .severity >= Severity .ERROR for r in issues ):
361401 click .secho ("No errors found." , fg = "green" )
0 commit comments