Skip to content

pudl_diff.terminal

Text rendering of a PUDL diff for a terminal.

TerminalProgress

Prints the column headings, then a line about each table as it's compared.

Meant to be used as the callbacks of run_dataset_diff(). Keeps each table's TableOutcome, in outcomes, for the summary at the end. Unless verbose, only prints the tables that aren't identical, and leaves out the size columns; outcomes still has every table. The column headings wait for the first table to print, and finish() says so if none did.

Source code in src/pudl_diff/terminal.py
class TerminalProgress:
    """Prints the column headings, then a line about each table as it's compared.

    Meant to be used as the callbacks of `run_dataset_diff()`.
    Keeps each table's `TableOutcome`, in
    `outcomes`, for the summary at the end.
    Unless `verbose`, only prints the tables that aren't identical, and leaves out
    the size columns; `outcomes` still has every table. The column headings wait for
    the first table to print, and `finish()` says so if none did.
    """

    def __init__(
        self,
        left_root: str,
        right_root: str,
        *,
        explicit: bool,
        show_progress: bool,
        intro: str | None = None,
        verbose: bool = True,
    ):
        """Set up to describe a comparison of two datasets.

        Args:
            left_root: Where the left dataset is, for the introduction.
            right_root: Where the right dataset is.
            explicit: Whether the tables to compare were named, rather than being
                all those in both datasets.
            show_progress: Whether to start each line with a `[n/total]` count.
            intro: What to say before the column headings instead of the usual
                description of the comparison, e.g. when showing a saved report.
            verbose: Whether to print identical tables and the size columns.
        """
        self._intro = intro
        self._left_root = left_root
        self._right_root = right_root
        self._explicit = explicit
        self._show_progress = show_progress
        self._verbose = verbose
        self._header_printed = False
        self._total = 0
        self.outcomes: list[TableOutcome] = []

    def tables_resolved(self, tables: list[str]) -> None:
        """Say what's about to be compared, and print the column headings."""
        self._total = len(tables)
        if self._intro is not None:
            click.echo(self._intro)
        else:
            echo_intro(
                tables, self._left_root, self._right_root, explicit=self._explicit
            )
        if self._verbose:
            self._echo_header()

    def _echo_header(self) -> None:
        """Print the column headings, once."""
        total = self._total
        click.echo(
            format_header(
                len(f"[{total}/{total}]") if self._show_progress else 0,
                verbose=self._verbose,
            )
        )
        self._header_printed = True

    def finish(self) -> None:
        """If no table was listed because all were identical, say so instead."""
        if self.outcomes and not self._header_printed:
            what = "The table was" if len(self.outcomes) == 1 else "All tables were"
            click.echo(
                click.style(f"{what} found to be functionally identical.", fg="green")
            )

    def table_compared(
        self, table_name: str, report: table_report.TableDiffReport
    ) -> None:
        """Print a line about a table that has just been compared."""
        outcome = table_outcome(table_name, report)
        self.outcomes.append(outcome)
        width = len(str(self._total))
        progress = (
            f"[{len(self.outcomes):>{width}}/{self._total}]"
            if self._show_progress
            else ""
        )
        if self._verbose or outcome.exit_code != 0:
            if not self._header_printed:
                self._echo_header()
            click.echo(format_outcome(outcome, progress, verbose=self._verbose))

__init__(left_root, right_root, *, explicit, show_progress, intro=None, verbose=True)

Set up to describe a comparison of two datasets.

Parameters:

Name Type Description Default
left_root str

Where the left dataset is, for the introduction.

required
right_root str

Where the right dataset is.

required
explicit bool

Whether the tables to compare were named, rather than being all those in both datasets.

required
show_progress bool

Whether to start each line with a [n/total] count.

required
intro str | None

What to say before the column headings instead of the usual description of the comparison, e.g. when showing a saved report.

None
verbose bool

Whether to print identical tables and the size columns.

True
Source code in src/pudl_diff/terminal.py
def __init__(
    self,
    left_root: str,
    right_root: str,
    *,
    explicit: bool,
    show_progress: bool,
    intro: str | None = None,
    verbose: bool = True,
):
    """Set up to describe a comparison of two datasets.

    Args:
        left_root: Where the left dataset is, for the introduction.
        right_root: Where the right dataset is.
        explicit: Whether the tables to compare were named, rather than being
            all those in both datasets.
        show_progress: Whether to start each line with a `[n/total]` count.
        intro: What to say before the column headings instead of the usual
            description of the comparison, e.g. when showing a saved report.
        verbose: Whether to print identical tables and the size columns.
    """
    self._intro = intro
    self._left_root = left_root
    self._right_root = right_root
    self._explicit = explicit
    self._show_progress = show_progress
    self._verbose = verbose
    self._header_printed = False
    self._total = 0
    self.outcomes: list[TableOutcome] = []

