1313import click
1414
1515from .base import devel_debug_option , devel_option , map_to_click_exceptions
16- from .formatter import JSONFormatter , JSONLinesFormatter , YAMLFormatter
16+ from .formatter import JSONFormatter , JSONLinesFormatter , TextFormatter , YAMLFormatter
1717from ..utils import pluralize
1818from ..validate ._core import validate as validate_
1919from ..validate ._io import (
@@ -283,32 +283,19 @@ def validate(
283283
284284 filtered = _filter_results (results , min_severity , ignore )
285285
286- if output_format == "text" :
287- _render_text (filtered , grouping , max_per_group = max_per_group )
288- if summary :
289- _print_summary (filtered , sys .stdout )
290- elif output_file is not None :
286+ if output_file is not None :
291287 with open (output_file , "w" ) as fh :
292- _render_structured (
293- filtered ,
294- output_format ,
295- fh ,
296- grouping ,
297- max_per_group = max_per_group ,
298- )
288+ _render (filtered , output_format , fh , grouping , max_per_group = max_per_group )
299289 lgr .info ("Validation output written to %s" , output_file )
300290 if summary :
301291 _print_summary (filtered , sys .stderr )
302292 else :
303- _render_structured (
304- filtered ,
305- output_format ,
306- sys .stdout ,
307- grouping ,
308- max_per_group = max_per_group ,
293+ _render (
294+ filtered , output_format , sys .stdout , grouping , max_per_group = max_per_group
309295 )
310296 if summary :
311- _print_summary (filtered , sys .stderr )
297+ summary_out = sys .stdout if output_format == "text" else sys .stderr
298+ _print_summary (filtered , summary_out )
312299
313300 _exit_if_errors (filtered )
314301
@@ -356,9 +343,11 @@ def _print_summary(results: list[ValidationResult], out: IO[str]) -> None:
356343
357344def _get_formatter (
358345 output_format : str , out : IO [str ] | None = None
359- ) -> JSONFormatter | JSONLinesFormatter | YAMLFormatter :
346+ ) -> JSONFormatter | JSONLinesFormatter | TextFormatter | YAMLFormatter :
360347 """Create a formatter for the given output format."""
361348 match output_format :
349+ case "text" :
350+ return TextFormatter (out = out )
362351 case "json" :
363352 return JSONFormatter (out = out )
364353 case "json_pp" :
@@ -371,43 +360,85 @@ def _get_formatter(
371360 raise ValueError (f"Unknown format: { output_format } " )
372361
373362
374- def _render_structured (
363+ def _render (
375364 results : list [ValidationResult ],
376365 output_format : str ,
377366 out : IO [str ],
378367 grouping : tuple [str , ...] = (),
379368 max_per_group : int | None = None ,
380369) -> None :
381- """Render validation results in a structured format."""
370+ """Render validation results in the given format.
371+
372+ Handles both text and structured (JSON/JSONL/YAML) formats, with
373+ optional grouping and truncation.
374+ """
375+ is_text = output_format == "text"
376+
382377 if grouping :
383- # Grouped output: build nested dict, serialize directly
384378 grouped : GroupedResults | TruncatedResults = _group_results (results , grouping )
385379 if max_per_group is not None :
386380 grouped = _truncate_leaves (grouped , max_per_group )
387- data = _serialize_grouped (grouped )
388- if output_format in ("json" , "json_pp" ):
389- indent = 2 if output_format == "json_pp" else None
390- json_mod .dump (data , out , indent = indent , sort_keys = True , default = str )
391- out .write ("\n " )
392- elif output_format == "yaml" :
393- import ruamel .yaml
394-
395- yaml = ruamel .yaml .YAML (typ = "safe" )
396- yaml .default_flow_style = False
397- yaml .dump (data , out )
381+ if is_text :
382+ # Text grouped output uses colored section headers
383+ if grouping == ("path" ,):
384+ # Legacy path grouping: per-path display_errors
385+ purviews = list (set (i .purview for i in results ))
386+ for purview in purviews :
387+ applies_to = [i for i in results if purview == i .purview ]
388+ display_errors (
389+ [purview ],
390+ [i .id for i in applies_to ],
391+ cast ("list[Severity]" , [i .severity for i in applies_to ]),
392+ [i .message for i in applies_to ],
393+ )
394+ else :
395+ _render_text_grouped (grouped , depth = 0 )
396+ if not any (
397+ r .severity is not None and r .severity >= Severity .ERROR for r in results
398+ ):
399+ click .secho ("No errors found." , fg = "green" )
398400 else :
399- raise ValueError (f"Unsupported format for grouped output: { output_format } " )
401+ # Structured grouped output: nested dict
402+ data = _serialize_grouped (grouped )
403+ if output_format in ("json" , "json_pp" ):
404+ indent = 2 if output_format == "json_pp" else None
405+ json_mod .dump (data , out , indent = indent , sort_keys = True , default = str )
406+ out .write ("\n " )
407+ elif output_format == "yaml" :
408+ import ruamel .yaml
409+
410+ yaml = ruamel .yaml .YAML (typ = "safe" )
411+ yaml .default_flow_style = False
412+ yaml .dump (data , out )
413+ else :
414+ raise ValueError (
415+ f"Unsupported format for grouped output: { output_format } "
416+ )
400417 else :
401- items : list [dict ] = [r .model_dump (mode = "json" ) for r in results ]
402- if max_per_group is not None and len (items ) > max_per_group :
403- items = items [:max_per_group ]
404- items .append (
405- {"_truncated" : True , "omitted_count" : len (results ) - max_per_group }
406- )
407- formatter = _get_formatter (output_format , out = out )
408- with formatter :
409- for item in items :
410- formatter (item )
418+ # Ungrouped: use formatter per-record
419+ if is_text :
420+ shown = results
421+ omitted = 0
422+ if max_per_group is not None and len (results ) > max_per_group :
423+ shown = results [:max_per_group ]
424+ omitted = len (results ) - max_per_group
425+ formatter = _get_formatter (output_format , out = out )
426+ with formatter :
427+ for r in shown :
428+ formatter (r )
429+ if omitted :
430+ click .secho (f"... and { pluralize (omitted , 'more issue' )} " , fg = "cyan" )
431+ else :
432+ items : list [dict ] = [r .model_dump (mode = "json" ) for r in results ]
433+ if max_per_group is not None and len (items ) > max_per_group :
434+ items = items [:max_per_group ]
435+ items .append (
436+ {"_truncated" : True , "omitted_count" : len (results ) - max_per_group }
437+ )
438+ formatter = _get_formatter (output_format , out = out )
439+ with formatter :
440+ for item in items :
441+ formatter (item )
411442
412443
413444def _exit_if_errors (results : list [ValidationResult ]) -> None :
@@ -500,41 +531,12 @@ def _render_text(
500531 grouping : tuple [str , ...],
501532 max_per_group : int | None = None ,
502533) -> None :
503- """Render validation results in colored text format."""
504- if not grouping :
505- shown = issues
506- omitted = 0
507- if max_per_group is not None and len (issues ) > max_per_group :
508- shown = issues [:max_per_group ]
509- omitted = len (issues ) - max_per_group
510- purviews = [i .purview for i in shown ]
511- display_errors (
512- purviews ,
513- [i .id for i in shown ],
514- cast ("list[Severity]" , [i .severity for i in shown ]),
515- [i .message for i in shown ],
516- )
517- if omitted :
518- click .secho (f"... and { pluralize (omitted , 'more issue' )} " , fg = "cyan" )
519- elif grouping == ("path" ,):
520- # Legacy path grouping: de-duplicate purviews, show per-path
521- purviews = list (set (i .purview for i in issues ))
522- for purview in purviews :
523- applies_to = [i for i in issues if purview == i .purview ]
524- display_errors (
525- [purview ],
526- [i .id for i in applies_to ],
527- cast ("list[Severity]" , [i .severity for i in applies_to ]),
528- [i .message for i in applies_to ],
529- )
530- else :
531- grouped : GroupedResults | TruncatedResults = _group_results (issues , grouping )
532- if max_per_group is not None :
533- grouped = _truncate_leaves (grouped , max_per_group )
534- _render_text_grouped (grouped , depth = 0 )
534+ """Render validation results in colored text format.
535535
536- if not any (r .severity is not None and r .severity >= Severity .ERROR for r in issues ):
537- click .secho ("No errors found." , fg = "green" )
536+ Thin wrapper around ``_render`` for backwards compatibility with tests
537+ and ``_process_issues``.
538+ """
539+ _render (issues , "text" , sys .stdout , grouping , max_per_group = max_per_group )
538540
539541
540542def _count_leaves (grouped : GroupedResults | TruncatedResults ) -> int :
0 commit comments