2
\$\begingroup\$

I'm on a quest for making a good debug_print() method for printing out debug information in some of my private projects. When testing I aliased my debug_print() imported from a module to debug() print, and some of the intrinsic of my code stopped working.

This led to a question on Stack Overflow, which with a little nudge in the right direction from SuperBiasedMan, led to the following code which I would like for you to review. First the code from utilities.py:

import inspect
import re

DEBUG_FLAG = True

def _log_args(*args):
    """Uses reflection to returning passing argument code with values."""

    prev_frame = inspect.currentframe().f_back
    func_name = prev_frame.f_code.co_name
    code_context = inspect.getframeinfo(prev_frame.f_back).code_context[0].strip()

    # Do some magic, which does work _unless_ func_name is aliased :-)
    print('code context     : {}'.format(code_context))
    print('func_name        : {}'.format(func_name))

    ## Main focus area for review starts here

    # Get globals from the calling frame
    globals_copy = prev_frame.f_back.f_globals

    # Extract legal tokens from code_context
    tokens = re.compile('[_a-zA-Z][a-zA-Z_0-9]*').findall(code_context)

    for token in tokens:
#        print( '  Checking token : {}'.format(token))

        # Check if token is found as an object in globals()        
        code_object = globals_copy.get(token, None)
        if not code_object:
            continue

        # Check if code_object is one of my userdefined functions
        if inspect.isfunction(code_object):
            code_func_name = getattr(code_object, '__name__', None)
        else:
            continue

        # Check if expanded token is actually an alias (or equal) to func_name
        if code_func_name == func_name:
            func_name = token
            break
    else:
        # For-loop went through all tokens, and didn't find anything
        func_name = None

    if func_name:
        print('Calling function : {}'.format(func_name))
    else:
        print('Didn\'t find a calling function?!')

    ## Main focus area for review ends here
    return ', '.join(str(arg) for arg in args)


def format_args(*args):
    """Returns string with name of arguments with values."""
    return _log_args(args)

def debug_print(*args):
    """Prints name of arguments with values."""
    if DEBUG_FLAG:
        print _log_args(args)

And then some test code from another file:

from utilities import debug_print, format_args, debug_print as debug, format_args as fargs

def main():

    a, b = "text", (12, 13)

    print "== Unaliased =="
    test_text = format_args(a, b)
    print test_text   # Returns 
    debug_print(a, b)

    print "\n== Aliased =="
    test_text = fargs(a, b)
    print test_text   # Returns 
    debug(a, b)


if __name__ == '__main__':
    main()

The current output reflects that this is a work in progress, and the main focus for this review is commented upon in the code.

== Unaliased ==
code context     : test_text = format_args(a, b)
func_name        : format_args
Calling function : format_args
('text', (12, 13))
code context     : debug_print(a, b)
func_name        : debug_print
Calling function : debug_print
('text', (12, 13))

== Aliased ==
code context     : test_text = fargs(a, b)
func_name        : format_args
Calling function : fargs
('text', (12, 13))
code context     : debug(a, b)
func_name        : debug_print
Calling function : debug
('text', (12, 13)

Addendum: Clarifying context

Note that this addendum to my question is just to help understand the context in which these functions are to be called. The context is that I would want debug output during developing and initial testing of my projects, and I want to lazily do stuff like:

from random import randint
from operator import add, sub

first, second = randint(1, 20), randint(1, 20)
action = add if first > 10 else sub
debug_print(first, action, second)
print('Result is: {}'.format(action(first, second)))

Which could/should result in the output:

first: 6, action: <built-in function sub>, second: 8
Result is: -2

The format_args() version is for use in logger.debug(format_args(...)). So "name of argument with value" with an argument of a will mean "a: 6" if it had the value of 6.

\$\endgroup\$
2
\$\begingroup\$

Readability

My main issue here is readability. Your initial goal was to see a function's 'real' unaliased name. Here's the three functions your utilities has:

def _log_args(*args):
    """Uses reflection to returning passing argument code with values."""
def debug_print(*args):
    """Prints name of arguments with values."""
def format_args(*args):
    """Returns string with name of arguments with values."""

I don't know what these mean. name of arguments with values? Do you mean the names of the arguments passed? The names of the function's required arguments? I don't see what that has to do with finding the original name of a function, nor do I understand what else it might be doing.

As for _log_args, I think there's a typo because I don't know what returning passing argument means. I don't know what any of that sentence means really. So I'll look at the actual return value... and you're just returning the arguments? That's unnecessary, when you could just run _log_args and then return args from format_args. No need to pass them in to just return out again. In fact, I'm not even sure why they are being returned/printed at all, is that part of how the function works? It doesn't use them but are they passed for the sake of your introspection?

I know that what you mainly wanted reviewed is after these points but these points make following what comes next harder.

_log_args

You have a potential bug. inspect.currentframe() can return None. From the docs

CPython implementation detail: This function relies on Python stack frame support in the interpreter, which isn’t guaranteed to exist in all implementations of Python. If running in an implementation without Python stack frame support this function returns None.

You should try except AttributeError to get around this:

try:
    prev_frame = inspect.currentframe().f_back
except AttributeError:
    raise Exception("Interpreter appears to lack stack frame support")

There may be a more suitable error and message, but you get the idea. func_name is clear about what it means but code_context less so. You could use a more meaningful name, or if you can't then you should comment it.

# String showing how code was called. eg. func(a, b)

globals_copy is a misleading name when it's just a reference. prev_globals or frame_globals would be clearer.

Your re-compiling the same regex every time the function's called. Just compile it as a constant above and then call PATTERN.findall(code_context).

Unlike getattr, calling get on a dictionary like globals_copy defaults to None even if it's not specified so you can leave that out.

Lastly, instead of escaping an apostrophe with a backslash, wrap the string in double quotes. It's easier to read and that's more important than consistent quote use.

\$\endgroup\$
  • \$\begingroup\$ Hopefully the naming will become clearer in the next installment of my quest! :-) The functions are related to printing the arguments passed alongside with the name, so that debug_print(a, b) would actually print "a= 'text', b = (10, 12)". \$\endgroup\$ – holroy Nov 6 '15 at 18:00
  • \$\begingroup\$ The potential bug is known, and handled in another version, but still a good catch on most of your points. The reason for format_args being as stupid as it is, is that I need to have a consistent number of levels for _log_args to inspect/reflect from. \$\endgroup\$ – holroy Nov 6 '15 at 18:02
  • 1
    \$\begingroup\$ Wrote a little addendum to my post, to give a little bit of context of the names and comments. But your remarks are spot on when just seeing the methods as you are, so I do need to extend my comments/docstrings to clarify what these methods actually do! \$\endgroup\$ – holroy Nov 6 '15 at 18:49
  • \$\begingroup\$ @holroy This makes it a lot clearer. I was thinking it was something like this but decided to be harsh and focus on only the information in the script to highlight my points. :) \$\endgroup\$ – SuperBiasedMan Nov 9 '15 at 9:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.