finish()

If no table was listed because all were identical, say so instead.

Source code in src/pudl_diff/terminal.py
def finish(self) -> None:
    """If no table was listed because all were identical, say so instead."""
    if self.outcomes and not self._header_printed:
        what = "The table was" if len(self.outcomes) == 1 else "All tables were"
        click.echo(
            click.style(f"{what} found to be functionally identical.", fg="green")
        )

table_compared(table_name, report)

Print a line about a table that has just been compared.

Source code in src/pudl_diff/terminal.py
def table_compared(
    self, table_name: str, report: table_report.TableDiffReport
) -> None:
    """Print a line about a table that has just been compared."""
    outcome = table_outcome(table_name, report)
    self.outcomes.append(outcome)
    width = len(str(self._total))
    progress = (
        f"[{len(self.outcomes):>{width}}/{self._total}]"
        if self._show_progress
        else ""
    )
    if self._verbose or outcome.exit_code != 0:
        if not self._header_printed:
            self._echo_header()
        click.echo(format_outcome(outcome, progress, verbose=self._verbose))

tables_resolved(tables)

Say what's about to be compared, and print the column headings.

Source code in src/pudl_diff/terminal.py
def tables_resolved(self, tables: list[str]) -> None:
    """Say what's about to be compared, and print the column headings."""
    self._total = len(tables)
    if self._intro is not None:
        click.echo(self._intro)
    else:
        echo_intro(
            tables, self._left_root, self._right_root, explicit=self._explicit
        )
    if self._verbose:
        self._echo_header()

echo_intro(tables, left_root, right_root, *, explicit)

Say which tables are about to be compared, and between what.

Source code in src/pudl_diff/terminal.py
def echo_intro(
    tables: list[str], left_root: str, right_root: str, *, explicit: bool
) -> None:
    """Say which tables are about to be compared, and between what."""
    if not explicit:
        click.echo(
            f"Comparing {len(tables)} tables present in both {left_root!r} and "
            f"{right_root!r}."
        )
        return
    what = repr(tables[0]) if len(tables) == 1 else f"{len(tables)} tables"
    click.echo(f"Comparing {what} between {left_root!r} and {right_root!r}.")

echo_summary(report, outcomes, report_path, *, saved=True)

Print how the run went: table counts, what was compared, time and memory.

Ends by saying the report was written to report_path, or, if saved is False, that it was read from there.

Source code in src/pudl_diff/terminal.py
def echo_summary(
    report: PudlDiffReport,
    outcomes: list[TableOutcome],
    report_path: Path | UPath,
    *,
    saved: bool = True,
) -> None:
    """Print how the run went: table counts, what was compared, time and memory.

    Ends by saying the report was written to `report_path`, or, if `saved` is
    False, that it was read from there.
    """
    summary = report.summary
    counts = [
        ("Identical", summary.identical_table_count, "green"),
        ("Changed", summary.changed_table_count, "yellow"),
        ("Error", summary.failed_table_count, "red"),
    ]
    click.echo(
        "\n"
        + "  ".join(
            f"{click.style(label, fg=color, bold=True)}: {click.style(str(n), bold=True)}"
            for label, n, color in counts
        )
    )
    click.echo("─" * _RULE_WIDTH)
    click.echo(_field("Left:", report.left_dataset.root))
    click.echo(_field("Right:", report.right_dataset.root))
    if report.elapsed_seconds is not None:
        click.echo(_field("Elapsed:", format_duration(report.elapsed_seconds)))
    if summary.peak_rss is not None:
        click.echo(
            _field("Peak memory:", f"{summary.peak_rss} ({summary.peak_rss_table})")
        )
    _echo_totals(summary)
    _echo_schema_totals(summary, outcomes)
    _echo_table_list("Tables with errors", summary.failed_tables)
    _echo_table_list("Tables removed (only in left)", report.tables_only_in_left)
    _echo_table_list("Tables added (only in right)", report.tables_only_in_right)
    verb = "written to" if saved else "read from"
    click.echo(f"{click.style(f'Report {verb}', bold=True)} {report_path}")

