Source code for dartwork_mpl.validate.fixes

"""Fix suggestions for visual-validation warnings and the auto-apply shell.

Part of the validate slice; re-exported under the public path
:mod:`dartwork_mpl.validate_fixes`. Surface: ``get_fix_suggestions``,
``validate_with_fixes``. Each check's fix handler lives next to the check in
``_checks/<check>.py`` and registers in ``_checks/_registry.py``.
"""

from __future__ import annotations

from matplotlib.figure import Figure

from ..layout import simple_layout
from ._checks._registry import _FIX_HANDLERS
from ._orchestrator import validate_figure
from ._types import Severity, VisualWarning

# Auto-apply path of ``validate_with_fixes`` calls ``dm.simple_layout``
# opportunistically. simple_layout can raise from any of these branches:
#   - RuntimeError: matplotlib renderer / canvas state errors
#   - ValueError: BBox arithmetic, dm.figsize unit parsing
#   - AttributeError: an artist subclass missing get_window_extent
#   - TypeError: an invalid kwarg slipping through callers
# A bare ``Exception`` catch was previously used (with a BLE001 noqa
# acknowledging it was too broad). It hid legitimate regressions in
# simple_layout because *every* exception was treated as "fix failed,
# carry on" instead of distinguishing "skippable" from "investigate
# now."
_LAYOUT_FIX_ERRORS: tuple[type[BaseException], ...] = (
    RuntimeError,
    ValueError,
    AttributeError,
    TypeError,
)


[docs] def get_fix_suggestions(warning: VisualWarning) -> list[str]: """Generate fix suggestions for a visual warning. Looks up ``warning.check_id`` in the handler registry above and delegates to the matching handler. Unknown check IDs return ``[]`` so callers don't have to special-case them. Parameters ---------- warning : VisualWarning The warning to generate fixes for Returns ------- list[str] List of suggested fixes (code snippets). Empty if no handler is registered for ``warning.check_id``. """ handler = _FIX_HANDLERS.get(warning.check_id) return handler(warning) if handler is not None else []
[docs] def validate_with_fixes( fig: Figure, auto_apply: bool = False, verbose: bool = True ) -> tuple[list[VisualWarning], list[str]]: """Validate figure and provide fix suggestions. Parameters ---------- fig : Figure Figure to validate auto_apply : bool Whether to attempt automatic fixes verbose : bool Whether to print suggestions Returns ------- tuple[list[VisualWarning], list[str]] Warnings and applied fixes """ warnings = validate_figure(fig, quiet=not verbose) applied_fixes: list[str] = [] if not warnings: return warnings, applied_fixes if verbose: print("\n=== FIX SUGGESTIONS ===") # ``dm.simple_layout(fig)`` is a whole-figure operation: one call # resolves every OVERFLOW / MARGIN_ASYMMETRY warning at once. The old # code called it once *per* such warning inside the loop, which re-ran # the layout solver redundantly and listed N identical "Applied # simple_layout" entries for a single mutation. Collect the trigger # here, apply exactly once below. layout_fix_check_ids: list[str] = [] for warning in warnings: suggestions = get_fix_suggestions(warning) if verbose and suggestions: print(f"\n{warning.check_id}: {warning.message}") for i, suggestion in enumerate(suggestions, 1): print( f" Option {i}:\n {suggestion.replace(chr(10), chr(10) + ' ')}" ) if ( auto_apply and warning.severity == Severity.WARNING and warning.check_id in ("OVERFLOW", "MARGIN_ASYMMETRY") ): layout_fix_check_ids.append(warning.check_id) if auto_apply and layout_fix_check_ids: triggers = ", ".join(sorted(set(layout_fix_check_ids))) try: simple_layout(fig) applied_fixes.append( f"Applied dm.simple_layout() once for {triggers} " f"({len(layout_fix_check_ids)} warning(s))" ) if verbose: print(f" ✓ Auto-applied once: dm.simple_layout() [{triggers}]") except _LAYOUT_FIX_ERRORS as e: # Auto-apply is opportunistic — known layout failure modes # (simple_layout regressions, backend errors, custom artist # exceptions) report a failed fix and continue rather than # aborting the whole validate_with_fixes run. Truly unexpected # errors (e.g. KeyboardInterrupt, MemoryError) still escape # so the user notices them. if verbose: print(f" ✗ Failed to auto-fix: {e}") # Re-validate after fixes if applied_fixes and auto_apply: new_warnings = validate_figure(fig, quiet=True) if verbose: print( f"\n=== AFTER AUTO-FIX: {len(new_warnings)} warnings (was {len(warnings)}) ===" ) return new_warnings, applied_fixes return warnings, applied_fixes