From 3499ef53668a089dddd76e066a8d521ff5c76963 Mon Sep 17 00:00:00 2001 From: Anusha Shekhar Date: Tue, 12 Dec 2023 22:32:18 -0500 Subject: [PATCH 1/7] Add Saferepr to tracer class --- src/pluggy/_tracing.py | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index de1e13a7..7be55493 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -7,6 +7,7 @@ from typing import Callable from typing import Sequence from typing import Tuple +import reprlib _Writer = Callable[[str], object] @@ -59,6 +60,96 @@ def setprocessor(self, tags: str | tuple[str, ...], processor: _Processor) -> No assert isinstance(tags, tuple) self._tags2proc[tags] = processor +def _try_repr_or_str(obj: object) -> str: + try: + return repr(obj) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: + return f'{type(obj).__name__}("{obj}")' + +def _format_repr_exception(exc: BaseException, obj: object) -> str: + try: + exc_info = _try_repr_or_str(exc) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})" + return "<[{} raised in repr()] {} object at 0x{:x}>".format( + exc_info, type(obj).__name__, id(obj) + ) + +def _ellipsize(s: str, maxsize: int) -> str: + if len(s) > maxsize: + i = max(0, (maxsize - 3) // 2) + j = max(0, maxsize - 3 - i) + return s[:i] + "..." + s[len(s) - j :] + return s + +class SafeRepr(reprlib.Repr): + """ + repr.Repr that limits the resulting size of repr() and includes + information on exceptions raised during the call. + """ + + def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None: + """ + :param maxsize: + If not None, will truncate the resulting repr to that specific size, using ellipsis + somewhere in the middle to hide the extra text. + If None, will not impose any size limits on the returning repr. + """ + super().__init__() + # ``maxstring`` is used by the superclass, and needs to be an int; using a + # very large number in case maxsize is None, meaning we want to disable + # truncation. + self.maxstring = maxsize if maxsize is not None else 1_000_000_000 + self.maxsize = maxsize + self.use_ascii = use_ascii + + def repr(self, x: object) -> str: + try: + if self.use_ascii: + s = ascii(x) + else: + s = super().repr(x) + + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + s = _format_repr_exception(exc, x) + if self.maxsize is not None: + s = _ellipsize(s, self.maxsize) + return s + + def repr_instance(self, x: object, level: int) -> str: + try: + s = repr(x) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + s = _format_repr_exception(exc, x) + if self.maxsize is not None: + s = _ellipsize(s, self.maxsize) + return s + + +# Maximum size of overall repr of objects to display during assertion errors. +DEFAULT_REPR_MAX_SIZE = 240 +def saferepr( + obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False +) -> str: + """Return a size-limited safe repr-string for the given object. + + Failing __repr__ functions of user instances will be represented + with a short exception info and 'saferepr' generally takes + care to never raise exceptions itself. + + This function is a wrapper around the Repr/reprlib functionality of the + stdlib. + """ + + return SafeRepr(maxsize, use_ascii).repr(obj) class TagTracerSub: def __init__(self, root: TagTracer, tags: tuple[str, ...]) -> None: From cff191d76d29360bdab15bbdafda2b75b7fce158 Mon Sep 17 00:00:00 2001 From: Anusha Shekhar Date: Tue, 12 Dec 2023 22:36:28 -0500 Subject: [PATCH 2/7] Update all repr occurences to use saferepr --- src/pluggy/_hooks.py | 7 ++++--- testing/benchmark.py | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 916ca704..668a8bbf 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -25,6 +25,7 @@ from typing import Union from ._result import Result +from ._tracing import saferepr _T = TypeVar("_T") @@ -456,7 +457,7 @@ def _add_hookimpl(self, hookimpl: HookImpl) -> None: self._hookimpls.insert(i + 1, hookimpl) def __repr__(self) -> str: - return f"" + return f"" def _verify_all_args_are_provided(self, kwargs: Mapping[str, object]) -> None: # This is written to avoid expensive operations when not needed. @@ -609,7 +610,7 @@ def _call_history(self) -> _CallHistory | None: # type: ignore[override] return self._orig._call_history def __repr__(self) -> str: - return f"<_SubsetHookCaller {self.name!r}>" + return f"<_SubsetHookCaller {saferepr(self.name)}>" @final @@ -667,7 +668,7 @@ def __init__( self.trylast: Final = hook_impl_opts["trylast"] def __repr__(self) -> str: - return f"" + return f"" @final diff --git a/testing/benchmark.py b/testing/benchmark.py index 906d8fe5..2860dfd4 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -9,6 +9,8 @@ from pluggy._callers import _multicall from pluggy._hooks import HookImpl +from ._tracing import saferepr + hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") @@ -77,7 +79,7 @@ def __init__(self, num: int) -> None: self.num = num def __repr__(self) -> str: - return f"" + return f"" @hookimpl def fun(self, hooks, nesting: int) -> None: @@ -89,7 +91,7 @@ def __init__(self, num: int) -> None: self.num = num def __repr__(self) -> str: - return f"" + return f"" @hookimpl(wrapper=True) def fun(self): From 86a0b355836e6ea9f2aca3a8ff7fc2569299e603 Mon Sep 17 00:00:00 2001 From: Anusha Shekhar Date: Tue, 12 Dec 2023 22:36:59 -0500 Subject: [PATCH 3/7] Add tests for saferepr --- testing/test_tracer.py | 171 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/testing/test_tracer.py b/testing/test_tracer.py index 5e538369..b63637c6 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -2,6 +2,7 @@ import pytest +from pluggy._tracing import saferepr, DEFAULT_REPR_MAX_SIZE from pluggy._tracing import TagTracer @@ -77,3 +78,173 @@ def test_setprocessor(rootlogger: TagTracer) -> None: log2("seen") tags, args = l2[0] assert args == ("seen",) + +def test_saferepr_simple_repr(): + assert saferepr(1) == "1" + assert saferepr(None) == "None" + + +def test_saferepr_maxsize(): + s = saferepr("x" * 50, maxsize=25) + assert len(s) == 25 + expected = repr("x" * 10 + "..." + "x" * 10) + assert s == expected + + +def test_saferepr_no_maxsize(): + text = "x" * DEFAULT_REPR_MAX_SIZE * 10 + s = saferepr(text, maxsize=None) + expected = repr(text) + assert s == expected + + +def test_saferepr_maxsize_error_on_instance(): + class A: + def __repr__(self): + raise ValueError("...") + + s = saferepr(("*" * 50, A()), maxsize=25) + assert len(s) == 25 + assert s[0] == "(" and s[-1] == ")" + + +def test_saferepr_exceptions() -> None: + class BrokenRepr: + def __init__(self, ex): + self.ex = ex + + def __repr__(self): + raise self.ex + + class BrokenReprException(Exception): + __str__ = None # type: ignore[assignment] + __repr__ = None # type: ignore[assignment] + + assert "Exception" in saferepr(BrokenRepr(Exception("broken"))) + s = saferepr(BrokenReprException("really broken")) + assert "TypeError" in s + assert "TypeError" in saferepr(BrokenRepr("string")) + + none = None + try: + none() # type: ignore[misc] + except BaseException as exc: + exp_exc = repr(exc) + obj = BrokenRepr(BrokenReprException("omg even worse")) + s2 = saferepr(obj) + assert s2 == ( + "<[unpresentable exception ({!s}) raised in repr()] BrokenRepr object at 0x{:x}>".format( + exp_exc, id(obj) + ) + ) + + +def test_saferepr_baseexception(): + """Test saferepr() with BaseExceptions, which includes pytest outcomes.""" + + class RaisingOnStrRepr(BaseException): + def __init__(self, exc_types): + self.exc_types = exc_types + + def raise_exc(self, *args): + try: + self.exc_type = self.exc_types.pop(0) + except IndexError: + pass + if hasattr(self.exc_type, "__call__"): + raise self.exc_type(*args) + raise self.exc_type + + def __str__(self): + self.raise_exc("__str__") + + def __repr__(self): + self.raise_exc("__repr__") + + class BrokenObj: + def __init__(self, exc): + self.exc = exc + + def __repr__(self): + raise self.exc + + __str__ = __repr__ + + baseexc_str = BaseException("__str__") + obj = BrokenObj(RaisingOnStrRepr([BaseException])) + assert saferepr(obj) == ( + "<[unpresentable exception ({!r}) " + "raised in repr()] BrokenObj object at 0x{:x}>".format(baseexc_str, id(obj)) + ) + obj = BrokenObj(RaisingOnStrRepr([RaisingOnStrRepr([BaseException])])) + assert saferepr(obj) == ( + "<[{!r} raised in repr()] BrokenObj object at 0x{:x}>".format( + baseexc_str, id(obj) + ) + ) + + with pytest.raises(KeyboardInterrupt): + saferepr(BrokenObj(KeyboardInterrupt())) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(SystemExit())) + + with pytest.raises(KeyboardInterrupt): + saferepr(BrokenObj(RaisingOnStrRepr([KeyboardInterrupt]))) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(RaisingOnStrRepr([SystemExit]))) + + with pytest.raises(KeyboardInterrupt): + print(saferepr(BrokenObj(RaisingOnStrRepr([BaseException, KeyboardInterrupt])))) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(RaisingOnStrRepr([BaseException, SystemExit]))) + + +def test_saferepr_buggy_builtin_repr(): + # Simulate a case where a repr for a builtin raises. + # reprlib dispatches by type name, so use "int". + + class int: + def __repr__(self): + raise ValueError("Buggy repr!") + + assert "Buggy" in saferepr(int()) + + +def test_saferepr_big_repr(): + from _pytest._io.saferepr import SafeRepr + + assert len(saferepr(range(1000))) <= len("[" + SafeRepr(0).maxlist * "1000" + "]") + + +def test_saferepr_repr_on_newstyle() -> None: + class Function: + def __repr__(self): + return "<%s>" % (self.name) # type: ignore[attr-defined] + + assert saferepr(Function()) + + +def test_saferepr_unicode(): + val = "£€" + reprval = "'£€'" + assert saferepr(val) == reprval + + +def test_saferepr_broken_getattribute(): + """saferepr() can create proper representations of classes with + broken __getattribute__ (#7145) + """ + + class SomeClass: + def __getattribute__(self, attr): + raise RuntimeError + + def __repr__(self): + raise RuntimeError + + assert saferepr(SomeClass()).startswith( + "<[RuntimeError() raised in repr()] SomeClass object at 0x" + ) \ No newline at end of file From e7f6b98bce1e2af2c32c07739bb2ab2870e57286 Mon Sep 17 00:00:00 2001 From: Anusha Shekhar Date: Tue, 12 Dec 2023 22:37:24 -0500 Subject: [PATCH 4/7] Add test for usage of saferepr --- testing/test_hookcaller.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 88ed1316..8a7d1aeb 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -11,6 +11,7 @@ from pluggy import PluginValidationError from pluggy._hooks import HookCaller from pluggy._hooks import HookImpl +from pluggy._tracing import saferepr hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") @@ -448,3 +449,21 @@ def conflict(self) -> None: "Hook 'conflict' is already registered within namespace " ".Api1'>" ) + +def test_hookcaller_repr_with_saferepr_failure(hc: HookCaller, addmeth: AddMeth) -> None: + @addmeth() + def he_method1() -> None: + pass + + @addmeth() + def he_method2() -> None: + # Intentional error to make the repr fail + raise ValueError("Intentional error in he_method2") + + @addmeth() + def he_method3() -> None: + pass + + # Verify that HookCaller.repr with saferepr still works despite the error + expected_repr = f"" + assert repr(hc) == expected_repr \ No newline at end of file From 5fa11642039ce79ee2e00521098fb33e99a525f5 Mon Sep 17 00:00:00 2001 From: Anusha Shekhar Date: Wed, 13 Dec 2023 22:32:20 -0500 Subject: [PATCH 5/7] linting --- src/pluggy/_hooks.py | 5 ++++- src/pluggy/_tracing.py | 17 +++++++++++++---- testing/benchmark.py | 3 +-- testing/test_hookcaller.py | 13 ++++--------- testing/test_tracer.py | 24 ++++++++++++++---------- 5 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 668a8bbf..253f1dde 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -668,7 +668,10 @@ def __init__( self.trylast: Final = hook_impl_opts["trylast"] def __repr__(self) -> str: - return f"" + return ( + f"" + ) @final diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index 7be55493..41d9b939 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -3,11 +3,11 @@ """ from __future__ import annotations +import reprlib from typing import Any from typing import Callable from typing import Sequence from typing import Tuple -import reprlib _Writer = Callable[[str], object] @@ -60,6 +60,7 @@ def setprocessor(self, tags: str | tuple[str, ...], processor: _Processor) -> No assert isinstance(tags, tuple) self._tags2proc[tags] = processor + def _try_repr_or_str(obj: object) -> str: try: return repr(obj) @@ -68,6 +69,7 @@ def _try_repr_or_str(obj: object) -> str: except BaseException: return f'{type(obj).__name__}("{obj}")' + def _format_repr_exception(exc: BaseException, obj: object) -> str: try: exc_info = _try_repr_or_str(exc) @@ -79,20 +81,24 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str: exc_info, type(obj).__name__, id(obj) ) + def _ellipsize(s: str, maxsize: int) -> str: if len(s) > maxsize: i = max(0, (maxsize - 3) // 2) j = max(0, maxsize - 3 - i) - return s[:i] + "..." + s[len(s) - j :] + + x = len(s) - j + return s[:i] + "..." + s[x:] return s + class SafeRepr(reprlib.Repr): """ repr.Repr that limits the resulting size of repr() and includes information on exceptions raised during the call. """ - def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None: + def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None: """ :param maxsize: If not None, will truncate the resulting repr to that specific size, using ellipsis @@ -136,8 +142,10 @@ def repr_instance(self, x: object, level: int) -> str: # Maximum size of overall repr of objects to display during assertion errors. DEFAULT_REPR_MAX_SIZE = 240 + + def saferepr( - obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False + obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False ) -> str: """Return a size-limited safe repr-string for the given object. @@ -151,6 +159,7 @@ def saferepr( return SafeRepr(maxsize, use_ascii).repr(obj) + class TagTracerSub: def __init__(self, root: TagTracer, tags: tuple[str, ...]) -> None: self.root = root diff --git a/testing/benchmark.py b/testing/benchmark.py index 2860dfd4..ea13bfb2 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -3,14 +3,13 @@ """ import pytest +from ._tracing import saferepr from pluggy import HookimplMarker from pluggy import HookspecMarker from pluggy import PluginManager from pluggy._callers import _multicall from pluggy._hooks import HookImpl -from ._tracing import saferepr - hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 8a7d1aeb..b21902e6 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -450,20 +450,15 @@ def conflict(self) -> None: ".Api1'>" ) -def test_hookcaller_repr_with_saferepr_failure(hc: HookCaller, addmeth: AddMeth) -> None: - @addmeth() - def he_method1() -> None: - pass +def test_hookcaller_repr_with_saferepr_failure( + hc: HookCaller, addmeth: AddMeth +) -> None: @addmeth() def he_method2() -> None: # Intentional error to make the repr fail raise ValueError("Intentional error in he_method2") - @addmeth() - def he_method3() -> None: - pass - # Verify that HookCaller.repr with saferepr still works despite the error expected_repr = f"" - assert repr(hc) == expected_repr \ No newline at end of file + assert repr(hc) == expected_repr diff --git a/testing/test_tracer.py b/testing/test_tracer.py index b63637c6..46658486 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -2,7 +2,8 @@ import pytest -from pluggy._tracing import saferepr, DEFAULT_REPR_MAX_SIZE +from pluggy._tracing import DEFAULT_REPR_MAX_SIZE +from pluggy._tracing import saferepr from pluggy._tracing import TagTracer @@ -79,6 +80,7 @@ def test_setprocessor(rootlogger: TagTracer) -> None: tags, args = l2[0] assert args == ("seen",) + def test_saferepr_simple_repr(): assert saferepr(1) == "1" assert saferepr(None) == "None" @@ -110,10 +112,10 @@ def __repr__(self): def test_saferepr_exceptions() -> None: class BrokenRepr: - def __init__(self, ex): + def __init__(self, ex) -> None: self.ex = ex - def __repr__(self): + def __repr__(self) -> str: raise self.ex class BrokenReprException(Exception): @@ -143,10 +145,10 @@ def test_saferepr_baseexception(): """Test saferepr() with BaseExceptions, which includes pytest outcomes.""" class RaisingOnStrRepr(BaseException): - def __init__(self, exc_types): + def __init__(self, exc_types) -> None: self.exc_types = exc_types - def raise_exc(self, *args): + def raise_exc(self, *args) -> None: try: self.exc_type = self.exc_types.pop(0) except IndexError: @@ -155,17 +157,19 @@ def raise_exc(self, *args): raise self.exc_type(*args) raise self.exc_type - def __str__(self): + def __str__(self) -> str: self.raise_exc("__str__") + return "" - def __repr__(self): + def __repr__(self) -> str: self.raise_exc("__repr__") + return "" class BrokenObj: - def __init__(self, exc): + def __init__(self, exc) -> None: self.exc = exc - def __repr__(self): + def __repr__(self) -> str: raise self.exc __str__ = __repr__ @@ -247,4 +251,4 @@ def __repr__(self): assert saferepr(SomeClass()).startswith( "<[RuntimeError() raised in repr()] SomeClass object at 0x" - ) \ No newline at end of file + ) From cb4bb025ecaa10f6d5d9b9bd25e5af549238b95e Mon Sep 17 00:00:00 2001 From: Anusha Shekhar Date: Wed, 13 Dec 2023 22:36:27 -0500 Subject: [PATCH 6/7] Fix failing test in Ubuntu --- testing/benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/benchmark.py b/testing/benchmark.py index ea13bfb2..a052b13f 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -3,12 +3,12 @@ """ import pytest -from ._tracing import saferepr from pluggy import HookimplMarker from pluggy import HookspecMarker from pluggy import PluginManager from pluggy._callers import _multicall from pluggy._hooks import HookImpl +from pluggy._tracing import saferepr hookspec = HookspecMarker("example") From 11d6c28108f2deb5195ac20f069578065ead9d42 Mon Sep 17 00:00:00 2001 From: Anusha Shekhar Date: Wed, 13 Dec 2023 22:57:06 -0500 Subject: [PATCH 7/7] Codecov --- testing/test_hookcaller.py | 79 +++++++++++++++++++++++++++++++------- testing/test_tracer.py | 3 -- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index b21902e6..f482eaa6 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -11,6 +11,7 @@ from pluggy import PluginValidationError from pluggy._hooks import HookCaller from pluggy._hooks import HookImpl +from pluggy._hooks import HookimplOpts from pluggy._tracing import saferepr hookspec = HookspecMarker("example") @@ -410,7 +411,8 @@ def hello(self, arg: object) -> None: pm.add_hookspecs(Api) - # make sure a bad signature still raises an error when using specname + """make sure a bad signature still raises an error when using specname""" + class Plugin: @hookimpl(specname="hello") def foo(self, arg: int, too, many, args) -> int: @@ -419,8 +421,9 @@ def foo(self, arg: int, too, many, args) -> int: with pytest.raises(PluginValidationError): pm.register(Plugin()) - # make sure check_pending still fails if specname doesn't have a - # corresponding spec. EVEN if the function name matches one. + """make sure check_pending still fails if specname doesn't have a + corresponding spec. EVEN if the function name matches one.""" + class Plugin2: @hookimpl(specname="bar") def hello(self, arg: int) -> int: @@ -451,14 +454,62 @@ def conflict(self) -> None: ) -def test_hookcaller_repr_with_saferepr_failure( - hc: HookCaller, addmeth: AddMeth -) -> None: - @addmeth() - def he_method2() -> None: - # Intentional error to make the repr fail - raise ValueError("Intentional error in he_method2") - - # Verify that HookCaller.repr with saferepr still works despite the error - expected_repr = f"" - assert repr(hc) == expected_repr +def test_hook_impl_initialization() -> None: + # Mock data + plugin = "example_plugin" + plugin_name = "ExamplePlugin" + + def example_function(x): + return x + + hook_impl_opts: HookimplOpts = { + "wrapper": False, + "hookwrapper": False, + "optionalhook": False, + "tryfirst": False, + "trylast": False, + "specname": "", + } + + # Initialize HookImpl + hook_impl = HookImpl(plugin, plugin_name, example_function, hook_impl_opts) + + # Verify attributes are set correctly + assert hook_impl.function == example_function + assert hook_impl.argnames == ("x",) + assert hook_impl.kwargnames == () + assert hook_impl.plugin == plugin + assert hook_impl.opts == hook_impl_opts + assert hook_impl.plugin_name == plugin_name + assert not hook_impl.wrapper + assert not hook_impl.hookwrapper + assert not hook_impl.optionalhook + assert not hook_impl.tryfirst + assert not hook_impl.trylast + + +def test_hook_impl_representation() -> None: + # Mock data + plugin = "example_plugin" + plugin_name = "ExamplePlugin" + + def example_function(x): + return x + + hook_impl_opts: HookimplOpts = { + "wrapper": False, + "hookwrapper": False, + "optionalhook": False, + "tryfirst": False, + "trylast": False, + "specname": "", + } + + # Initialize HookImpl + hook_impl = HookImpl(plugin, plugin_name, example_function, hook_impl_opts) + + # Verify __repr__ method + expected_repr = ( + f"" + ) + assert repr(hook_impl) == expected_repr diff --git a/testing/test_tracer.py b/testing/test_tracer.py index 46658486..2fdc2eac 100644 --- a/testing/test_tracer.py +++ b/testing/test_tracer.py @@ -243,9 +243,6 @@ def test_saferepr_broken_getattribute(): """ class SomeClass: - def __getattribute__(self, attr): - raise RuntimeError - def __repr__(self): raise RuntimeError