33import io
44import json
55import logging
6+ import textwrap
67from dataclasses import dataclass , field , replace
78from enum import Enum
89from pathlib import Path
@@ -31,7 +32,7 @@ class CoverageType(Enum):
3132 UNIT_TEST = "unit-test"
3233
3334
34- class CoverageFormat (str , Enum ):
35+ class OutputFormat (str , Enum ):
3536 STRING_TABLE = "string"
3637 MARKDOWN_TABLE = "markdown"
3738
@@ -632,89 +633,135 @@ def find_new_misses(self):
632633
633634 return res
634635
635- def summary (self ):
636+ def summary (self , output_format : OutputFormat ):
636637 buf = io .StringIO ()
637638
638639 if self .after .entity_type != CoverageReport .EntityType .CATALOG :
639640 raise TypeError (
640641 f"Unsupported report_type for summary method: " f"{ self .after .entity_type } "
641642 )
642643
643- buf .write (f"{ '' :10} { 'before' :>10} { 'after' :>10} { '+/-' :>15} \n " )
644- buf .write ("=" * 45 + "\n " )
645-
646644 before_cov = self .before .coverage if self .before .coverage is not None else 0.0
647645 after_cov = self .after .coverage if self .after .coverage is not None else 0.0
648- buf .write (
649- f"{ 'Coverage' :10} { before_cov :10.2%} { after_cov :10.2%} "
650- f"{ (after_cov - before_cov ):+15.2%} \n "
651- )
652- buf .write ("=" * 45 + "\n " )
653-
654- add_del = (
646+ total_diff = after_cov - before_cov
647+ tables_add_del = (
655648 f"{ len (set (self .after .subentities ) - set (self .before .subentities )):+d} /"
656649 f"{ - len (set (self .before .subentities ) - set (self .after .subentities )):+d} "
657650 )
658- buf .write (
659- f"{ 'Tables' :10} { len (self .before .subentities ):10d} "
660- f"{ len (self .after .subentities ):10d} "
661- f"{ add_del :>15} \n "
662- )
663-
664- add_del = (
651+ columns_add_del = (
665652 f"{ len (self .after .total - self .before .total ):+d} /"
666653 f"{ - len (self .before .total - self .after .total ):+d} "
667654 )
668- buf .write (
669- f"{ 'Columns' :10} { len (self .before .total ):10d} { len (self .after .total ):10d} "
670- f"{ add_del :>15} \n "
671- )
672- buf .write ("=" * 45 + "\n " )
673-
674- add_del = (
655+ hits_add_del = (
675656 f"{ len (self .after .covered - self .before .covered ):+d} /"
676657 f"{ - len (self .before .covered - self .after .covered ):+d} "
677658 )
678- buf .write (
679- f"{ 'Hits' :10} { len (self .before .covered ):10d} { len (self .after .covered ):10d} "
680- f"{ add_del :>15} \n "
681- )
682-
683- add_del = (
659+ misses_add_del = (
684660 f"{ len (self .after .misses - self .before .misses ):+d} /"
685661 f"{ - len (self .before .misses - self .after .misses ):+d} "
686662 )
687- buf .write (
688- f"{ 'Misses' :10} { len (self .before .misses ):10d} { len (self .after .misses ):10d} "
689- f"{ add_del :>15} \n "
690- )
691663
692- buf .write ("=" * 45 + "\n " )
664+ if output_format == OutputFormat .MARKDOWN_TABLE :
665+ template = textwrap .dedent (
666+ """\
667+ | | before | after | +/- |
668+ |:----------|:-----------------:|:----------------:|:-----------------:|
669+ | Coverage | {before_coverage} | {after_coverage} | {total_diff} |
670+ | Tables | {before_tables} | {after_tables} | {tables_add_del} |
671+ | Columns | {before_columns} | {after_columns} | {columns_add_del} |
672+ | Hits | {before_hits} | {after_hits} | {hits_add_del} |
673+ | Misses | {before_misses} | {after_misses} | {misses_add_del} |
674+ """
675+ )
676+
677+ formatted = template .format (
678+ before_coverage = f"{ before_cov :.2%} " ,
679+ after_coverage = f"{ after_cov :.2%} " ,
680+ total_diff = f"{ total_diff :.2%} " ,
681+ before_tables = len (self .before .subentities ),
682+ after_tables = len (self .after .subentities ),
683+ tables_add_del = tables_add_del ,
684+ before_columns = len (self .before .total ),
685+ after_columns = len (self .after .total ),
686+ columns_add_del = columns_add_del ,
687+ before_hits = len (self .before .covered ),
688+ after_hits = len (self .after .covered ),
689+ hits_add_del = hits_add_del ,
690+ before_misses = len (self .before .misses ),
691+ after_misses = len (self .after .misses ),
692+ misses_add_del = misses_add_del ,
693+ )
694+
695+ buf .write (formatted )
696+ elif output_format == OutputFormat .STRING_TABLE :
697+ buf .write (f"{ '' :10} { 'before' :>10} { 'after' :>10} { '+/-' :>15} \n " )
698+ buf .write ("=" * 45 + "\n " )
699+
700+ buf .write (
701+ f"{ 'Coverage' :10} { before_cov :10.2%} { after_cov :10.2%} " f"{ total_diff :+15.2%} \n "
702+ )
703+ buf .write ("=" * 45 + "\n " )
704+
705+ buf .write (
706+ f"{ 'Tables' :10} { len (self .before .subentities ):10d} "
707+ f"{ len (self .after .subentities ):10d} "
708+ f"{ tables_add_del :>15} \n "
709+ )
710+
711+ buf .write (
712+ f"{ 'Columns' :10} { len (self .before .total ):10d} { len (self .after .total ):10d} "
713+ f"{ columns_add_del :>15} \n "
714+ )
715+ buf .write ("=" * 45 + "\n " )
716+
717+ buf .write (
718+ f"{ 'Hits' :10} { len (self .before .covered ):10d} { len (self .after .covered ):10d} "
719+ f"{ hits_add_del :>15} \n "
720+ )
721+
722+ buf .write (
723+ f"{ 'Misses' :10} { len (self .before .misses ):10d} { len (self .after .misses ):10d} "
724+ f"{ misses_add_del :>15} \n "
725+ )
726+
727+ buf .write ("=" * 45 + "\n " )
728+ else :
729+ raise ValueError (f"Unsupported output_format: { output_format } " )
693730
694731 return buf .getvalue ()
695732
696- def new_misses_summary (self ):
733+ def new_misses_summary (self , output_format : OutputFormat , last : bool = False ):
697734 if self .after .entity_type == CoverageReport .EntityType .COLUMN :
698- return self ._new_miss_summary_row ()
735+ return self ._new_miss_summary_row (output_format , last )
699736
700737 elif self .after .entity_type == CoverageReport .EntityType .TABLE :
701738 buf = io .StringIO ()
702739
703- buf .write (self ._new_miss_summary_row ())
740+ buf .write (self ._new_miss_summary_row (output_format ))
704741 if self .after .cov_type != CoverageType .UNIT_TEST : # Unit tests work on the table level
705- for col in self .new_misses .values ():
706- buf .write (col .new_misses_summary ())
742+ for i , col in enumerate (self .new_misses .values ()):
743+ last = i == len (self .new_misses ) - 1
744+ buf .write (col .new_misses_summary (output_format , last = last ))
707745
708746 return buf .getvalue ()
709747
710748 elif self .after .entity_type == CoverageReport .EntityType .CATALOG :
711749 buf = io .StringIO ()
712- buf .write ("=" * 94 + "\n " )
713- buf .write (self ._new_miss_summary_row ())
714- buf .write ("=" * 94 + "\n " )
715- for table in self .new_misses .values ():
716- buf .write (table .new_misses_summary ())
750+ if output_format == OutputFormat .MARKDOWN_TABLE :
751+ buf .write ("| | before (%) | after (%) |\n " )
752+ buf .write ("|:----|:----------:|:---------:|\n " )
753+ buf .write (self ._new_miss_summary_row (output_format ))
754+ for table in self .new_misses .values ():
755+ buf .write (table .new_misses_summary (output_format ))
756+ elif output_format == OutputFormat .STRING_TABLE :
757+ buf .write ("=" * 94 + "\n " )
758+ buf .write (self ._new_miss_summary_row (output_format ))
717759 buf .write ("=" * 94 + "\n " )
760+ for table in self .new_misses .values ():
761+ buf .write (table .new_misses_summary (output_format ))
762+ buf .write ("=" * 94 + "\n " )
763+ else :
764+ raise ValueError (f"Unsupported output_format: { output_format } " )
718765
719766 return buf .getvalue ()
720767
@@ -724,13 +771,13 @@ def new_misses_summary(self):
724771 f"{ self .after .entity_type } "
725772 )
726773
727- def _new_miss_summary_row (self ):
774+ def _new_miss_summary_row (self , output_format : OutputFormat , last : bool = False ):
728775 if self .after .entity_type == CoverageReport .EntityType .CATALOG :
729776 title_prefix = ""
730777 elif self .after .entity_type == CoverageReport .EntityType .TABLE :
731- title_prefix = "- "
778+ title_prefix = "┌ "
732779 elif self .after .entity_type == CoverageReport .EntityType .COLUMN :
733- title_prefix = "-- "
780+ title_prefix = "└── " if last else "├── "
734781 else :
735782 raise TypeError (
736783 f"Unsupported report_type for _new_miss_summary_row method: "
@@ -758,10 +805,23 @@ def _new_miss_summary_row(self):
758805 )
759806
760807 buf = io .StringIO ()
761- buf .write (f"{ title :50} " )
762- buf .write (f"{ before_covered :>5} /{ before_total :<5} { before_coverage :^9} " )
763- buf .write (" -> " )
764- buf .write (f"{ after_covered :>5} /{ after_total :<5} { after_coverage :^9} \n " )
808+ if output_format == OutputFormat .MARKDOWN_TABLE :
809+ title = (
810+ f"`{ title } `"
811+ if self .after .entity_type == CoverageReport .EntityType .CATALOG
812+ else title
813+ )
814+ buf .write (
815+ f"| { title } | { before_covered } /{ before_total } { before_coverage } "
816+ f"| { after_covered } /{ after_total } { after_coverage } |\n "
817+ )
818+ elif output_format == OutputFormat .STRING_TABLE :
819+ buf .write (f"{ title :50} " )
820+ buf .write (f"{ before_covered :>5} /{ before_total :<5} { before_coverage :^9} " )
821+ buf .write (" -> " )
822+ buf .write (f"{ after_covered :>5} /{ after_total :<5} { after_coverage :^9} \n " )
823+ else :
824+ raise ValueError (f"Unsupported output_format: { output_format } " )
765825
766826 return buf .getvalue ()
767827
@@ -878,11 +938,13 @@ def compute_coverage(catalog: Catalog, cov_type: CoverageType):
878938 return coverage_report
879939
880940
881- def compare_reports (report : CoverageReport , compare_report : CoverageReport ) -> CoverageDiff :
882- diff = CoverageDiff (compare_report , report )
941+ def compare_reports (
942+ report : CoverageReport , compare_report : CoverageReport , output_format : OutputFormat
943+ ) -> CoverageDiff :
883944
884- print (diff .summary ())
885- print (diff .new_misses_summary ())
945+ diff = CoverageDiff (compare_report , report )
946+ print (diff .summary (output_format ))
947+ print (diff .new_misses_summary (output_format ))
886948
887949 return diff
888950
@@ -930,7 +992,7 @@ def do_compute(
930992 cov_fail_compare : Path = None ,
931993 model_path_filter : Optional [List [str ]] = None ,
932994 model_path_exclusion_filter : Optional [List [str ]] = None ,
933- cov_format : CoverageFormat = CoverageFormat .STRING_TABLE ,
995+ output_format : OutputFormat = OutputFormat .STRING_TABLE ,
934996):
935997 """
936998 Computes coverage for a dbt project.
@@ -949,7 +1011,7 @@ def do_compute(
9491011
9501012 coverage_report = compute_coverage (catalog , cov_type )
9511013
952- if cov_format == CoverageFormat .MARKDOWN_TABLE :
1014+ if output_format == OutputFormat .MARKDOWN_TABLE :
9531015 print (coverage_report .to_markdown_table ())
9541016 else :
9551017 print (coverage_report .to_formatted_string ())
@@ -965,7 +1027,9 @@ def do_compute(
9651027 return coverage_report
9661028
9671029
968- def do_compare (report : Path , compare_report : Path ) -> CoverageDiff :
1030+ def do_compare (
1031+ report : Path , compare_report : Path , output_format : OutputFormat = OutputFormat .STRING_TABLE
1032+ ) -> CoverageDiff :
9691033 """
9701034 Compares two coverage reports generated by the ``compute`` command.
9711035
@@ -974,6 +1038,7 @@ def do_compare(report: Path, compare_report: Path) -> CoverageDiff:
9741038 Args:
9751039 report: ``Path`` to the current report - the after state.
9761040 compare_report: ``Path`` to the report to compare against - the before state.
1041+ output_format: The OutputFormat to print, either `string` or `markdown`
9771042
9781043 Returns:
9791044 The ``CoverageDiff`` between the two coverage reports.
@@ -982,7 +1047,7 @@ def do_compare(report: Path, compare_report: Path) -> CoverageDiff:
9821047 report = read_coverage_report (report )
9831048 compare_report = read_coverage_report (compare_report )
9841049
985- diff = compare_reports (report , compare_report )
1050+ diff = compare_reports (report , compare_report , output_format )
9861051
9871052 return diff
9881053
@@ -1009,8 +1074,8 @@ def compute(
10091074 model_path_exclusion_filter : Optional [List [str ]] = typer .Option (
10101075 None , help = "The model_path string(s) to filter tables on excluding tables that match."
10111076 ),
1012- cov_format : CoverageFormat = typer .Option (
1013- CoverageFormat .STRING_TABLE ,
1077+ output_format : OutputFormat = typer .Option (
1078+ OutputFormat .STRING_TABLE ,
10141079 help = "The output format to print, either `string` or `markdown`" ,
10151080 ),
10161081):
@@ -1025,7 +1090,7 @@ def compute(
10251090 cov_fail_compare ,
10261091 model_path_filter ,
10271092 model_path_exclusion_filter ,
1028- cov_format ,
1093+ output_format ,
10291094 )
10301095
10311096
@@ -1035,10 +1100,14 @@ def compare(
10351100 compare_report : Path = typer .Argument (
10361101 ..., help = "Path to another coverage report to compare with - the before state."
10371102 ),
1103+ output_format : OutputFormat = typer .Option (
1104+ OutputFormat .STRING_TABLE ,
1105+ help = "The output format to print, either `string` or `markdown`" ,
1106+ ),
10381107):
10391108 """Compare two coverage reports generated by the compute command."""
10401109
1041- return do_compare (report , compare_report )
1110+ return do_compare (report , compare_report , output_format )
10421111
10431112
10441113@app .callback ()
0 commit comments