Skip to content
Closed
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
5 changes: 5 additions & 0 deletions changelog/796.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions src/pluggy/_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
36 changes: 36 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,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")
Expand Down
Loading