Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/792.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix hook tracing raising ``TypeError`` for specced mocks passed as hook arguments or returned from first-result hooks.
11 changes: 7 additions & 4 deletions src/pluggy/_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import enum
import os
from typing import Any
from typing import cast


_Writer = Callable[[str], object]
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 = {}
Expand Down
27 changes: 27 additions & 0 deletions testing/test_tracer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import enum
import os
import pathlib
from unittest.mock import Mock

import pytest

Expand Down Expand Up @@ -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")
Expand Down
Loading