diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index e90418f5..650de1bd 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -13,6 +13,13 @@ _Processor = Callable[[tuple[str, ...], tuple[Any, ...]], object] +def _safe_str(obj: object) -> str: + try: + return str(obj) + except Exception as exc: + return f"{object.__repr__(obj)} (str failed: {type(exc).__name__}: {exc})" + + class TagTracer: def __init__(self) -> None: self._tags2proc: dict[tuple[str, ...], _Processor] = {} @@ -29,13 +36,13 @@ def _format_message(self, tags: Sequence[str], args: Sequence[object]) -> str: else: extra = {} - content = " ".join(map(str, args)) + content = " ".join(_safe_str(arg) for arg in args) indent = " " * self.indent lines = [f"{indent}{content} [{':'.join(tags)}]\n"] for name, value in extra.items(): - lines.append(f"{indent} {name}: {value}\n") + lines.append(f"{indent} {name}: {_safe_str(value)}\n") return "".join(lines) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 219b17d5..73d24697 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -695,6 +695,33 @@ def he_method1(self): undo() +def test_hook_tracing_with_broken_repr(he_pm: PluginManager) -> None: + class BadRepr: + def __repr__(self) -> str: + raise RuntimeError("broken repr") + + class api1: + @hookimpl + def he_method1(self, arg): + return None + + he_pm.register(api1()) + out: list[str] = [] + he_pm.trace.root.setwriter(out.append) + undo = he_pm.enable_tracing() + try: + result = he_pm.hook.he_method1(arg=BadRepr()) + finally: + undo() + + assert result == [] + assert len(out) == 2 + assert "he_method1" in out[0] + assert "arg:" in out[0] + assert "BadRepr" in out[0] + assert "finish" in out[1] + + @pytest.mark.parametrize("historic", [False, True]) def test_register_while_calling( pm: PluginManager,