Assuming you want to do this having the function at hand (even in compiled pyc
form) and not through string search operations (which I guess you would have already thought about), then you can use the dis
module.
A call of the form y = np.random.choice(x)
will be compiled into something like this (output of dis.dis()
):
8 0 LOAD_GLOBAL 0 (np)
2 LOAD_ATTR 1 (random)
4 LOAD_METHOD 2 (choice)
6 LOAD_FAST 1 (x)
8 CALL_METHOD 1
10 STORE_FAST 2 (y)
The order of these instructions and their arguments should always be the same assuming that your students are using the global import numpy as np
. The third LOAD_METHOD
could become LOAD_ATTR
depending on how the method is being loaded.
The actual call is more difficult to detect, it could become either CALL_METHOD
, CALL_FUNCTION_EX
, CALL_FUNCTION_KW
or CALL_FUNCTION
depending on how it's done. It's also not so straightforward to check that the function being called is actually the one you want like in the above case where it's obvious. Checking that the actual call is made is still possible, but requires keeping track of the Python stack and doing real parsing of the instructions and their arguments, you can check the documentation if you wish to dive into that.
I'll limit myself to just checking if np.random.choice
is actually loaded in the checked function. You can do so with the following code:
import dis
def zip3(g):
try:
a, b, c = next(g), next(g), next(g)
while 1:
yield a, b, c
a, b, c = b, c, next(g)
except StopIteration:
pass
def check(func):
for a, b, c in zip3(dis.get_instructions(func)):
if a.opname == 'LOAD_GLOBAL' and a.argval == 'np':
if b.opname == 'LOAD_ATTR' and b.argval == 'random':
if c.opname in ('LOAD_ATTR', 'LOAD_METHOD') and c.argval == 'choice':
return True
return False
check(checkme) # -> True
NOTE: opcodes could change depending on Python version, but I am assuming you will run all the tests under the same Python version, so you can adjust the matches to fit your needs (use dis.dis()
to check). Of course with this method you will not be able to catch more convoluted stuff like a = np; b = a.random; b.choice(x)
or import numpy as whatever
, but that's also true for string matching anyway.