ipython: IPythonShellEmbed fails to recognize local variables

Issue

Embedded IPython shells can lose track of local variables.

Test Case

Minimal test case:

class Foo(object):
    """ Container-like object """
    def __setattr__(self, obj, val):
        self.__dict__[obj] = val

    def __getattr__(self, obj, val):
        return self.__dict__[obj]

f = Foo()
f.indices = set([1,2,3,4,5])
f.values = {}
for x in f.indices:
    f.values[x] = x

def bar(foo):
    import IPython
    IPython.Shell.IPShellEmbed()()
    return sum(foo.values[x] for x in foo.indices)

print bar(f)

To see the error, first run the code in Python (or IPython) and exit from the spawned shell; the final print statement correctly displays ‘15’. Run the code again, but this time type sum(foo.values[x] for x in foo.indices) in the spawned shell, and we receive the error

" NameError: global name ‘foo’ is not defined".

About this issue

  • Original URL
  • State: open
  • Created 14 years ago
  • Reactions: 3
  • Comments: 19 (11 by maintainers)

Most upvoted comments

I looked into this issue a bit, and it’s definitely fixable (though maintaining fixes for both Python 2 and 3 may be messy).

The ChainMap solution would be easiest to include into IPython proper. However, there’s a slight catch that eval/exec require globals to be a dict. Creating a class MyChainMap(ChainMap, dict): pass can work around this.

I also wrote a Python 3.5+ fix based on a different strategy of simulating closure cells and forcing the python compiler to emit the correct bytecode to work with them. The relevant file is here, part of my xdbg demo. It works by replacing get_ipython().run_ast_nodes.

As far as I can tell, the two approaches differ only in their handling of closures. When xdbg is embedded at a scope that has closed over some variables, it can correctly access those variables by reference and mutate them. Additionally, if any functions are created in the interactive interpreter, they will close over any local variables they need while allowing the rest of the local scope to be garbage collected.

As a work around, you can use globals().update(locals()) to carry embedding sessions’ local into process globals.

Source: https://stackoverflow.com/a/67517617/695964

Based on https://bugs.python.org/issue13557 and the fact that explicitly passing locals() and globals() to an exec fixes @takluyver’s reproduction of the bug, it seems like this ought to be possible to fix in IPython by passing the correct local and global namespaces.

x = 1

def func():    
    x = 2
    exec("def g():\n print(x)\ng()")  # prints 1 :(
    exec("def g():\n print(x)\ng()", locals(), globals())  # prints 2 yay!

    
if __name__ == "__main__":
    func()