1

In a utility function displaying both the argument and values of a calling function, I need to know the original name of an possibly aliased function imported from another module. Is this possibly for the simple case when aliasing on import?

Here is a simplified use case, where I first present some code from the utilities.py module:

import inspect

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))
    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 here is some code accessing these functions first by original name, and then by the aliases:

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
    debug(a, b)

if __name__ == '__main__':
    main()

The output from this is:

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

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

As can be seen I've found the correct code context, and I found the name of the calling function, but alas the first reports the alias name and the latter reports the actual name. So my question is whether it is possible to reverse the operation so that I can know that format_args has been aliased to fargs, and debug_print has been aliased to debug?

Some related issues, which do not address this reversal of aliasing:

Community
  • 1
  • 1
holroy
  • 3,047
  • 25
  • 41
  • 3
    Short answer: no, there is not, not without extensive AST parsing and analysis of the sourcecode for the calling frame so you can guess what name was used to produce the call. – Martijn Pieters Nov 04 '15 at 22:31
  • 1
    @MartijnPieters, AST? Is that Abstract Syntax Tree? – holroy Nov 04 '15 at 22:32
  • 2
    Yes, you'd have to load the source code, then analyse how the call was made and what name the callable object had. Note that you can create other references that not necessarily have a name; `callables = [fargs, debug], then `callables[0]()` uses references to function objects in a list. – Martijn Pieters Nov 04 '15 at 22:35
  • @MartijnPieters, As stated in start I'm aiming for the simple aliasing, so if someone breaks this functionality by using something like your `callables` then they are on their own! :-) – holroy Nov 04 '15 at 22:37
  • @MartijnPieters, As I have a partial code context, can I try words from this and check if they evaluate to one of my two methods? (Using a lookup in some dict... ) – holroy Nov 04 '15 at 22:37
  • You could try, but it'll be fragile. Calls can span multiple physical lines, for example, I don't think your code context takes that into account. – Martijn Pieters Nov 04 '15 at 22:39
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/94253/discussion-between-holroy-and-martijn-pieters). – holroy Nov 04 '15 at 22:40

1 Answers1

0

It turns out that finding out which alias has been defined for debug_print or format_args is rather hard, but fortunately I do have the code context and could do the reverse operation of locating which part of my code context is actually one my functions.

The following train of thoughts leading to this solution was partly inspired by the comments made by Martijn Pieters related to abstract syntax trees, and partly by a hint given by SuperBiasedMan related to doing a help(fargs):

  • help(fargs) actually listed the format_args function
  • Within IPython, using help??, I found a hint that it uses pydoc.help
  • A source code of pydoc.py was found here
  • Found the call sequence: help > doc > render_doc > resolve > name = getattr(thing, '__name__', None)
  • Tried getattr(fargs, '__name__', None) in my test code, and it worked
  • Tried getattr('fargs', ...), and it failed
  • After some searching found that globals()['fargs'] did return the function object
  • Extracted tokens from my code_context, and wrote some code to do the various lookups

All this resulted in the following working code:

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))

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

    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?!')

    return ', '.join(str(arg) for arg in args)

I do know that this depends upon the calling function being present in the code context, and if you break the code over several lines it would break this method. Another caveat is if someone calls the function via a list or dict. However as this is mainly for debugging purposes, and it can be documented that they shouldn't do stuff like that.

The output is now:

== 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)

This output does now enable to proceed on my quest for making a nice debug_print(). Please comment (or answer), if you see improvements or flaws in this design.

Community
  • 1
  • 1
holroy
  • 3,047
  • 25
  • 41