Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 570
href_ Avatar asked May 02 '12 11:05

href_


People also ask

Are decorators Pythonic?

A decorator is a design pattern in Python that allows a user to add new functionality to an existing object without modifying its structure. Decorators are usually called before the definition of a function you want to decorate.

What is a decorated function?

By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

Is decorator important in Python?

Needless to say, Python's decorators are incredibly useful. Not only can they be used to slow down the time it takes to write some code, but they can also be incredibly helpful at speeding up code. Not only are decorators incredibly useful when you find them about, but it is also a great idea to write your own.

What are fancy decorators in Python?

These fancy symbols are called decorators. Python decorators represent a form of metaprogramming. This means they have the ability to modify another part of the program while the code is running. In simple terminology, they are functions that take other functions as arguments.


3 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
like image 76
PaulMcG Avatar answered Oct 20 '22 16:10

PaulMcG


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.

like image 38
unholysampler Avatar answered Oct 20 '22 18:10

unholysampler


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>                                                  
like image 2
Błażej Michalik Avatar answered Oct 20 '22 17:10

Błażej Michalik