format_header(progress_width=0, *, verbose=True)

The two lines of column headings for the lines made by format_outcome().

Unless verbose, leaves out the size columns, as format_outcome() does.

A heading may name its column on the first line and say what it holds on the second, so that it needn't be wider than the values below it. The second line is the one that sits directly above the values.

Source code in src/pudl_diff/terminal.py
def format_header(progress_width: int = 0, *, verbose: bool = True) -> str:
    """The two lines of column headings for the lines made by `format_outcome()`.

    Unless `verbose`, leaves out the size columns, as `format_outcome()` does.

    A heading may name its column on the first line and say what it holds on the
    second, so that it needn't be wider than the values below it. The second line is
    the one that sits directly above the values.
    """
    # Each column's two lines of heading, its width, and whether it's right-aligned.
    columns = [
        (("", ""), _EMOJI_WIDTH, False),
        (("", "PK"), _EMOJI_WIDTH, False),
        (("LEFT", "COLS"), _LEFT_COLUMNS_WIDTH, True),
        (("COL CHANGES", "+add/~chg/-del"), _COLUMNS_WIDTH, False),
        (("LEFT", "ROWS"), _LEFT_ROWS_WIDTH, True),
        (("ROW CHANGES", "+add/~chg/-del"), _ROWS_WIDTH, False),
        (("% OF LEFT ROWS", "+add/~chg/-del"), _PERCENT_WIDTH, False),
        (("", "LEFT SIZE"), _SIZE_WIDTH, True),
        (("", "RIGHT SIZE"), _RIGHT_SIZE_WIDTH, True),
        (("", "SIZE CHANGE"), _SIZE_CHANGE_WIDTH, True),
        (("", "% SIZE"), _PERCENT_CHANGE_WIDTH, True),
        (("", "TIME"), _ELAPSED_WIDTH, True),
        (("", "TABLE"), 0, False),
    ]
    if not verbose:
        del columns[7:11]
    lines = []
    for line in (0, 1):
        parts: list[str] = [" " * progress_width]
        for headings, width, right_aligned in columns:
            heading = headings[line]
            parts.append(
                heading.rjust(width) if right_aligned else heading.ljust(width)
            )
        lines.append(click.style("  ".join(p for p in parts if p).rstrip(), bold=True))
    return "\n".join(lines)

format_outcome(outcome, progress='', *, verbose=True)

One line summarizing a table's comparison.

Unless verbose, leaves out the sizes and their change.

The table name goes last, so that the (variable length) names don't disturb the alignment of everything before it.

Source code in src/pudl_diff/terminal.py
def format_outcome(
    outcome: TableOutcome, progress: str = "", *, verbose: bool = True
) -> str:
    """One line summarizing a table's comparison.

    Unless `verbose`, leaves out the sizes and their change.

    The table name goes last, so that the (variable length) names don't disturb
    the alignment of everything before it.
    """
    elapsed = (
        format_elapsed(outcome.elapsed_seconds)
        if outcome.elapsed_seconds is not None
        else ""
    )
    left_rows = f"{outcome.left_rows:,}" if outcome.left_rows is not None else ""
    left_columns = (
        f"{outcome.left_columns:,}" if outcome.left_columns is not None else ""
    )
    row_counts, row_percents = _row_cells(outcome)
    size_change, size_percent = _size_change_segments(outcome.sizes)
    size_cells = [
        (outcome.sizes.left_table_size or "").rjust(_SIZE_WIDTH),
        (outcome.sizes.right_table_size or "").rjust(_RIGHT_SIZE_WIDTH),
        _render_right(size_change, _SIZE_CHANGE_WIDTH),
        _render_right(size_percent, _PERCENT_CHANGE_WIDTH),
    ]
    parts = [
        progress,
        _TAGS[outcome.exit_code],
        _format_key(outcome.rows.has_primary_key),
        left_columns.rjust(_LEFT_COLUMNS_WIDTH),
        _render(_columns_segments(outcome), _COLUMNS_WIDTH),
        left_rows.rjust(_LEFT_ROWS_WIDTH),
        row_counts,
        row_percents,
        *(size_cells if verbose else []),
        elapsed.rjust(_ELAPSED_WIDTH),
        outcome.table_name,
    ]
    return "  ".join(part for part in parts if part)