Repository: ipython/ipython

Issue:
Running debugger multiple times causes AttributeError: 'NoneType' object has no attribute 'f_globals'
Running the %run magic more than once causes an AttributeError: 'NoneType' object has no attribute 'f_globals'.  I have not encountered this problem before, and it is present on multiple files.  I created a new file to just print one line and the problem persists.

```
~/Workspace/puzzles/FiveThirtyEightRiddler $ ipython
Python 3.4.2 (default, Jun 20 2016, 14:25:19) 
Type "copyright", "credits" or "license" for more information.

IPython 5.1.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: %run -d test.py
Breakpoint 1 at /home/andrew/Workspace/puzzles/FiveThirtyEightRiddler/test.py:1
NOTE: Enter 'c' at the ipdb>  prompt to continue execution.
> /home/andrew/Workspace/puzzles/FiveThirtyEightRiddler/test.py(1)<module>()
1---> 1 print('test')

ipdb> c
test

In [2]: %run -d test.py
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-2-63fffe36c4af> in <module>()
----> 1 get_ipython().magic('run -d test.py')

/usr/lib/python3.4/site-packages/IPython/core/interactiveshell.py in magic(self, arg_s)
   2156         magic_name, _, magic_arg_s = arg_s.partition(' ')
   2157         magic_name = magic_name.lstrip(prefilter.ESC_MAGIC)
-> 2158         return self.run_line_magic(magic_name, magic_arg_s)
   2159 
   2160     #-------------------------------------------------------------------------

/usr/lib/python3.4/site-packages/IPython/core/interactiveshell.py in run_line_magic(self, magic_name, line)
   2077                 kwargs['local_ns'] = sys._getframe(stack_depth).f_locals
   2078             with self.builtin_trap:
-> 2079                 result = fn(*args,**kwargs)
   2080             return result
   2081 

<decorator-gen-57> in run(self, parameter_s, runner, file_finder)

/usr/lib/python3.4/site-packages/IPython/core/magic.py in <lambda>(f, *a, **k)
    186     # but it's overkill for just that one bit of state.
    187     def magic_deco(arg):
--> 188         call = lambda f, *a, **k: f(*a, **k)
    189 
    190         if callable(arg):

/usr/lib/python3.4/site-packages/IPython/core/magics/execution.py in run(self, parameter_s, runner, file_finder)
    713                         opts.get('b', ['1'])[0], filename)
    714                     self._run_with_debugger(
--> 715                         code, code_ns, filename, bp_line, bp_file)
    716                 else:
    717                     if 'm' in opts:

/usr/lib/python3.4/site-packages/IPython/core/magics/execution.py in _run_with_debugger(self, code, code_ns, filename, bp_line, bp_file)
    816             maxtries = 10
    817             bp_file = bp_file or filename
--> 818             checkline = deb.checkline(bp_file, bp_line)
    819             if not checkline:
    820                 for bp in range(bp_line + 1, bp_line + maxtries + 1):

/usr/lib64/python3.4/pdb.py in checkline(self, filename, lineno)
    743         # this method should be callable before starting debugging, so default
    744         # to "no globals" if there is no current frame
--> 745         globs = self.curframe.f_globals if hasattr(self, 'curframe') else None
    746         line = linecache.getline(filename, lineno, globs)
    747         if not line:

AttributeError: 'NoneType' object has no attribute 'f_globals'

In [3]: 

```

test.py:

```
print('test')
```

```
~/Workspace/puzzles/FiveThirtyEightRiddler $ python3 -c "import IPython; print(IPython.sys_info())"
{'commit_hash': '5c9c918',
 'commit_source': 'installation',
 'default_encoding': 'UTF-8',
 'ipython_path': '/usr/lib/python3.4/site-packages/IPython',
 'ipython_version': '5.1.0',
 'os_name': 'posix',
 'platform': 'Linux-4.0.7-300.fc22.x86_64-x86_64-with-fedora-22-Twenty_Two',
 'sys_executable': '/usr/bin/python3',
 'sys_platform': 'linux',
 'sys_version': '3.4.2 (default, Jun 20 2016, 14:25:19) \n'
                '[GCC 5.3.1 20160406 (Red Hat 5.3.1-6)]'}
```



