Repository: spyder-ide/spyder

Issue:
"Directly enter debugging when errors appear" is not working
### Issue Report Checklist
Yep, did it

## Problem Description
The debugger does not start when an unhandled exception is raised.

### What steps reproduce the problem?

1. tools, prefs, Run, General Settings, check "Directly enter debugging when errors appear"
2. Create new file, contents "raise RuntimeError"
3. F5

### What is the expected output? What do you see instead?




Expected (version 3.3.6):

```
In [1]: runfile('/home/user/.config/spyder-py3/temp.py', wdir='/home/user/.config/spyder-py3', post_mortem=True)
Traceback (most recent call last):

  File "<ipython-input-1-527ce6822f9c>", line 1, in <module>
    runfile('/home/user/.config/spyder-py3/temp.py', wdir='/home/user/.config/spyder-py3', post_mortem=True)

  File "/home/user/miniconda3/envs/test3/lib/python3.6/site-packages/spyder_kernels/customize/spydercustomize.py", line 827, in runfile
    execfile(filename, namespace)

  File "/home/user/miniconda3/envs/test3/lib/python3.6/site-packages/spyder_kernels/customize/spydercustomize.py", line 110, in execfile
    exec(compile(f.read(), filename, 'exec'), namespace)

  File "/home/user/.config/spyder-py3/temp.py", line 1, in <module>
    raise RuntimeError

RuntimeError

****************************************
Entering post mortem debugging...
****************************************
> /home/user/.config/spyder-py3/temp.py(1)<module>()
----> 1 raise RuntimeError


ipdb> 
```


Actual (version 4.1.1)

```
In [1]: runfile('/home/user/untitled1.py', wdir='/home/user', post_mortem=True)
Traceback (most recent call last):

  File "/home/user/untitled1.py", line 1, in <module>
    raise RuntimeError

RuntimeError


In [2]: 
```




## Versions

3.3.6 is good, 4.1.1 is broken.


Discussion:
Testing with 4.0.0b4, I suspect the problem is actually in spyder-kernels.

good: 1.4.0-py37_0
bad: 1.5.0-py37_0
02cd43403ee68c566088350cc0918b6b02afd067 is the first bad commit

"Add context manager for namespace"
I can confirm this bug with "run config per file" as well
@impact27, could take a look at this one?

Resolving patch:
diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py b/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py
--- a/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py
+++ b/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py
@@ -314,20 +314,11 @@ def _patched_preparation_data(name):
 # =============================================================================
 # Handle Post Mortem Debugging and Traceback Linkage to Spyder
 # =============================================================================
-def clear_post_mortem():
-    """
-    Remove the post mortem excepthook and replace with a standard one.
-    """
-    ipython_shell = get_ipython()
-    ipython_shell.set_custom_exc((), None)
-
-
 def post_mortem_excepthook(type, value, tb):
     """
     For post mortem exception handling, print a banner and enable post
     mortem debugging.
     """
-    clear_post_mortem()
     ipython_shell = get_ipython()
     ipython_shell.showtraceback((type, value, tb))
     p = pdb.Pdb(ipython_shell.colors)
@@ -342,34 +333,11 @@ def post_mortem_excepthook(type, value, tb):
         p.send_initial_notification = False
         p.reset()
         frame = tb.tb_frame
-        prev = frame
-        while frame.f_back:
-            prev = frame
-            frame = frame.f_back
-        frame = prev
         # wait for stdout to print
         time.sleep(0.1)
         p.interaction(frame, tb)
 
 
-def set_post_mortem():
-    """
-    Enable the post mortem debugging excepthook.
-    """
-    def ipython_post_mortem_debug(shell, etype, evalue, tb,
-                                  tb_offset=None):
-        post_mortem_excepthook(etype, evalue, tb)
-
-    ipython_shell = get_ipython()
-    ipython_shell.set_custom_exc((Exception,), ipython_post_mortem_debug)
-
-
-# Add post mortem debugging if requested and in a dedicated interpreter
-# existing interpreters use "runfile" below
-if "SPYDER_EXCEPTHOOK" in os.environ:
-    set_post_mortem()
-
-
 # ==============================================================================
 # runfile and debugfile commands
 # ==============================================================================
