Function and method debugging decorator - Part 2 This is a follow-up to this question.



I've refactored my previous debugging decorator, and added a couple new features, and changed a few things. Here's a complete list of things that have changed:


There is only one decorator, Debug, and it now supports functions, and class methods.
Each debug message is prefixed with [debug] to help distinguish it from normal output.
The output now tells you what it's outputting, rather than just outputting unreadable data.
The decorator will now output local variables names, along with argument and keyword argument names as well.


I'm wondering the following:


Is there a way to get the values of local variables in the function, or is that just not possible?
Is there a shorter way to get the names of local variables than function.__code__.co_varnames?
Is it a good idea to create an empty string, and then add to, and re-assign it to build an output string?
Is this Python 3, and Python 2.7 compatible?
How's my documentation?
Is this code "pythonic"?


debug.py

from pprint import pformat
from inspect import getargspec

class Debug(object):
    """Decorator for debugging functions.

    This decorator is used to debug a function, or
    a class method. If this is applied to a normal
    function, it will print out the arguments of

    Keyword arguments:
    debug -- Whether or not you want to output debug info. Generally, a global DEBUG variable is passed in here.
    """
    def __init__(self, debug=True):
        self.debug = debug

    def __format_debug_string(self, function, *args, **kwargs):
        """Return a formatted debug string.

        This is a small private helper function that will
        return a string value with certain debug information.

        Keyword arguments:
        function -- The function to debug.
        *args    -- The normal arguments of the function.
        **kwargs -- The keyword arguments of the function.
        """
        debug_string = ""
        debug_string += "[debug] {}\n".format(pformat(function))
        debug_string += "[debug] Passed args: {}\n".format(pformat(args))
        debug_string += "[debug] Passed kwargs: {}\n".format(pformat(kwargs))
        debug_string += "[debug] Locals: {}".format(pformat(function.__code__.co_varnames))
        return debug_string

    def __call__(self, function):
        def wrapper(*args, **kwargs):
            if self.debug:
                if getargspec(function).args[0] != "self":
                    print(self.__format_debug_string(function, *args, **kwargs))
                else:
                    print(self.__format_debug_string(function, *args, **kwargs))
                    print("[debug] Parent attributes: {}".format(pformat(args[0].__dict__)))

            return function(*args, **kwargs)
        return wrapper


Here are a few small, albeit unreadable tests, but it's good enough to get the point across:

from debug import Debug

@Debug(debug=True)
def a(a, b):
    d = 10
    return a * b

print(a(10, 10))

class B(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    @Debug(debug=True)
    def e(self, c):
        return self.a * self.b * c

c = B(10, 10)
print(c.e(10))


Here's the output of these tests:




[debug] &lt;function a at 0x1bf9d38&gt;
[debug] Passed args: (10, 10)
[debug] Passed kwargs: {}
[debug] Locals: ('a', 'b', 'd')
100
[debug] &lt;function B.e at 0x1944ce8&gt;
[debug] Passed args: (&lt;B object at 0x1bfc838&gt;, 10)
[debug] Passed kwargs: {}
[debug] Locals: ('self', 'c')
[debug] Parent attributes: {'a': 10, 'b': 10}
1000
You can improve the following:


            if getargspec(function).args[0] != "self":
                print(self.__format_debug_string(function, *args, **kwargs))
            else:
                print(self.__format_debug_string(function, *args, **kwargs))
                print("[debug] Parent attributes: {}".format(pformat(args[0].__dict__)))



If the code is executed no matter the statement, and it always goes first, move it above the condition: (and don't forget to reverse the condition)

            print(self.__format_debug_string(function, *args, **kwargs))
            if getargspec(function).args[0] == "self":
                print("[debug] Parent attributes: {}".format(pformat(args[0].__dict__)))


As for this:


    debug_string = ""
    debug_string += "[debug] {}\n".format(pformat(function))
    debug_string += "[debug] Passed args: {}\n".format(pformat(args))
    debug_string += "[debug] Passed kwargs: {}\n".format(pformat(kwargs))
    debug_string += "[debug] Locals: {}".format(pformat(function.__code__.co_varnames))
    return debug_string



You can remove the = "" entirely:

    debug_string =  "[debug] {}\n".format(pformat(function))
    debug_string += "[debug] Passed args: {}\n".format(pformat(args))
    debug_string += "[debug] Passed kwargs: {}\n".format(pformat(kwargs))
    return debug_string + "[debug] Locals: {}".format(pformat(function.__code__.co_varnames))


It may not look as visually stimulating, but, it's not as redundant.


  Is it a good idea to create an empty string, and then add to, and re-assign it to build an output string?


If you were directly printing these then it would be a bad idea, but in this case, not really. However, I suppose you could move them to an object, or an array and return the result of a join function.

You could even return it as an array, and print each [debug] result. Which would remove the need for the \ns at the end, and DRY up the [debug] at the beginning of the strings (put it in the loop, not altogether)



You've got a few too long lines, by PEP8 standard:


debug -- Whether or not you want to output debug info. Generally, a global DEBUG variable is passed in here.
debug_string += "[debug] Locals: {}".format(pformat(function.__code__.co_varnames))
print(self.__format_debug_string(function, *args, **kwargs))
print("[debug] Parent attributes: {}".format(pformat(args[0].__dict__)))





As for your documentation:


function, it will print out the arguments of

Keyword arguments:



I'm a bit confused by that, grammatically.

__call__ is a more complex function (in my mind, at least) than __format_debug_string, but it has no documentation.




  Is this Python 3, and Python 2.7 compatible?


It ran fine when I tested it in Python 2.7.9 and 3.1.1