You can use inspect.getclosurevars:
Get the mapping of external name references in a Python function or
method func to their current values. A named tuple
ClosureVars(nonlocals, globals, builtins, unbound) is returned.
nonlocals maps referenced names to lexical closure variables, globals
to the function’s module globals and builtins to the builtins visible
from the function body. unbound is the set of names referenced in the
function that could not be resolved at all given the current module
globals and builtins.
Let's see what it returns for your examples:
Case 1:
def foo1(x):
return y + x # yes - it has referenced 'y' which is out of foo1 scope
inspect.getclosurevars(foo1)
# ClosureVars(nonlocals={}, globals={'y': 1}, builtins={}, unbound=set())
Here it detected you are using variable in the global scope.
Case 2:
import colorsys
def foo2(x):
return colorsys.rgb_to_hsv(*x) # yes - it has referenced 'colorsys' which is out of foo2 scope
inspect.getclosurevars(foo2)
# ClosureVars(nonlocals={}, globals={'colorsys': <module 'colorsys' from '...\\lib\\colorsys.py'>}, builtins={}, unbound={'rgb_to_hsv'})
Here it detected you are using a module's function.
Case 3:
def foo3(x):
return x*x # no - it has only referenced 'x' which is an input to foo3, so the code executed within the scope of foo3
inspect.getclosurevars(foo3)
# ClosureVars(nonlocals={}, globals={}, builtins={}, unbound=set())
Here we see "nothing unusual".
Therefore, we can wrap this procedure in a function which looks if any of the fields (except for built-ins; i.e. foo can use abs and no problem) is non-empty:
import inspect
def references_sth_out_of_scope(fun):
closure_vars = inspect.getclosurevars(fun)
referenced = any(getattr(closure_vars, field) for field in closure_vars._fields if field != "builtins")
return referenced
Usage:
>>> references_sth_out_of_scope(foo1)
True
>>> references_sth_out_of_scope(foo2)
True
>>> references_sth_out_of_scope(foo3)
False
>>> references_sth_out_of_scope(references_sth_out_of_scope) # :)
True