Source code for dartwork_mpl.validate._orchestrator
"""Orchestrator: runs every registered check and renders the stdout log."""
from __future__ import annotations
import sys
from functools import partial
from typing import TYPE_CHECKING
from .._helpers import get_renderer
from ._checks import registered_checks
from ._types import BBOX_ERRORS, VisualWarning
if TYPE_CHECKING:
from collections.abc import Callable
from matplotlib.figure import Figure
__all__ = ["validate_figure"]
def _run_check_safely(
check_fn: Callable[[], list[VisualWarning]],
) -> list[VisualWarning] | None:
"""Run one check, returning its warnings or ``None`` if it errored.
Isolating the ``try`` here (rather than inside the orchestration
loop) keeps a failing check from crashing the save pipeline while
still letting the caller distinguish "ran clean" from "could not
run" — the latter must never be reported as a clean figure.
"""
try:
return check_fn()
except BBOX_ERRORS:
return None
[docs]
def validate_figure(
fig: Figure, *, checks: tuple[str, ...] | None = None, quiet: bool = False
) -> list[VisualWarning]:
"""Run comprehensive visual validation on a Matplotlib figure.
Parameters
----------
fig : matplotlib.figure.Figure
The figure to inspect for visual defects.
checks : tuple[str, ...] | None, optional
Check IDs to run. If None, all registered checks are executed.
Supported IDs: ``OVERFLOW``, ``OVERLAP``, ``UNIT_DUP``,
``CROSS_AXES_OVERLAP``, ``LEGEND_OVERFLOW``, ``TICK_CROWD``,
``TICK_ROTATION``, ``TICK_DECIMAL``, ``EMPTY_AXES``,
``MARGIN_ASYMMETRY``, ``PIE_LABEL_OFFSET``, ``CLIPPED_TEXT``.
quiet : bool, optional
If True, suppresses stdout output. Default is False.
Returns
-------
list[VisualWarning]
List of detected visual issues.
"""
# Render once so all bounding boxes are computed.
fig.canvas.draw()
renderer = get_renderer(fig)
# Checks self-register via ``@register_check`` in their own modules;
# ``registered_checks()`` returns them in deterministic run order, so
# the orchestrator no longer hand-mirrors a check→callable dict that
# could silently drift out of sync with the check modules.
selected = registered_checks()
valid_ids = {check.check_id for check in selected}
# Fail loud on unknown IDs: a typo (``"OVERFLW"``) or a renamed
# check would otherwise silently run *zero* checks and report the
# figure clean when it was never inspected.
if checks is not None:
unknown = set(checks) - valid_ids
if unknown:
raise ValueError(
f"Unknown check IDs: {sorted(unknown)}. "
f"Valid IDs: {sorted(valid_ids)}"
)
selected = [check for check in selected if check.check_id in checks]
warnings: list[VisualWarning] = []
errored: list[str] = []
for check in selected:
# Never crash the save pipeline, but don't silently swallow a
# failed check either — a check that raises must not let the
# figure be reported "clean" as if it had run and found nothing.
# Every check is invoked uniformly as ``fn(fig, renderer)``.
result = _run_check_safely(partial(check.fn, fig, renderer))
if result is None:
errored.append(check.check_id)
else:
warnings.extend(result)
# Structured stdout output for agent consumption.
if not quiet:
if warnings:
for w in warnings:
print(str(w), file=sys.stdout, flush=True)
elif errored:
print(
"[VISUAL] ⚠️ No issues found, but "
f"{len(errored)} check(s) could not run "
f"({', '.join(sorted(errored))}); result is incomplete.",
file=sys.stdout,
flush=True,
)
else:
print(
"[VISUAL] ✅ No visual issues detected.",
file=sys.stdout,
flush=True,
)
return warnings