Is there a way to step into decorated functions, skipping decorator code

I have a module which decorates some key functions with custom decorators.

Debugging these functions with pdb often is a bit of a pain, because every time I step into a decorated function I first have to step through the decorator code itself.

I could of course just set the debugger to break within the function I'm interested in, but as key functions they are called many times from many places so I usually prefer to start debugging outside the function.

I tried to illustrate it with code, but I don't know if that helps:

def i_dont_care_about_this(fn):
    @functiontools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@i_dont_care_about_this
def i_only_care_about_this():
    # no use to set pdb here

def i_am_here():
    import pdb; pdb.set_trace()
    i_only_care_about_this()

So, is there a way for me to step into i_only_care_about_this from i_am_herewithout going through i_dont_care_about_this?

Essentially I want to skip all decorator code when using s to (s)tep into a given decorated function.

Answers


If the decorator is purely for logging or other non-functional behavior, then make it a no-op for debugging - insert this code right after the definition of i_dont_care_about_this:

DEBUG = False
# uncomment this line when pdb'ing
# DEBUG = True
if DEBUG:
    i_dont_care_about_this = lambda fn : fn

But if it contains actual active code, then you will have to do the work using pdb methods, such as a conditionalized call to pdb.set_trace inside the code inside the decorator:

BREAK_FLAG = False
...
# (inside your function you want to debug)
if BREAK_FLAG:
    import pdb; pdb.set_trace()
...
# at your critical calling point
BREAK_FLAG = True

I don't think you can do that. It would change the meaning of step to be something very different.

However, there is a way to achieve something similar to what you want. Set a breakpoint in your decorated function and one just before the decorated function is called. Now, disable the breakpoint inside the function.

Now, when you run the code, it will only break when you reach the specific invocation you care about. Once that break happens, re-enable the breakpoint in the function and continue the execution. This will execute all the decorated code and break on the first line of the decorated function.


TL;DR: Modify bdb.Bdb so that it adds decorator's module name to the list of skipped code. This works with both pdb and ipdb, possibly many others. Examples at the bottom.

From my own experiments with pdb.Pdb (the class that actually does the debugging in case of pdb and ipdb), it seems like it is perfectly doable without modifying neither the code of the function you want to debug nor the decorator.

Python debuggers have facilities that make it possible to skip some predefined code. After all, the debuger has to skip its own code to be of any use.

In fact, the base class for python debuggers has something called "skip argument". It's an argument to it's __init__(), that specifies what the debugger should ignore.

From Python Documentation:

The skip argument, if given, must be an iterable of glob-style module name patterns. The debugger will not step into frames that originate in a module that matches one of these patterns. Whether a frame is considered to originate in a certain module is determined by the name in the frame globals.

The problem with this is that it is specified on a call to set_trace(), after which we already landed in the frame of the decorator, on a break. So there is no feature there that would let us add to that argument at runtime.

Fortunately, modifying existing code at runtime is easy in Python, and there are hacks that we can use to add decorator's module name whenever Bdb.__init__() is called. We can "decorate" Bdb class, so that our module is added to skip list whenever someone creates a Bdb object.

So, here be the example of just that. Please excuse the weird signature and usage of Bdb.__init__() instead of super() - in order to be compatible with pdb we have to do it this way:

# magic_decorator.py
import bdb

old_bdb = bdb.Bdb


class DontDebugMeBdb(bdb.Bdb):
    @classmethod
    def __init__(cls, *args, **kwargs):
        if 'skip' not in kwargs or kwargs['skip'] is None:
            kwargs['skip'] = []
        kwargs['skip'].append(__name__)
        old_bdb.__init__(*args, **kwargs)

    @staticmethod
    def reset(*args, **kwargs):
        old_bdb.reset(*args, **kwargs)


bdb.Bdb = DontDebugMeBdb


def dont_debug_decorator(func):
    print("Decorating {}".format(func))

    def decorated():
        """IF YOU SEE THIS IN THE DEBUGER - YOU LOST"""
        print("I'm decorated")
        return func()
    return decorated

# buged.py
from magic_decorator import dont_debug_decorator


@dont_debug_decorator
def debug_me():
    print("DEBUG ME")

Output of ipdb.runcall in Ipython:

In [1]: import buged, ipdb                             
Decorating <function debug_me at 0x7f0edf80f9b0>       

In [2]: ipdb.runcall(buged.debug_me)                   
I'm decorated                                          
--Call--                                               
> /home/mrmino/treewrite/buged.py(4)debug_me()         
      3                                                
----> 4 @dont_debug_decorator                          
      5 def debug_me():                                

ipdb>                                                  

With the following:

def my_decorator(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@my_decorator
def my_func():
    ...

I invoke pdb with import pdb; pdb.run('my_func()') which enters pdb here:

> <string>(1)<module>()
  1. step to enter the call stack – we are now looking at the first line of the decorator function definition:

       def my_decorator(fn):
    ->     def wrapper(*args, **kwargs):
               return fn(*args, **kwargs)
           return wrapper
    
  2. next until pdb is on (pointing at) the line where we return the original function (this may be one next or multiple – just depends on your code):

       def my_decorator(fn):
           def wrapper(*args, **kwargs):
    ->         return fn(*args, **kwargs)
           return wrapper
    
  3. step into the original function and voila! we are now at at the point where we can next through our original function.

    -> @my_decorator
       def my_funct():
           ...
    

Need Your Help

How to search and replace text in a file using Python?

file text python-3.x replace

How do I search and replace text in a file using Python 3?

Quickblox: an issue in deleting content item (picture file / blob)

ios sdk quickblox

I am having a weird issue when I try to delete a content item (picture blob item for the user), stored as PNG picture file in the back-end database.