wrapt: pydevd error when debugging with wrapt

Hi there,

I tested with python 3.12 & python 3.12.1 (x64) on windows 10 22H2. When I debug with pycharm 2023.3 and wrapt-1.16.0, I get this error:

File "C:\Users\endmarsfr\AppData\Local\Programs\Python\Lib\site-packages\wrapt\decorators.py", line 239, in _build return AdapterWrapper(wrapped=wrapped, wrapper=wrapper, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "_pydevd_bundle/pydevd_pep_669_tracing_cython.pyx", line 504, in _pydevd_bundle.pydevd_pep_669_tracing_cython.PyRaiseCallback.__call__ frame = self.frame File "_pydevd_bundle/pydevd_pep_669_tracing_cython.pyx", line 47, in _pydevd_bundle.pydevd_pep_669_tracing_cython.PEP669CallbackBase.frame while frame and isinstance(frame.f_locals.get('self'), PEP669CallbackBase): ValueError: wrapper has not been initialized python-BaseException

Kind regards

About this issue

  • Original URL
  • State: open
  • Created 7 months ago
  • Reactions: 3
  • Comments: 27 (10 by maintainers)

Most upvoted comments

@hartym @endmarsfr @GrahamDumpleton Found a workaround on the PyCharm issues tracker. In PyCharm, go to Help > Find Action > Registry and uncheck the box for python.debug.low.impact.monitoring.api.

Debugger did not terminate but am still trying to get the variables to show (this may be from me tinkering with other settings trying to find a solution). Works fine now.

Thanks for the confirmation that raising AttributeError avoids issue as suspected it might. The trick of using multiple inheritance so the exception type is both AttributeError and ValueError is also very interesting. I didn’t even think about such a trick and resolves a concern I had of how to change the error type raised without potentially breaking existing code. I was thinking I would have to release a new version which still used ValueError, but via an environment variable flag switch it to AttributeError and allow people to use that to flesh out problems in real world applications before commit to switch to AttributeError as default.

As to monkey patching a temporary fix into existing code, I suspect that wrapt could be used to do that and have it monkey patch itself. I will need to play with that idea as a temporary fix.

@zyoung-rc I confirm your fixture works for me, the small adjustments I made were to use scope=“session” (should the patch be applied once per module ? I believe for now that it can be set for the whole testing session) and autouse=True (so I don’t have to explicitely request it in each test). Also, I did put that code in a conftest.py file at root so pytest just get it. Thanks a lot for your work on that temporary fix !

@GrahamDumpleton I’ve narrowed it down further. It appears PyCharm does not like the ValueError raised. Changing it to an AttributeError seems to appease the debugger. So something like

from wrapt.wrappers import ObjectProxy

class ObjectProxyWithAttributeError(ObjectProxy):
    def __getattr__(self, name):
        if name == '__wrapped__':
            raise AttributeError('wrapper has not been initialised')

        return getattr(self.__wrapped__, name)

passes = ObjectProxyWithAttributeError("test")  # This works
fails = ObjectProxy("test")  # This raises an error from the PyCharm debugger

Something like this also works and would be more backwards compatible

class WrapperNotInitalisedError(AttributeError, ValueError):
    def __init__(self, msg:str = 'wrapper has not been initialised'):
        super().__init__(msg)

class ObjectProxyWithAttributeError(ObjectProxy):
    def __getattr__(self, name):
        if name == '__wrapped__':
            raise WrapperNotInitalisedError()

        return getattr(self.__wrapped__, name)

I hope this helps!

I use pytest and this fixture temporarily fixes the problem until a permanent fix can be made. It can easily be adapted to unittest or another framework.

@pytest.fixture(scope="session")
def patch_wrapt_for_pycharm():
    from wrapt import decorators, FunctionWrapper
    from wrapt.decorators import AdapterWrapper, _AdapterFunctionSurrogate

    class _PatchedAdapterFunctionSurrogate(_AdapterFunctionSurrogate):
        @property
        def __class__(self):
            try:
                return super().__class__
            except ValueError:
                return type(self)


    class PatchedAdapterWrapper(AdapterWrapper):
        def __init__(self, *args, **kwargs):
            adapter = kwargs.pop("adapter")
            FunctionWrapper.__init__(self, *args, **kwargs)
            self._self_surrogate = _PatchedAdapterFunctionSurrogate(self.__wrapped__, adapter)
            self._self_adapter = adapter

        @property
        def __class__(self):
            try:
                return super().__class__
            except ValueError:
                return type(self)

    with pytest.MonkeyPatch.context() as patch:
        patch.setattr(decorators, "AdapterWrapper", PatchedAdapterWrapper)
        yield

It’s important to note this patching needs to run before @wrapt.decorator is used so you may need to play with the placement.

This test will not raise a PyCharm debugging error

def test_wrapt(patch_wrapt_for_pycharm):
    @wrapt.decorator
    def pass_through(wrapped, instance, args, kwargs):
        return wrapped(*args, **kwargs)

    @pass_through
    def function():
        print("Hello world")

    function()

This test will

@wrapt.decorator
def pass_through(wrapped, instance, args, kwargs):
    return wrapped(*args, **kwargs)

def test_wrapt(patch_wrapt_for_pycharm):
    @pass_through
    def function():
        print("Hello world")

    function()

Seems to be the __class__ property in your example. Here is the example in it’s minimally failing form

class Object: pass

class Wrapper:

    def __init__(self, force_error=False):
        if force_error:
            print(self.__wrapped__)
        self.func()
        object.__setattr__(self, "__wrapped__", Object())

    def __getattr__(self, name):
        if name == '__wrapped__':
            raise ValueError('wrapper has not been initialised')

        return getattr(self.__wrapped__, name)

    @property
    def __class__(self):
        return self.__wrapped__.__class__

    @__class__.setter
    def __class__(self, value):
        self.__wrapped__.__class__ = value

    def func(self):
        pass


wrapper = Wrapper()