Discussion:
Reproduced, debugging now.

I think this is possibly a bug in pdb itself: that line checks for `hasattr(self, 'curframe')`, but it doesn't handle `self.curframe == None`, which happens if you call `debugger.reset()`. I'll work around this in IPython and then file a bug on Python.


Resolving patch:
diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py
--- a/IPython/core/magics/execution.py
+++ b/IPython/core/magics/execution.py
@@ -816,6 +816,11 @@ def _run_with_debugger(self, code, code_ns, filename=None,
             self.shell.InteractiveTB.pdb = self.shell.InteractiveTB.debugger_cls()
             deb = self.shell.InteractiveTB.pdb
 
+        # deb.checkline() fails if deb.curframe exists but is None; it can
+        # handle it not existing. https://github.com/ipython/ipython/issues/10028
+        if hasattr(deb, 'curframe'):
+            del deb.curframe
+
         # reset Breakpoint state, which is moronically kept
         # in a class
         bdb.Breakpoint.next = 1


Test patch:
diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py
--- a/IPython/core/tests/test_run.py
+++ b/IPython/core/tests/test_run.py
@@ -207,9 +207,16 @@ def test_builtins_type(self):
     def test_run_profile( self ):
         """Test that the option -p, which invokes the profiler, do not
         crash by invoking execfile"""
-        get_ipython()
         self.run_tmpfile_p()
 
+    def test_run_debug_twice(self):
+        # https://github.com/ipython/ipython/issues/10028
+        _ip = get_ipython()
+        with tt.fake_input(['c']):
+            _ip.magic('run -d %s' % self.fname)
+        with tt.fake_input(['c']):
+            _ip.magic('run -d %s' % self.fname)
+
 
 class TestMagicRunSimple(tt.TempFileMixin):
 
diff --git a/IPython/testing/tools.py b/IPython/testing/tools.py
--- a/IPython/testing/tools.py
+++ b/IPython/testing/tools.py
@@ -6,16 +6,8 @@
 """
 
 
-#-----------------------------------------------------------------------------
-#  Copyright (C) 2009 The IPython Development Team
-#
-#  Distributed under the terms of the BSD License.  The full license is in
-#  the file COPYING, distributed as part of this software.
-#-----------------------------------------------------------------------------
-
-#-----------------------------------------------------------------------------
-# Imports
-#-----------------------------------------------------------------------------
+# Copyright (c) IPython Development Team.
+# Distributed under the terms of the Modified BSD License.
 
 import os
 import re
@@ -25,6 +17,7 @@
 from contextlib import contextmanager
 from io import StringIO
 from subprocess import Popen, PIPE
+from unittest.mock import patch
 
 try:
     # These tools are used by parts of the runtime, so we make the nose
@@ -45,9 +38,6 @@
 from . import decorators as dec
 from . import skipdoctest
 
-#-----------------------------------------------------------------------------
-# Functions and classes
-#-----------------------------------------------------------------------------
 
 # The docstring for full_path doctests differently on win32 (different path
 # separator) so just skip the doctest there.  The example remains informative.
@@ -443,6 +433,25 @@ def make_tempfile(name):
     finally:
         os.unlink(name)
 
+def fake_input(inputs):
+    """Temporarily replace the input() function to return the given values
+
+    Use as a context manager:
+
+    with fake_input(['result1', 'result2']):
+        ...
+
+    Values are returned in order. If input() is called again after the last value
+    was used, EOFError is raised.
+    """
+    it = iter(inputs)
+    def mock_input(prompt=''):
+        try:
+            return next(it)
+        except StopIteration:
+            raise EOFError('No more inputs given')
+
+    return patch('builtins.input', mock_input)
 
 def help_output_test(subcommand=''):
     """test that `ipython [subcommand] -h` works"""