2

How can I determine programmatically if one function calls another function? I cannot modify either function.

Here's what I want (source_calls_target):

>>> def check():
>>>     pass
>>> def test_check():
>>>     check()
>>> def source_calls_target(source, target):
>>>     # if source() calls target() somewhere, return True
>>>     ???
>>> source_calls_target(test_check, check)
True
>>> source_calls_target(check, test_check)
False

Ideally, I do not want to actually call target().

Ideally, I want to check if a call to target() appears within the definition for source. It may or may not actually call it depending on conditional statements.

9
  • 4
    How is this relevant to unit testing? Unit testing is about determining behaviour in compliance with a specification. Commented Aug 17, 2012 at 21:30
  • @Marcin This checks compliance of test_check with my specification that it must call check(). Commented Aug 17, 2012 at 21:34
  • 6
    @devtk, this is at best code coverage analysis, not unit testing. Unit testing doesn't care if test_check() calls check() or not. It only deals with the return value and side effects of test_check() being conform to the (current) specification. Commented Aug 17, 2012 at 21:37
  • 2
    Are you able to call the functions during source_calls_target, or are you limited to analysing the source code? Commented Aug 17, 2012 at 21:44
  • 4
    Are you perhaps interested in knowing whether source could call target? (Are you just asking whether target is present somewhere in the code for source?) Or do you really want to know if it will call target? Commented Aug 17, 2012 at 22:10

5 Answers 5

6

If you can guarantee having access to the source code, you can use ast.parse:

import ast
call_names = [c.func.id for c in ast.walk(ast.parse(inspect.getsource(source)))
              if isinstance(c, ast.Call)]
return 'target' in call_names

Note that calls are always by name, so it's difficult (and potentially impossible) to tell whether a call is to a particular function or another of the same name.

In the absence of source code, the only way is via disassembly:

import dis
def ops(code):
    i, n = 0, len(code)
    while i < n:
        op = ord(code[i])
        i += 1
        if op == dis.EXTENDED_ARG:
            ext = ord(code[i]) + ord(code[i+1])*256
            op = ord(code[i + 2])
            i += 3
        else:
            ext = 0
        if op >= dis.HAVE_ARGUMENT:
            arg = ord(code[i]) + ord(code[i+1])*256 + ext*65536
            i += 2
            yield op, arg
        else:
            yield op, None

source_ops = list(ops(source.func_code.co_code))

The problem is that it's in practice impossible to tell whether a function is calling another function or just loading a reference to it; if the other function is passed to map or reduce etc. then it will be called but passed to another function it might not be. Practically the sensible thing is to assume that if the function is in source.func_code.co_names then it might be called:

'target' in source.func_code.co_names
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks! This is the answer I had in mind, just wasn't sure how to implement it. Will give it a shot.
This bombs out if the function is indented. For example, if it's within a class. :/
@devtk you could use textwrap.dedent to fix that issue.
Actually, just inspect.getsource(source).lstrip() should do.
3

Here's a simple example using sys.settrace(). It does require that the source function be called to work. It is also not guaranteed to work, since in some rare instances, two different functions may share the same code object.

import sys

def check():
    pass

def test_check():
    check()

def source_calls_target(source, target):
    orig_trace = sys.gettrace()
    try:
        tracer = Tracer(target)
        sys.settrace(tracer.trace)
        source()
        return tracer.called
    finally:
        sys.settrace(orig_trace)

class Tracer:
    def __init__(self, target):
        self.target = target
        self.called = False

    def trace(self, frame, event, arg):
        if frame.f_code == self.target.func_code:
            self.called = True

print source_calls_target(test_check, check)
print source_calls_target(check, test_check)

3 Comments

Does this only tell you if the target was called at some point, but not directly by source?
Correct. If you want to know whether the target was called directly by source, then you could also check that frame.f_back.f_code == self.source.func_code (and you would have to pass source to the Tracer instance).
Very inventive. Thanks :) We'll see if anyone can do it without actually calling the target.
1

Probably very ugly way but it works:

import profile

def source_calls_target(source, target):
    pro = profile.Profile()
    pro.run(source.__name__ + '()')
    pro.create_stats()
    return target.__name__ in [ele[2] for ele in pro.stats]

2 Comments

What happens if you have multiple functions with the same name?
It fails - but as I said, this is ugly solution.
1

I had been coming up with something that ended up looking very similar to @Luke's good answer. Mine is just a simple function, but it checks if the immediate caller of the target function was the source function:

import sys
from functools import partial

def trace_calls(call_list, frame, event, arg):
    if event != 'call':
        return
    call_list.append(frame.f_code)

def test_call(caller, called):
    traces = []
    old = sys.gettrace()
    sys.settrace(partial(trace_calls, traces))
    try:
        caller()
    finally:
        sys.settrace(old)

    try:
        idx = traces.index(called.func_code)
    except ValueError:
        return False

    if idx and traces[idx-1] == caller.func_code:
        return True

    return False

And testing...

def a():
    b()

def b():
    pass

def c():
    a()


test_call(c,a) #True
test_call(a,b) #True
test_call(c,b) #False

Comments

1

This post http://stefaanlippens.net/python_inspect will help you

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.