This was a fun exercise in metaprogramming.
Because this uses AST rewriting, you need access to the source code of any file you use @resolve
in.
This also requires you to call resolve with a variable name for the constant dictionary, although it can probably support more complex expressions with very little changes.
If you decide to change the name of resolve
(for example to resolve_constants
or something, don't forget to also change ast.Name('resolve')
in visit_FunctionDef
.
It should theoretically work in combination with other decorators, however I haven't tested that at all.
import ast
from functools import partial
import inspect
import sys
CONSTS = {'pos': 1}
class Resolver(ast.NodeTransformer):
def __init__(self, consts):
self.consts = consts
def visit_Subscript(self, node):
match node:
case ast.Subscript(ast.Name(self.consts_name), ast.Constant(value)):
return ast.Constant(self.consts[value])
return super().generic_visit(node)
def visit_FunctionDef(self, node):
# prevent infinite invocation of resolve
# and also record the name of the constants dictionary
for deco in node.decorator_list:
match deco:
case ast.Call(ast.Name('resolve'), [ast.Name(consts_name)]):
self.consts_name = consts_name
break
node.decorator_list = []
return super().generic_visit(node)
def resolve(f, *, dictionary=None):
if dictionary is None:
return partial(resolve, dictionary=f)
# Get the AST from source code
source = inspect.getsource(f)
tree = ast.parse(source)
# Transform the AST
new_tree = ast.fix_missing_locations(Resolver(dictionary).visit(tree))
# Execute the AST as part of the original namespace, so it can still access other globals
ns = sys.modules[f.__module__].__dict__
exec(compile(new_tree, f.__module__, 'exec'), ns)
# The desired function is found in the namespace
return ns[f.__name__]
@resolve(CONSTS)
def f(x: list):
return x[CONSTS['pos']]
# So you know it's not accessing the dictionary during the function call:
del CONSTS
print(f([1, 2]))