Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exec Python code with nonlocals

Say I have three dictionaries a, b and c. I want to exec() a code snippet where a is the globals, b is the nonlocals and c is the locals. That is no problem for the globals and locals, as I just have to use exec(code, a, c) -- but what about b? How can I make the values in b visible to the code snippet as nonlocal variables?

I think this clarifies the concept:

assert globals() == a and locals() == a
def foo():
  assert globals() == a and locals() == b
  def bar():
    assert globals() == a and locals() == c
    exec(code)
like image 607
Niklas R Avatar asked Nov 08 '22 11:11

Niklas R


1 Answers

While it is not exactly executing Python code with nonlocals, I was able to rewrite the AST so that it behaves the way I want. Every variable that is being accessed and that is not locally defined in a function or class scope is rewritten to access a mapping instead.

This is an example that demonstrates a possible use case where a is the dictionary that takes precedence over b and will receive all variable assignments, but the values of b are still taken into account if not shadowed by a.

from nr.datastructures.chaindict import ChainDict
from nr.ast.dynamic_eval import dynamic_exec

d1 = {'a': 42}
d2 = {'b': 'spam'}

code ='''
print(a, b) # prints 42 spam
a = 'egg'
b = 'ham'
'''

dynamic_exec(code, ChainDict(d1, d2))
assert d1 == {'a': 'egg', 'b': 'ham'}, d1
assert d2 == {'b': 'spam'}, d2

Available with nr v2.0.4 (disclaimer: I am the developer of that package).


A little bit more detail

The dynamic_exec() function will parse the Python code using ast.parse() and then apply an ast.NodeTransformer which rewrites global variable names. Example:

import os
from os import path

parent_dir = path.dirname(__file__)

def main():
  filename = path.join(parent_dir, 'foo.py')
  print(filename)

This will be turned into an AST that is semantically equivalent to

import os; __dict__['os'] = os
from os import path; __dict__['path'] = path

__dict__['parent_dir'] = __dict__['path'].dirname(__dict__['__file__'])

def main():
  filename = __dict__['path'].join(__dict__['parent_dir'], 'foo.py')
  __dict__['print'](filename)
like image 76
Niklas R Avatar answered Nov 14 '22 22:11

Niklas R