Search code examples
pythondebuggingdecoratorpython-decorators

Python Decorator for printing every line executed by a function


I want to, for debugging purposes, print out something pertaining to each and every line executed in a python method.

For example if there was some assignment in the line, i want to print what value was assigned for that variable, and if there was a function call, i want to print out the value returned by the function, etc.

So, for example if i were to use a decorator, applied on function/method such as :

@some_decorator
def testing() : 
    a = 10
    b = 20
    c = a + b
    e = test_function()

the function testing when called, should print the following :

a = 10
b = 20  
c = 30
e = some_value

Is there some way to achieve this? More fundamentally, i want to know whether i can write a code that can go through some other code line by line, check what type of an instruction it is, etc. Or maybe like we can get a dictionary for finding out all the variables in a class, can i get a dictionary like datastructure for getting every instruction in a function, which is as good a metaprogram can get.

Hence, I am particularly looking a solution using decorators, as I am curious if one can have a decorator that can go through an entire function line by line, and decorate it line by line, but any and all solutions are welcome.

Thanks in advance.


Solution

  • How about something like this? Would this work for you?

    Debug Context:

    import sys
    
    class debug_context():
        """ Debug context to trace any function calls inside the context """
    
        def __init__(self, name):
            self.name = name
    
        def __enter__(self):
            print('Entering Debug Decorated func')
            # Set the trace function to the trace_calls function
            # So all events are now traced
            sys.settrace(self.trace_calls)
    
        def __exit__(self, *args, **kwargs):
            # Stop tracing all events
            sys.settrace = None
    
        def trace_calls(self, frame, event, arg): 
            # We want to only trace our call to the decorated function
            if event != 'call':
                return
            elif frame.f_code.co_name != self.name:
                return
            # return the trace function to use when you go into that 
            # function call
            return self.trace_lines
    
        def trace_lines(self, frame, event, arg):
            # If you want to print local variables each line
            # keep the check for the event 'line'
            # If you want to print local variables only on return
            # check only for the 'return' event
            if event not in ['line', 'return']:
                return
            co = frame.f_code
            func_name = co.co_name
            line_no = frame.f_lineno
            filename = co.co_filename
            local_vars = frame.f_locals
            print ('  {0} {1} {2} locals: {3}'.format(func_name, 
                                                      event,
                                                      line_no, 
                                                      local_vars))
    

    Debug Decorator:

    def debug_decorator(func):
        """ Debug decorator to call the function within the debug context """
        def decorated_func(*args, **kwargs):
            with debug_context(func.__name__):
                return_value = func(*args, **kwargs)
            return return_value
        return decorated_func
    

    Usage

    @debug_decorator
    def testing() : 
        a = 10
        b = 20
        c = a + b
    
    testing()
    

    Output

    ###########################################################
    #output:
    #   Entering Debug Decorated func
    #     testing line 44 locals: {}
    #     testing line 45 locals: {'a': 10}
    #     testing line 46 locals: {'a': 10, 'b': 20}
    #     testing return 46 locals: {'a': 10, 'b': 20, 'c': 30}
    ###########################################################