Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pydevd error when debugging with wrapt #257

Open
endmarsfr opened this issue Dec 10, 2023 · 30 comments
Open

pydevd error when debugging with wrapt #257

endmarsfr opened this issue Dec 10, 2023 · 30 comments

Comments

@endmarsfr
Copy link

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

@GrahamDumpleton
Copy link
Owner

The debugger is trying to access attributes of the wrapt ProxyObject instance before it is initialized properly. The exception is due to that access when it is in an uninitialized state.

All you can really do is step over the initialization of the decorator and not into it, or see if it you can run to exit of the function in hope that the debugger isn't still trying to introspect the object instance while it is in the uninitialized state.

@endmarsfr
Copy link
Author

Hi Graham,

Do you think the problem is with the debugger?
All my scripts that use wrapt can no longer be debugged.
The debugger seems to work fine with other scripts.

kind regards

@GrahamDumpleton
Copy link
Owner

It is the nature of debuggers that they may provide a live view of objects on the stack or otherwise somehow in scope. If that feature is enabled, possibly simply by having a tracing view in the debugger visible, it may try to aggressively introspect objects, which could be an issue when objects can be in intermediate initialised states. That said, it should ignore any exceptions when doing introspection and not fail, so am not sure, especially not on the limited information you have given.

If you could record a video and post it on YouTube or somewhere else where I could watch how you use the debugger and what you are doing, plus where the error shows, that would be helpful as I don't use Python debuggers myself so don't know what they might actually be doing.

@cerrussell
Copy link

Hi @GrahamDumpleton, I'm having the same issue. Here's a video and traceback.

Additional details:

  • I am working on the feature/csaf-refactor branch of owasp-dep-scan/dep-scan.
  • I wanted to examine the output of the variables returned by the summarise function of line 1073 of cli.py and set my breakpoint a few lines later.
  • Our pygithub dependency uses wrapt, and the import of depscan.lib.github into cli.py means this happens whenever I try to debug.

@GrahamDumpleton
Copy link
Owner

Can you confirm that if you tell the debugger to continue from that point that it then will run until the next break point or exception?

Also, is there a way to tell the debugger to ignore exceptions which occur at certain points in the code when it is doing its introspection and continue running anyway?

As alluded to before, the issue is that the debugger is inserting a tracing function which then tries to introspect objects for every Python opcode (??) execution. This means it could easily trigger exceptions when objects are in an intermediary initialised state.

Right now am not sure what options I have to avoid this occurring. One might be to raise an AttributeError instead of ValueError exception. The AttributeError is often treated differently by things and causes stuff to fallback to some other action and do something differently or just ignore things.

If you wanted to see if changing the exception type would help, you would need to get down a copy of wrapt source code and change all places which raise ValueError with message "wrapper has not been initialized" to AttributeError instead. Install from that modified source code into your local virtual environment for testing and try again.

@cerrussell
Copy link

@GrahamDumpleton

Can you confirm that if you tell the debugger to continue from that point that it then will run until the next break point or exception?

