From 1b23f2e17e84177909a20145c8811cc31b779634 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 12 Sep 2026 21:27:47 +0200 Subject: [PATCH 1/4] Make hook tracing defensive about repr and surrogates Tracing could turn a working hook call into a failing one in two ways: an object whose __repr__/__str__ raises propagated that exception out of the hook call (#424), and a lone surrogate in a hook argument or return value produced a message the writer could not encode (#681). Both are now handled in one place. _safe_repr()/_safe_str() catch Exception from the conversion and render an unpresentable-object marker in the shape of pytest's saferepr, naming the exception, or only its type when the exception's own repr fails too, so no user code runs in the last fallback. Anything outside Exception -- KeyboardInterrupt, SystemExit, pytest's outcome exceptions -- propagates: a __str__ or __repr__ that raises one of those is a bug in that object, not something tracing should hide. Lone surrogates are escaped with backslashreplace afterwards, which also covers surrogates that come out of an object's own __repr__. Traced values -- hook kwargs and the hook result -- now use repr() so their type is visible in the log; structural labels such as the hook name and the finish/--> markers keep using str() and stay unquoted. Supersedes #627, #666, #684 and #716. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Claude Code --- changelog/424.bugfix.rst | 5 +++ changelog/681.bugfix.rst | 10 +++++ src/pluggy/_manager.py | 7 +++- src/pluggy/_tracing.py | 43 ++++++++++++++++++++- testing/test_pluginmanager.py | 71 +++++++++++++++++++++++++++++++++++ testing/test_tracer.py | 70 ++++++++++++++++++++++++++++++++++ 6 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 changelog/424.bugfix.rst create mode 100644 changelog/681.bugfix.rst diff --git a/changelog/424.bugfix.rst b/changelog/424.bugfix.rst new file mode 100644 index 00000000..7041de57 --- /dev/null +++ b/changelog/424.bugfix.rst @@ -0,0 +1,5 @@ +Tracing no longer breaks hook execution when a traced object has a broken ``__repr__`` +or ``__str__``. Such a value is now rendered as +``<[RuntimeError(...) raised in repr()] Broken object at 0x...>``, in the same style +pytest uses for unpresentable objects, instead of propagating the exception out of the +hook call. diff --git a/changelog/681.bugfix.rst b/changelog/681.bugfix.rst new file mode 100644 index 00000000..c2b91fe8 --- /dev/null +++ b/changelog/681.bugfix.rst @@ -0,0 +1,10 @@ +Tracing no longer crashes with ``UnicodeEncodeError`` when a hook argument or return +value contains lone surrogates; they are escaped with ``backslashreplace`` before the +message reaches the writer. + +As part of this, traced *values* -- the hook keyword arguments and the hook return +value -- are now formatted with ``repr()`` rather than ``str()``, so the trace output +shows ``plugin_name: 'lfplugin'`` and ``start_path: PosixPath('/x')`` instead of +``plugin_name: lfplugin`` and ``start_path: /x``. Structural labels such as the hook +name and ``finish``/``-->`` markers are unchanged. Consumers that parse +``--debug``-style trace output may need to adapt. diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 080ef333..7061824a 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -600,7 +600,12 @@ def after( kwargs: Mapping[str, object], ) -> None: if outcome.exception is None: - hooktrace("finish", hook_name, "-->", outcome.get_result()) + hooktrace( + "finish", + hook_name, + "-->", + _tracing._safe_repr(outcome.get_result()), + ) hooktrace.root.indent -= 1 return self.add_hookcall_monitoring(before, after) diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index e90418f5..c5fc3be5 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -13,6 +13,45 @@ _Processor = Callable[[tuple[str, ...], tuple[Any, ...]], object] +def _describe_failure(exc: Exception, obj: object, func: str) -> str: + try: + exc_info = repr(exc) + except Exception: + exc_info = f"unpresentable {type(exc).__name__}" + name = type(obj).__name__ + return f"<[{exc_info} raised in {func}()] {name} object at 0x{id(obj):x}>" + + +def _escape_surrogates(text: str) -> str: + """Escape lone surrogates so the result survives any text writer. + + ``repr()`` passes surrogates through unchanged when they originate in an + object's own ``__repr__``, and writing such a string to a utf-8 target + raises :exc:`UnicodeEncodeError` inside the trace call. + """ + if text.isascii(): + return text + return text.encode("utf-8", "backslashreplace").decode("utf-8") + + +def _safe_str(obj: object) -> str: + """``str(obj)`` for structural trace labels, with a failure rendered.""" + try: + text = str(obj) + except Exception as exc: + text = _describe_failure(exc, obj, "str") + return _escape_surrogates(text) + + +def _safe_repr(obj: object) -> str: + """``repr(obj)`` for traced values, with a failure rendered.""" + try: + text = repr(obj) + except Exception as exc: + text = _describe_failure(exc, obj, "repr") + return _escape_surrogates(text) + + class TagTracer: def __init__(self) -> None: self._tags2proc: dict[tuple[str, ...], _Processor] = {} @@ -29,13 +68,13 @@ def _format_message(self, tags: Sequence[str], args: Sequence[object]) -> str: else: extra = {} - content = " ".join(map(str, args)) + content = " ".join(map(_safe_str, 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_repr(value)}\n") return "".join(lines) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 65b322d4..69395030 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -958,6 +958,77 @@ def he_method1(self): undo() +def test_hook_tracing_escapes_surrogate_values(pm: PluginManager) -> None: + """Surrogates in traced arguments and results never reach the writer. + + Regression test for #681 (pytest-dev/pytest#13750). + """ + + class Hooks: + @hookspec(firstresult=True) + def he_method1(self, arg: object) -> object: + raise NotImplementedError() + + class Plugin: + @hookimpl + def he_method1(self, arg: object) -> object: + return arg + + out: list[str] = [] + + def write(message: str) -> None: + message.encode() + out.append(message) + + pm.add_hookspecs(Hooks) + pm.register(Plugin()) + pm.trace.root.setwriter(write) + undo = pm.enable_tracing() + try: + result = pm.hook.he_method1(arg="\ud800") + finally: + undo() + + assert result == "\ud800" + assert out == [ + " he_method1 [hook]\n arg: '\\ud800'\n", + " finish he_method1 --> '\\ud800' [hook]\n", + ] + + +def test_hook_tracing_with_broken_repr(he_pm: PluginManager) -> None: + """A broken ``__repr__`` does not break the hook call. + + Regression test for #424 (kedro-org/kedro#2630). + """ + + class BrokenRepr: + def __repr__(self) -> str: + raise RuntimeError("repr is broken") + + class api1: + @hookimpl + def he_method1(self, arg): + return arg + + he_pm.register(api1()) + out: list[str] = [] + he_pm.trace.root.setwriter(out.append) + undo = he_pm.enable_tracing() + arg = BrokenRepr() + try: + result = he_pm.hook.he_method1(arg=arg) + finally: + undo() + + assert result == [arg] + assert len(out) == 2 + assert "he_method1" in out[0] + assert "RuntimeError('repr is broken') raised in repr()" in out[0] + assert "BrokenRepr object at 0x" in out[0] + assert "finish" in out[1] + + @pytest.mark.parametrize("historic", [False, True]) def test_register_while_calling( pm: PluginManager, diff --git a/testing/test_tracer.py b/testing/test_tracer.py index 13b29721..48b68074 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -158,3 +158,73 @@ def hello_again(self, arg): " hello [hook]\n arg: 3\n", " finish hello --> [] [hook]\n", ] + + +class BrokenRepr: + def __repr__(self) -> str: + raise RuntimeError("repr is broken") + + +class BrokenStr: + def __repr__(self) -> str: + return "BrokenStr()" + + def __str__(self) -> str: + raise RuntimeError("str is broken") + + +class SurrogateRepr: + def __repr__(self) -> str: + return "\ud800" + + +def test_dictargs_use_repr(rootlogger: TagTracer) -> None: + """Traced values are repred so their type is visible in the log.""" + out = rootlogger._format_message(["test"], ["call", {"name": "value", "n": 1}]) + assert out == "call [test]\n name: 'value'\n n: 1\n" + + +def test_labels_are_not_repred(rootlogger: TagTracer) -> None: + """Structural labels stay unquoted, only values are repred.""" + out = rootlogger._format_message(["test"], ["finish", "he_method1", "-->", "[]"]) + assert out == "finish he_method1 --> [] [test]\n" + + +def test_dictargs_escape_surrogate_values(rootlogger: TagTracer) -> None: + out = rootlogger._format_message(["test"], ["test", {"arg": "\ud800"}]) + assert out == "test [test]\n arg: '\\ud800'\n" + out.encode() + + +def test_escape_surrogates_from_repr(rootlogger: TagTracer) -> None: + """A surrogate coming out of the object's own repr is escaped too.""" + out = rootlogger._format_message(["test"], ["test", {"arg": SurrogateRepr()}]) + assert out == "test [test]\n arg: \\ud800\n" + out.encode() + + +def test_escape_surrogates_in_labels(rootlogger: TagTracer) -> None: + out = rootlogger._format_message(["test"], ["\ud800"]) + assert out == "\\ud800 [test]\n" + out.encode() + + +def test_non_ascii_values_are_kept(rootlogger: TagTracer) -> None: + """Legible text is not mangled, only lone surrogates are escaped.""" + out = rootlogger._format_message(["test"], ["héllo", {"arg": "wörld"}]) + assert out == "héllo [test]\n arg: 'wörld'\n" + out.encode() + + +def test_broken_repr_value_does_not_raise(rootlogger: TagTracer) -> None: + out = rootlogger._format_message(["test"], ["test", {"arg": BrokenRepr()}]) + assert "RuntimeError('repr is broken') raised in repr()" in out + assert "BrokenRepr object at 0x" in out + out.encode() + + +def test_broken_str_label_does_not_raise(rootlogger: TagTracer) -> None: + out = rootlogger._format_message(["test"], [BrokenStr()]) + assert "RuntimeError('str is broken') raised in str()" in out + assert "BrokenStr object at 0x" in out + out.encode() From f75132c15dc7106e5f82a9b5bc2c3f3d90238e6c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 12 Sep 2026 21:36:39 +0200 Subject: [PATCH 2/4] Keep trace output str-based The previous commit also switched traced values from str() to repr(), following the design distilled in #681. That is a user visible change to pytest's --debug output, and it makes that output worse to read. The worst case is a value that is meant to be read as a block. With enable_assertion_pass_hook, pytest passes the assertion explanation to pytest_assertion_pass as a multi line string. Under str() the trace shows it as written: expl: {'x': [0, 1, ...} == {'x': [0, 1, ...} Omitting 2 identical items, use -vv to show Use -v to get more diff Under repr() the same value becomes one escaped line: expl: "{'x': [0, 1, ...} == {'x': [0, 1, ...}\n \n Omitting 2 identical items, use -vv to show\n Use -v to get more diff" The rest is quieter but hits every run: of 439 traced kwarg values in a real pytest --debug run, 123 render differently, and 115 of those are nothing but quotes added around strings that were already readable -- every plugin registration line turns plugin_name: lfplugin into a quoted string. 105 of the 674 lines in the sampled trace change, so anything parsing that output breaks as well. The trace is pytest UX. A fix for a crash that nobody hits in normal use must not degrade the daily reading experience of everyone who does not hit it. The cases where repr() genuinely helps are real -- PosixPath vs py.path.local for two arguments that print the same path, ExitCode vs a bare int -- but they are 15 lines out of 674, and they do not pay for the other 115 plus the escaped blocks. The crash fixes never depended on repr(): _safe_str() guards the conversion and escapes lone surrogates just as well, so #424 and #681 stay fixed while pytest --debug output is byte for byte what it was before (verified: 674 trace lines, 0 differences). The type visibility idea is not rejected, only unbundled -- it can be argued on its own in Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Claude Code --- changelog/424.bugfix.rst | 2 +- changelog/681.bugfix.rst | 9 +-------- src/pluggy/_manager.py | 7 +------ src/pluggy/_tracing.py | 28 +++++++++++----------------- testing/test_pluginmanager.py | 6 +++--- testing/test_tracer.py | 18 ++++++------------ 6 files changed, 23 insertions(+), 47 deletions(-) diff --git a/changelog/424.bugfix.rst b/changelog/424.bugfix.rst index 7041de57..96b3ec6d 100644 --- a/changelog/424.bugfix.rst +++ b/changelog/424.bugfix.rst @@ -1,5 +1,5 @@ Tracing no longer breaks hook execution when a traced object has a broken ``__repr__`` or ``__str__``. Such a value is now rendered as -``<[RuntimeError(...) raised in repr()] Broken object at 0x...>``, in the same style +``<[RuntimeError(...) raised in str()] Broken object at 0x...>``, in the same style pytest uses for unpresentable objects, instead of propagating the exception out of the hook call. diff --git a/changelog/681.bugfix.rst b/changelog/681.bugfix.rst index c2b91fe8..f2d94a4c 100644 --- a/changelog/681.bugfix.rst +++ b/changelog/681.bugfix.rst @@ -1,10 +1,3 @@ Tracing no longer crashes with ``UnicodeEncodeError`` when a hook argument or return value contains lone surrogates; they are escaped with ``backslashreplace`` before the -message reaches the writer. - -As part of this, traced *values* -- the hook keyword arguments and the hook return -value -- are now formatted with ``repr()`` rather than ``str()``, so the trace output -shows ``plugin_name: 'lfplugin'`` and ``start_path: PosixPath('/x')`` instead of -``plugin_name: lfplugin`` and ``start_path: /x``. Structural labels such as the hook -name and ``finish``/``-->`` markers are unchanged. Consumers that parse -``--debug``-style trace output may need to adapt. +message reaches the writer. Trace output is otherwise unchanged. diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 7061824a..080ef333 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -600,12 +600,7 @@ def after( kwargs: Mapping[str, object], ) -> None: if outcome.exception is None: - hooktrace( - "finish", - hook_name, - "-->", - _tracing._safe_repr(outcome.get_result()), - ) + hooktrace("finish", hook_name, "-->", outcome.get_result()) hooktrace.root.indent -= 1 return self.add_hookcall_monitoring(before, after) diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index c5fc3be5..3e01e8bf 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -13,21 +13,21 @@ _Processor = Callable[[tuple[str, ...], tuple[Any, ...]], object] -def _describe_failure(exc: Exception, obj: object, func: str) -> str: +def _describe_str_failure(exc: Exception, obj: object) -> str: try: exc_info = repr(exc) except Exception: exc_info = f"unpresentable {type(exc).__name__}" name = type(obj).__name__ - return f"<[{exc_info} raised in {func}()] {name} object at 0x{id(obj):x}>" + return f"<[{exc_info} raised in str()] {name} object at 0x{id(obj):x}>" def _escape_surrogates(text: str) -> str: """Escape lone surrogates so the result survives any text writer. - ``repr()`` passes surrogates through unchanged when they originate in an - object's own ``__repr__``, and writing such a string to a utf-8 target - raises :exc:`UnicodeEncodeError` inside the trace call. + A lone surrogate reaching the writer raises :exc:`UnicodeEncodeError` + inside the trace call for any utf-8 target, such as the file behind + pytest's ``--debug``. """ if text.isascii(): return text @@ -35,20 +35,14 @@ def _escape_surrogates(text: str) -> str: def _safe_str(obj: object) -> str: - """``str(obj)`` for structural trace labels, with a failure rendered.""" - try: - text = str(obj) - except Exception as exc: - text = _describe_failure(exc, obj, "str") - return _escape_surrogates(text) + """``str(obj)`` for tracing, with a failing ``__str__`` rendered, not raised. - -def _safe_repr(obj: object) -> str: - """``repr(obj)`` for traced values, with a failure rendered.""" + The result has lone surrogates escaped, so any text writer accepts it. + """ try: - text = repr(obj) + text = str(obj) except Exception as exc: - text = _describe_failure(exc, obj, "repr") + text = _describe_str_failure(exc, obj) return _escape_surrogates(text) @@ -74,7 +68,7 @@ def _format_message(self, tags: Sequence[str], args: Sequence[object]) -> str: lines = [f"{indent}{content} [{':'.join(tags)}]\n"] for name, value in extra.items(): - lines.append(f"{indent} {name}: {_safe_repr(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 69395030..e36070f2 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -991,8 +991,8 @@ def write(message: str) -> None: assert result == "\ud800" assert out == [ - " he_method1 [hook]\n arg: '\\ud800'\n", - " finish he_method1 --> '\\ud800' [hook]\n", + " he_method1 [hook]\n arg: \\ud800\n", + " finish he_method1 --> \\ud800 [hook]\n", ] @@ -1024,7 +1024,7 @@ def he_method1(self, arg): assert result == [arg] assert len(out) == 2 assert "he_method1" in out[0] - assert "RuntimeError('repr is broken') raised in repr()" in out[0] + assert "RuntimeError('repr is broken') raised in str()" in out[0] assert "BrokenRepr object at 0x" in out[0] assert "finish" in out[1] diff --git a/testing/test_tracer.py b/testing/test_tracer.py index 48b68074..08af8f21 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -178,21 +178,15 @@ def __repr__(self) -> str: return "\ud800" -def test_dictargs_use_repr(rootlogger: TagTracer) -> None: - """Traced values are repred so their type is visible in the log.""" +def test_dictargs_keep_str_rendering(rootlogger: TagTracer) -> None: + """Values keep their ``str`` rendering, the trace is a log not a repr dump.""" out = rootlogger._format_message(["test"], ["call", {"name": "value", "n": 1}]) - assert out == "call [test]\n name: 'value'\n n: 1\n" - - -def test_labels_are_not_repred(rootlogger: TagTracer) -> None: - """Structural labels stay unquoted, only values are repred.""" - out = rootlogger._format_message(["test"], ["finish", "he_method1", "-->", "[]"]) - assert out == "finish he_method1 --> [] [test]\n" + assert out == "call [test]\n name: value\n n: 1\n" def test_dictargs_escape_surrogate_values(rootlogger: TagTracer) -> None: out = rootlogger._format_message(["test"], ["test", {"arg": "\ud800"}]) - assert out == "test [test]\n arg: '\\ud800'\n" + assert out == "test [test]\n arg: \\ud800\n" out.encode() @@ -212,13 +206,13 @@ def test_escape_surrogates_in_labels(rootlogger: TagTracer) -> None: def test_non_ascii_values_are_kept(rootlogger: TagTracer) -> None: """Legible text is not mangled, only lone surrogates are escaped.""" out = rootlogger._format_message(["test"], ["héllo", {"arg": "wörld"}]) - assert out == "héllo [test]\n arg: 'wörld'\n" + assert out == "héllo [test]\n arg: wörld\n" out.encode() def test_broken_repr_value_does_not_raise(rootlogger: TagTracer) -> None: out = rootlogger._format_message(["test"], ["test", {"arg": BrokenRepr()}]) - assert "RuntimeError('repr is broken') raised in repr()" in out + assert "RuntimeError('repr is broken') raised in str()" in out assert "BrokenRepr object at 0x" in out out.encode() From 0022c4631aae10defed99526adc434fda07c1f1f Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 12 Sep 2026 22:45:23 +0200 Subject: [PATCH 3/4] Cover the unpresentable object paths in tracing The guards around str() were only exercised for the simple case of a broken __str__. The exception explaining that failure can be just as broken, and Ctrl-C has to stay reliable at both levels, so cover: an exception whose repr fails, which falls back to its type name; an exception whose repr raises an exception that is itself unpresentable, which must not be rendered at all; and KeyboardInterrupt raised from the value and from the explanation. _tracing.py is at 100% statement and branch coverage. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Claude Code --- testing/test_tracer.py | 70 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/testing/test_tracer.py b/testing/test_tracer.py index 08af8f21..c1f6f1a7 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -222,3 +222,73 @@ def test_broken_str_label_does_not_raise(rootlogger: TagTracer) -> None: assert "RuntimeError('str is broken') raised in str()" in out assert "BrokenStr object at 0x" in out out.encode() + + +def test_keyboard_interrupt_from_str_propagates(rootlogger: TagTracer) -> None: + """Ctrl-C during a traced call still interrupts, it is not swallowed.""" + + class Interrupting: + def __str__(self) -> str: + raise KeyboardInterrupt + + with pytest.raises(KeyboardInterrupt): + rootlogger._format_message(["test"], ["test", {"arg": Interrupting()}]) + + +def test_broken_exception_repr_falls_back_to_type_name(rootlogger: TagTracer) -> None: + """The exception explaining the failure may itself be unpresentable.""" + + class BadError(Exception): + def __repr__(self) -> str: + raise RuntimeError("exception repr is broken") + + class Broken: + def __str__(self) -> str: + raise BadError + + out = rootlogger._format_message(["test"], ["test", {"arg": Broken()}]) + assert "<[unpresentable BadError raised in str()] Broken object at 0x" in out + + +def test_error_from_exception_repr_is_not_rendered( + rootlogger: TagTracer, +) -> None: + """Rendering stops at the type name, so a chain of broken reprs cannot escape.""" + + class Unpresentable(Exception): + def __repr__(self) -> str: + raise ValueError("repr is broken") # pragma: no cover + + def __str__(self) -> str: + raise ValueError("str is broken") # pragma: no cover + + class BadError(Exception): + def __repr__(self) -> str: + raise Unpresentable + + def __str__(self) -> str: + raise Unpresentable # pragma: no cover + + class Broken: + def __str__(self) -> str: + raise BadError + + out = rootlogger._format_message(["test"], ["test", {"arg": Broken()}]) + assert "<[unpresentable BadError raised in str()] Broken object at 0x" in out + + +def test_keyboard_interrupt_from_exception_repr_propagates( + rootlogger: TagTracer, +) -> None: + """Ctrl-C while rendering the failure explanation propagates as well.""" + + class InterruptingError(Exception): + def __repr__(self) -> str: + raise KeyboardInterrupt + + class Broken: + def __str__(self) -> str: + raise InterruptingError + + with pytest.raises(KeyboardInterrupt): + rootlogger._format_message(["test"], ["test", {"arg": Broken()}]) From c5e0e4abd532958561b3e9118ab8f390b49c0180 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 12 Sep 2026 22:50:44 +0200 Subject: [PATCH 4/4] Drop an unused repr from a tracing test double BrokenStr's __repr__ was never called -- _safe_str reaches for __str__, and the failure message is built from the type name -- so it only showed up as an uncovered line. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Claude Code --- testing/test_tracer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/testing/test_tracer.py b/testing/test_tracer.py index c1f6f1a7..a7527cab 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -166,9 +166,6 @@ def __repr__(self) -> str: class BrokenStr: - def __repr__(self) -> str: - return "BrokenStr()" - def __str__(self) -> str: raise RuntimeError("str is broken")