From b12171ef3496667628f46e2e209b886f72108e9a Mon Sep 17 00:00:00 2001 From: Priyansh Date: Thu, 15 Oct 2026 21:43:48 +0000 Subject: [PATCH] fix: don't route specced Mocks through str/dict tracing (#792) Hook tracing crashed with TypeError when a hook argument or first-result value was a specced unittest.mock.Mock (Mock(spec=str) or Mock(spec=dict)). Mock(spec=...) reports its spec through __class__, so the isinstance checks in _tracing.py matched the mock and sent it into the string formatting / dict metadata path. Use exact-type checks (type(obj) is str, type(args[-1]) is dict) so only real strings and dicts take those paths; a specced Mock now renders as an ordinary value and the hook result is left untouched. Adds a parametrized regression test covering both str and dict specs, and a changelog fragment. --- changelog/796.bugfix.rst | 5 +++++ src/pluggy/_tracing.py | 4 ++-- testing/test_tracer.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 changelog/796.bugfix.rst diff --git a/changelog/796.bugfix.rst b/changelog/796.bugfix.rst new file mode 100644 index 00000000..dabd47da --- /dev/null +++ b/changelog/796.bugfix.rst @@ -0,0 +1,5 @@ +Fixed a crash in hook tracing when a specced ``unittest.mock.Mock`` +(``Mock(spec=str)`` or ``Mock(spec=dict)``) was passed as a hook argument +or returned as a first-result value: the tracer's ``isinstance`` checks +routed the mock through the string/dict special-casing and raised +``TypeError`` instead of rendering it. diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index 9211c0b7..018dfad6 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -63,7 +63,7 @@ def _render_value(obj: object) -> str: # Before the str check: a StrEnum member is a str whose str() drops the name. if isinstance(obj, (enum.Enum, os.PathLike)): return _safe_repr(obj) - if isinstance(obj, str): + if type(obj) is str: if len(obj.splitlines()) > 1: return _safe_str(obj) if _is_plain_token(obj): @@ -94,7 +94,7 @@ def get(self, name: str) -> TagTracerSub: return TagTracerSub(self, (name,)) def _format_message(self, tags: Sequence[str], args: Sequence[object]) -> str: - if isinstance(args[-1], dict): + if type(args[-1]) is dict: extra = args[-1] args = args[:-1] else: diff --git a/testing/test_tracer.py b/testing/test_tracer.py index 4fc16292..1b8e12e8 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -1,6 +1,7 @@ import enum import os import pathlib +from unittest.mock import Mock import pytest @@ -164,6 +165,41 @@ def hello_again(self, arg): ] +@pytest.mark.parametrize("spec", [str, dict]) +def test_tracing_specced_mock_does_not_raise(pm: PluginManager, spec: type) -> None: + """A specced ``Mock`` must not be routed through the str/dict special-casing. + + ``Mock(spec=str)`` / ``Mock(spec=dict)`` report their spec through + ``__class__``, so ``isinstance`` would send them down the string + formatting or dict metadata path and raise ``TypeError``. See #792. + """ + + class Api: + @hookspec(firstresult=True) + def echo(self, value: object) -> None: + "return the value" + + pm.add_hookspecs(Api) + test_hc = pm.hook.echo + + class Plugin: + @hookimpl + def echo(self, value): + return value + + value = Mock(spec=spec) + pm.register(Plugin()) + + trace_out: list[str] = [] + pm.trace.root.setwriter(trace_out.append) + pm.enable_tracing() + + out = test_hc(value=value) + assert out is value + assert "echo [hook]" in "".join(trace_out) + assert "finish echo" in "".join(trace_out) + + class BrokenRepr: def __repr__(self) -> str: raise RuntimeError("repr is broken")