From c30aa42ee45e17cf86efc190fe26abbf8975de59 Mon Sep 17 00:00:00 2001 From: igerber Date: Mon, 29 Jun 2026 15:53:41 -0400 Subject: [PATCH] fix: surface BusinessReport appendix render failures Make full_report(include_appendix=True) render a visible "Technical Appendix unavailable ()" note when the estimator summary() raises, instead of silently omitting the appendix. Only the exception class name is shown (no message or traceback) to avoid leaking paths, data values, or other internals. Co-Authored-By: Claude Opus 4.8 (1M context) --- diff_diff/business_report.py | 17 +++++++++++++---- tests/test_business_report.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/diff_diff/business_report.py b/diff_diff/business_report.py index b3160a6e..f8331ac7 100644 --- a/diff_diff/business_report.py +++ b/diff_diff/business_report.py @@ -353,12 +353,21 @@ def full_report(self) -> str: """Return a structured multi-section markdown report.""" base = _render_full_report(self.to_dict()) if self._include_appendix: + appendix_text = None try: appendix = self._results.summary() - except Exception: # noqa: BLE001 - appendix = None - if appendix: - base = base + "\n\n## Technical Appendix\n\n```\n" + str(appendix) + "\n```\n" + if appendix: + appendix_text = str(appendix) + except Exception as exc: # noqa: BLE001 + appendix_error = type(exc).__name__ or "Exception" + base = ( + base + + "\n\n## Technical Appendix\n\n" + + "Technical appendix unavailable: estimator summary rendering failed " + + f"({appendix_error}).\n" + ) + if appendix_text: + base = base + "\n\n## Technical Appendix\n\n```\n" + appendix_text + "\n```\n" return base def export_markdown(self) -> str: diff --git a/tests/test_business_report.py b/tests/test_business_report.py index 80b04fdf..57e82013 100644 --- a/tests/test_business_report.py +++ b/tests/test_business_report.py @@ -480,6 +480,40 @@ def test_include_appendix_false_omits(self, event_study_fit): md = br.full_report() assert "## Technical Appendix" not in md + def test_appendix_summary_failure_is_visible(self, event_study_fit): + fit, _ = event_study_fit + with patch.object(fit, "summary", side_effect=RuntimeError("internal path /tmp/private")): + br = BusinessReport(fit, auto_diagnostics=False, include_appendix=True) + md = br.full_report() + + assert "## Technical Appendix" in md + assert "Technical appendix unavailable" in md + assert "RuntimeError" in md + + def test_appendix_summary_failure_does_not_leak_exception_details(self, event_study_fit): + fit, _ = event_study_fit + with patch.object(fit, "summary", side_effect=RuntimeError("secret-token-123")): + br = BusinessReport(fit, auto_diagnostics=False, include_appendix=True) + md = br.full_report() + + assert "secret-token-123" not in md + assert "Traceback" not in md + + def test_appendix_stringification_failure_is_visible(self, event_study_fit): + class _BadSummary: + def __str__(self): + raise ValueError("sensitive-render-context") + + fit, _ = event_study_fit + with patch.object(fit, "summary", return_value=_BadSummary()): + br = BusinessReport(fit, auto_diagnostics=False, include_appendix=True) + md = br.full_report() + + assert "## Technical Appendix" in md + assert "Technical appendix unavailable" in md + assert "ValueError" in md + assert "sensitive-render-context" not in md + # --------------------------------------------------------------------------- # BaconDecompositionResults