Repository: pallets/werkzeug

Issue:
NameError when using lambda function in a list comprehension in the interactive debugger under Python 3
I’ve encountered a weird edge case around lambda functions in list comprehensions in the interactive debugger. I set up a function in the debugger as follows:

```pycon
>>> f = lambda x: x
>>> f
<function <lambda> at 0x1068752a8>
>>> f(1)
1
>>> [f(x) for x in (1, 2, 3)]
```

If I run this code in Python 2.7.10, the output is exactly what you’d expected:

```pycon
>>> [f(x) for x in (1, 2, 3)]
[1, 2, 3]
```

But if I run the same line in Python 3.5.0, I get a NameError:

```pycon
>>> [f(x) for x in (1, 2, 3)]
Traceback (most recent call last):
  File "<debugger>", line 1, in <module>
    [f(x) for x in (1, 2, 3)]
 File "<debugger>", line 1, in <listcomp>
    [f(x) for x in (1, 2, 3)]
NameError: name 'f' is not defined
```

I’m running Werkzeug 0.11.11 on macOS 10.11.6. Each time I’m using a clean virtualenv, and using a minimal Flask app that drops straight into the interactive debugger.

I can reproduce the issue if I install from master at f34b02b.

I have a few other issues with defining functions in Python 3 in the interactive debugger, which might be related – I’ll raise as a separate issue.


Discussion:
I tried this usecase for python 2.7 and python 3.4 and reproduced the issue.

This may be a bug in [`InteractiveInterpreter`](https://docs.python.org/3/library/code.html#code.InteractiveInterpreter) from the stdlib. Could you try with that? (I think you're supposed to instantiate that and call `runsource`)

I tried [InteractiveConsole](https://docs.python.org/3/library/code.html#code.InteractiveConsole) .
It has a correct behavior.

``` python
import code
c = code.InteractiveConsole()
c.push('f=lambda x: x')
c.push('[f(x) for x in (1, 2, 3)]')
```

Also the problem is wider. The problem appears also when use external variables in func. Like there:

``` python
>>> f = 1
>>> g = lambda x: f
>>> g(3)
```

Like @alexpantyukhin, I can’t seem to reproduce this problem with `InteractiveInterpreter`:

``` pycon
>>> import code
>>> c = code.InteractiveInterpreter()
>>> c.runsource('f = lambda x: x')
False
>>> c.runsource('[f(x) for x in (1, 2, 3)]')
[1, 2, 3]
False
```

I’ve had a stab at reproducing it with the Werkzeug classes, but I’m probably not calling them properly – no repro there either.

``` pycon
>>> from werkzeug.debug.console import _local, _InteractiveConsole
>>> c = _InteractiveConsole({}, {})
>>> _local._current_ipy = c
>>> c.runsource('f = lambda x: x')
>>> c.runsource('[f(1)]')
>>> c.runsource('[f(x) for x in (1, 2, 3)]')
```

I've made a PR. It seems it fixes the problem. Can you help me to check it?

Any news here?

```
x = lambda x: x + 1
[x(i) for i in range(10)]
```

still an issue.. Using `Werkzeug==0.14.1`
@alexpantyukhin where is your pr? Can't find it? Did you refeer this issue?
It's https://github.com/pallets/werkzeug/pull/1038 but I don't think it was a complete solution IIRC. I'd be happy to merge something that solves this issue but doesn't introduce new ones (see code comments on that PR, no idea why they're collapsed)

Resolving patch:
diff --git a/src/werkzeug/debug/console.py b/src/werkzeug/debug/console.py
--- a/src/werkzeug/debug/console.py
+++ b/src/werkzeug/debug/console.py
@@ -148,11 +148,12 @@ def func(source, filename, symbol):
 
 class _InteractiveConsole(code.InteractiveInterpreter):
     def __init__(self, globals, locals):
+        locals = dict(globals)
+        locals.update(locals)
+        locals["dump"] = dump
+        locals["help"] = helper
+        locals["__loader__"] = self.loader = _ConsoleLoader()
         code.InteractiveInterpreter.__init__(self, locals)
-        self.globals = dict(globals)
-        self.globals["dump"] = dump
-        self.globals["help"] = helper
-        self.globals["__loader__"] = self.loader = _ConsoleLoader()
         self.more = False
         self.buffer = []
         _wrap_compiler(self)
@@ -177,7 +178,7 @@ def runsource(self, source):
 
     def runcode(self, code):
         try:
-            eval(code, self.globals, self.locals)
+            exec(code, self.locals)
         except Exception:
             self.showtraceback()
 
diff --git a/src/werkzeug/debug/tbtools.py b/src/werkzeug/debug/tbtools.py
--- a/src/werkzeug/debug/tbtools.py
+++ b/src/werkzeug/debug/tbtools.py
@@ -464,8 +464,8 @@ def __init__(self, exc_type, exc_value, tb):
         if os.path.isfile(fn):
             fn = os.path.realpath(fn)
         self.filename = to_unicode(fn, get_filesystem_encoding())
-        self.module = self.globals.get("__name__")
-        self.loader = self.globals.get("__loader__")
+        self.module = self.globals.get("__name__", self.locals.get("__name__"))
+        self.loader = self.globals.get("__loader__", self.locals.get("__loader__"))
         self.code = tb.tb_frame.f_code
 
         # support for paste's traceback extensions


Test patch:
diff --git a/tests/test_debug.py b/tests/test_debug.py
--- a/tests/test_debug.py
+++ b/tests/test_debug.py
@@ -16,6 +16,7 @@
 import requests
 
 from werkzeug._compat import PY2
+from werkzeug.debug import console
 from werkzeug.debug import DebuggedApplication
 from werkzeug.debug import get_machine_id
 from werkzeug.debug.console import HTMLStringO
@@ -356,6 +357,17 @@ def app(environ, start_response):
         assert r.text == "hello"
 
 
+def test_console_closure_variables(monkeypatch):
+    # restore the original display hook
+    monkeypatch.setattr(sys, "displayhook", console._displayhook)
+    c = console.Console()
+    c.eval("y = 5")
+    c.eval("x = lambda: y")
+    ret = c.eval("x()")
+    expected = ">>> x()\n5" if PY2 else ">>> x()\n5\n"
+    assert ret == expected
+
+
 @pytest.mark.skipif(PY2, reason="Python 2 doesn't have chained exceptions.")
 @pytest.mark.timeout(2)
 def test_chained_exception_cycle():