@@ -421,7 +389,7 @@ def transform_cell(code):
     return '\n' * number_empty_lines + code
 
 
-def exec_code(code, filename, ns_globals, ns_locals=None):
+def exec_code(code, filename, ns_globals, ns_locals=None, post_mortem=False):
     """Execute code and display any exception."""
     global SHOW_INVALID_SYNTAX_MSG
 
@@ -470,6 +438,9 @@ def exec_code(code, filename, ns_globals, ns_locals=None):
                 and ipython_shell.kernel._pdb_obj):
             # Ignore BdbQuit if we are debugging, as it is expected.
             ipython_shell.kernel._pdb_obj = None
+        elif post_mortem and isinstance(error, Exception):
+            error_type, error, tb = sys.exc_info()
+            post_mortem_excepthook(error_type, error, tb.tb_next)
         else:
             # We ignore the call to exec
             ipython_shell.showtraceback(tb_offset=1)
@@ -551,17 +522,15 @@ def runfile(filename=None, args=None, wdir=None, namespace=None,
                 os.chdir(wdir)
             else:
                 _print("Working directory {} doesn't exist.\n".format(wdir))
-        if post_mortem:
-            set_post_mortem()
 
         if __umr__.has_cython:
             # Cython files
             with io.open(filename, encoding='utf-8') as f:
                 ipython_shell.run_cell_magic('cython', '', f.read())
         else:
-            exec_code(file_code, filename, ns_globals, ns_locals)
+            exec_code(file_code, filename, ns_globals, ns_locals,
+                      post_mortem=post_mortem)
 
-        clear_post_mortem()
         sys.argv = ['']
 
 
@@ -589,7 +558,7 @@ def debugfile(filename=None, args=None, wdir=None, post_mortem=False,
 builtins.debugfile = debugfile
 
 
-def runcell(cellname, filename=None):
+def runcell(cellname, filename=None, post_mortem=False):
     """
     Run a code cell from an editor as a file.
 
@@ -643,13 +612,14 @@ def runcell(cellname, filename=None):
         file_code = None
     with NamespaceManager(filename, current_namespace=True,
                           file_code=file_code) as (ns_globals, ns_locals):
-        exec_code(cell_code, filename, ns_globals, ns_locals)
+        exec_code(cell_code, filename, ns_globals, ns_locals,
+                  post_mortem=post_mortem)
 
 
 builtins.runcell = runcell
 
 
-def debugcell(cellname, filename=None):
+def debugcell(cellname, filename=None, post_mortem=False):
     """Debug a cell."""
     if filename is None:
         filename = get_current_file_name()
diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py
--- a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py
+++ b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py
@@ -134,7 +134,8 @@ def interaction(self, frame, traceback):
             if frame and frame.f_back:
                 return self.interaction(frame.f_back, traceback)
         if (frame is not None
-                and "spydercustomize.py" in frame.f_code.co_filename):
+                and "spydercustomize.py" in frame.f_code.co_filename
+                and "exec_code" == frame.f_code.co_name):
             self.onecmd('exit')
         else:
             self.setup(frame, traceback)


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -3148,5 +3148,26 @@ def test_running_namespace(main_window, qtbot, tmpdir):
     assert nsb.editor.source_model._data['a']['view'] == '10'
     assert nsb.editor.source_model._data['b']['view'] == '10'
 
+
+@pytest.mark.slow
+@flaky(max_runs=3)
+def test_post_mortem(main_window, qtbot, tmpdir):
+    """Test post mortem works"""
+    # Check we can use custom complete for pdb
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+    control = main_window.ipyconsole.get_focus_widget()
+
+    test_file = tmpdir.join('test.py')
+    test_file.write('raise RuntimeError\n')
+
+    with qtbot.waitSignal(shell.executed):
+        shell.execute(
+            "runfile(" + repr(str(test_file)) + ", post_mortem=True)")
+
+    assert "ipdb>" in control.toPlainText()
+
+
 if __name__ == "__main__":
     pytest.main()