diff --git a/changelog/792.bugfix.rst b/changelog/792.bugfix.rst new file mode 100644 index 00000000..0f738d1c --- /dev/null +++ b/changelog/792.bugfix.rst @@ -0,0 +1 @@ +Fix hook tracing raising ``TypeError`` for specced mocks passed as hook arguments or returned from first-result hooks. diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index 9211c0b7..e8776f76 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -9,6 +9,7 @@ import enum import os from typing import Any +from typing import cast _Writer = Callable[[str], object] @@ -60,10 +61,12 @@ def _format_block(indent: str, lines: list[str]) -> list[str]: def _render_value(obj: object) -> str: """Render a traced value, adding detail only where ``str`` is ambiguous.""" + obj_type = type(obj) # Before the str check: a StrEnum member is a str whose str() drops the name. - if isinstance(obj, (enum.Enum, os.PathLike)): + if issubclass(obj_type, (enum.Enum, os.PathLike)): return _safe_repr(obj) - if isinstance(obj, str): + if issubclass(obj_type, str): + obj = cast(str, obj) if len(obj.splitlines()) > 1: return _safe_str(obj) if _is_plain_token(obj): @@ -94,8 +97,8 @@ 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): - extra = args[-1] + if issubclass(type(args[-1]), dict): + extra = cast(dict[object, object], args[-1]) args = args[:-1] else: extra = {} diff --git a/testing/test_tracer.py b/testing/test_tracer.py index 4fc16292..467a8248 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,32 @@ def hello_again(self, arg): ] +@pytest.mark.parametrize("spec", [str, dict]) +def test_plugin_tracing_with_mock(pm: PluginManager, spec: type) -> None: + class Api: + @hookspec(firstresult=True) + def hello(self, arg: object) -> object: + "Return the argument." + + class Plugin: + @hookimpl + def hello(self, arg: object) -> object: + return arg + + pm.add_hookspecs(Api) + pm.register(Plugin()) + trace_out: list[str] = [] + pm.trace.root.setwriter(trace_out.append) + pm.enable_tracing() + + value = Mock(spec=spec) + assert pm.hook.hello(arg=value) is value + assert trace_out == [ + f" hello [hook]\n arg: {value}\n", + f" finish hello --> {value} [hook]\n", + ] + + class BrokenRepr: def __repr__(self) -> str: raise RuntimeError("repr is broken")