If I tell it to continue, the debugger just terminates with that traceback in the console (plus "Process finished with exit code 1 at the bottom). There is no way of continuing to debug.

I'll give changing the exception type a try.

@hartym
Copy link

hartym commented Dec 29, 2023

I do see the same problem, that I could reduce to the following minimal case:

import wrapt

@wrapt.decorator
def foo(wrapped, instance, args, kwargs):
    pass

Running this (useless) code works (as in, it does nothing but does not raise anything) but if I run it through the intellij/pycharm debugger, the same exception as seen by the OP stops the process (no resume if I ask the debugger to continue).

Here is the stack trace and context (paths shortened):

.../bin/python3.12 -X pycache_prefix=~/Library/Caches/JetBrains/IntelliJIdea2023.3/cpython-cache ~/Library/Application Support/JetBrains/IntelliJIdea2023.3/plugins/python/helpers/pydev/pydevd.py --multiprocess --qt-support=auto --client 127.0.0.1 --port 49284 --file test_github.py 
Connected to pydev debugger (build 233.13135.103)
Traceback (most recent call last):
  File "~/Library/Application Support/JetBrains/IntelliJIdea2023.3/plugins/python/helpers/pydev/pydevd.py", line 1527, in _exec
    pydev_imports.execfile(file, globals, locals)  # execute the script
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~/Library/Application Support/JetBrains/IntelliJIdea2023.3/plugins/python/helpers/pydev/_pydev_imps/_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "test_github.py", line 4, in <module>
    @wrapt.decorator
     ^^^^^^^^^^^^^^^
  File "lib/python3.12/site-packages/wrapt/decorators.py", line 427, in decorator
    return _build(wrapper, _wrapper, adapter=decorator)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/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__
  File "_pydevd_bundle/pydevd_pep_669_tracing_cython.pyx", line 47, in _pydevd_bundle.pydevd_pep_669_tracing_cython.PEP669CallbackBase.frame
ValueError: wrapper has not been initialized
python-BaseException

Process finished with exit code 1

The "solution" I have for now is to go back to python3.11, the issue does not exist with it.

Any help will be greatly appreciated. As it is, it makes anything using wrapt unusable with jetbrains' debugger (like in my case, PyGithub or deprecated.deprecated)

@GrahamDumpleton
Copy link
Owner

Do you get the same problem if you set and export the environment variable:

WRAPT_DISABLE_EXTENSIONS=true

This will clarify whether it is only specific to C extension variant of wrapt, or also the pure Python version.

If know the pure Python version is affected, then I can try and create a small test case (independent of wrapt), which I believe may replicate the specific code I think is giving the debugger a problem.

If that shows the issue, then the exception type change as already explained above can be tried.

If can narrow it done that way, then can provide the details to debugger authors and they can look at the issue of why they fail on the particular code.

@cerrussell
Copy link

cerrussell commented Dec 30, 2023

@GrahamDumpleton I get the same problem even with that environment variable set.

This problem could be related to implementation of PEP 669 in Python 3.12. I meant to check this, but it slipped my mind until I saw @hartym's comment.

I can confirm that I do not have this problem in a Python 3.10 or 3.11 virtual environment. Looking at the pydev issue tracker, I don't see anything about this issue definitively, but there are a couple of other issues occurring in 3.12 that may have the same origin, including that pydev's own tests are failing.

@hartym May I ask what OS you're using? I can see that the original OP is using Windows, as am I. Given the tests were failing for Windows and Mac in the last pydev release, but not Linux, I'm wondering if this problem is OS-specific.

@hartym
Copy link

hartym commented Dec 30, 2023

@cerrussell I'm using MacOS 12.6

@GrahamDumpleton I tried running again my "test case" using python 3.12 and with WRAPT_DISABLE_EXTENSIONS=true, unfortunately, the behaviour is quite similar to before, the only difference is that now the debugger "understands" what file/line matches the error, which was not the case with the cython frames.

Without extensions
image

With extensions
image

@cerrussell
Copy link

cerrussell commented Dec 30, 2023

@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.

@endmarsfr
Copy link
Author

Hi @cerrussell
Thanks for the workaround.
I wish you happy holiday celebrations

@GrahamDumpleton
Copy link
Owner

GrahamDumpleton commented Dec 30, 2023

Can someone try and run this little test program through the debugger to see if it fails in same was as debugger has so far.

class Object: pass

class Wrapper:

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

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

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

        return getattr(self.__wrapped__, name)

    def __setattr__(self, name, value):
        setattr(self.__wrapped__, name, value)

    def func(self):
        pass


wrapper = Wrapper()

wrapper.xxx = True
print(wrapper.xxx)

print(wrapper.__dict__)
print(wrapper.__wrapped__.__dict__)

# wrapper = Wrapper(force_error=True)

If it doesn't I will try and tweak the example further as right now it doesn't mirror exactly what pure Python version of wrapt code does.

@cerrussell
Copy link

@GrahamDumpleton It didn't fail for me.

@GrahamDumpleton
Copy link
Owner

I just modified it so there would be code run before the setattr. Can you try again if you use the original version.

@GrahamDumpleton
Copy link
Owner

I have made another change to the code to try. This time added a __dict__ property. The debugger is likely using dir() to introspect the object but that fails. The prior screen shots since they don't expand all the stack frames, or can't do so since debugger is itself C code, you can't verify what it fails on.

Fudging up a simple debugger which tries to introspect an object, one would see:

started Object
started Wrapper
started caller
  caller: 49
started __init__
Traceback (most recent call last):
  File "/private/tmp/debug.py", line 55, in <module>
started __init__
started __init__
started getstate
started decode
    caller()
  File "/private/tmp/debug.py", line 49, in caller
started __init__
started __init__
started getstate
started decode
    wrapper = Wrapper()
              ^^^^^^^^^
  File "/private/tmp/debug.py", line 8, in __init__
started __init__
started __init__
started getstate
started decode
    def __init__(self, force_error=False):

  File "/private/tmp/debugger.py", line 51, in start_handler
started __init__
started __init__
started getstate
started decode
    dir(frame.f_locals.get('self'))
  File "/private/tmp/debug.py", line 23, in __dict__
started __init__
started __init__
started getstate
started decode
    return self.__wrapped__.__dict__
           ^^^^^^^^^^^^^^^^
  File "/private/tmp/debug.py", line 27, in __getattr__
started __init__
started __init__
started getstate
started decode
    raise ValueError('wrapper has not been initialised')
ValueError: wrapper has not been initialised

So try with latest code from comment #257 (comment)

@cerrussell
Copy link

@GrahamDumpleton No errors still.

@GrahamDumpleton
Copy link
Owner

Try again with this one. We just need to try and work out which special method might be tripping it up.

class Object: pass

class Wrapper:

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

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

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

        return getattr(self.__wrapped__, name)

    def __setattr__(self, name, value):
        setattr(self.__wrapped__, name, value)

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

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

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

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

    def __dir__(self):
        return dir(self.__wrapped__)

    def __str__(self):
        return str(self.__wrapped__)

    def __bytes__(self):
        return bytes(self.__wrapped__)

    def __repr__(self):
        return '<{} at 0x{:x} for {} at 0x{:x}>'.format(
                type(self).__name__, id(self),
                type(self.__wrapped__).__name__,
                id(self.__wrapped__))

    def func(self):
        pass


wrapper = Wrapper()

wrapper.xxx = True
print(wrapper.xxx)

print(wrapper.__dict__)
print(wrapper.__wrapped__.__dict__)

# wrapper = Wrapper(force_error=True)

@cerrussell
Copy link

@GrahamDumpleton That one got the error!

@GrahamDumpleton
Copy link
Owner

Do you get a nice stack traceback so we know which access is the problem?

@cerrussell
Copy link

cerrussell commented Dec 31, 2023

Here ya go.
traceback.txt

I think the repr exception is from PyCharm trying to render the variables after the initial exception.

@zyoung-rc
Copy link

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()

@zyoung-rc
Copy link

zyoung-rc commented Jan 20, 2024

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()

@zyoung-rc
Copy link

zyoung-rc commented Jan 20, 2024

@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!

@hartym
Copy link

hartym commented Jan 22, 2024

@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
Copy link
Owner

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
Copy link

@hartym I edited my comment to reflect the usage of scope="session". There was no particular reason for using module.

@GrahamDumpleton Glad to help! I would have written a PR, but, sadly, my C skills are non-existent.

@yangmeishu
Copy link

switch python to 3.10 can use

@shacharCaduri
Copy link

shacharCaduri commented Sep 20, 2024

Hi all,
I see this is still an open issue, and I am also experiencing it, when will a fix be merged?

EDIT: sorry I missed the MR mentioned above, on which version is it?

thanks in advance

@GrahamDumpleton
Copy link
Owner

Was actually hoping to catch up on this issue and look at it again in the coming week as finally have some time available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants