Repository: spyder-ide/spyder

Issue:
Hitting debug twice makes it unable to continue
When hitting debug one more time during a debugging session the prompt changes from IPdb [2] to ipdb>
Then I'm unable to continue debugging and have to enter any pdb command to exit.

Can reproduce every time.
Just add a break point and hit debug twice.

Issue since 4.2.0 and 4.2.1
Works ok in 4.1.5


Discussion:
Hi @nassimbnm,

I'm able to reproduce this error, my question is why are you hitting the debug button once in the debug session? In this way we can see how to address this behavior in Spyder.
For no special reason, sometimes by accident when already debugging for a long time I forget and hit the button again and loose my session.
Also the icon may suggest the debugging can be paused and resumed, perhaps the button sould be greyed / deactivated.
Also I was curious to see if nested debug sessions were possible.
> Also the icon may suggest the debugging can be paused and resumed, perhaps the button sould be greyed / deactivated.

Good point @nassimbnm! Thanks for bringing it out.

@impact27, what do you think about this one?
So nested debugging should be possible. This is definitely a bug to fix. 
> So nested debugging should be possible

I was thinking the same.

> This is definitely a bug to fix.

Could you help us with it?
Thank you guys @steff456 @ccordoba12  @impact27
You and all people behind Spyder are doing an amazing job.
Thanks @nassimbnm for your kind words! We really appreciate them.
> 
> Also the icon may suggest the debugging can be paused and resumed, perhaps the button sould be greyed / deactivated.

Yes, I really dislike that this button appears to be a "pause" button. Coming from IDL, in which you can just pause a program, it totally threw me that this wasn't actually a pause program button.



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
@@ -27,7 +27,7 @@
 
 from spyder_kernels.comms.frontendcomm import CommError, frontend_request
 from spyder_kernels.customize.namespace_manager import NamespaceManager
-from spyder_kernels.customize.spyderpdb import SpyderPdb
+from spyder_kernels.customize.spyderpdb import SpyderPdb, enter_debugger
 from spyder_kernels.customize.umr import UserModuleReloader
 from spyder_kernels.py3compat import TimeoutError, PY2, _print, encode
 
@@ -374,18 +374,6 @@ def get_current_file_name():
         return None
 
 
-def get_debugger(filename):
-    """Get a debugger for a given filename."""
-    debugger = pdb.Pdb()
-    filename = debugger.canonic(filename)
-    debugger._wait_for_mainpyfile = 1
-    debugger.mainpyfile = filename
-    debugger._user_requested_quit = 0
-    if os.name == 'nt':
-        filename = filename.replace('\\', '/')
-    return debugger, filename
-
-
 def count_leading_empty_lines(cell):
     """Count the number of leading empty cells."""
     if PY2:
@@ -597,10 +585,12 @@ def debugfile(filename=None, args=None, wdir=None, post_mortem=False,
         filename = get_current_file_name()
         if filename is None:
             return
-    debugger, filename = get_debugger(filename)
-    debugger.continue_if_has_breakpoints = True
-    debugger.run("runfile(%r, args=%r, wdir=%r, current_namespace=%r)" % (
-        filename, args, wdir, current_namespace))
+
+    enter_debugger(
+        filename, True,
+        "runfile({}" +
+        ", args=%r, wdir=%r, current_namespace=%r)" % (
+            args, wdir, current_namespace))
 
 
 builtins.debugfile = debugfile
@@ -678,11 +668,10 @@ def debugcell(cellname, filename=None, post_mortem=False):
         if filename is None:
             return
 
-    debugger, filename = get_debugger(filename)
-    # The breakpoint might not be in the cell
-    debugger.continue_if_has_breakpoints = False
-    debugger.run("runcell({}, {})".format(
-        repr(cellname), repr(filename)))
+    enter_debugger(
+        filename, False,
+        "runcell({}, ".format(repr(cellname)) +
+        "{})")
 
 
 builtins.debugcell = debugcell
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
@@ -8,8 +8,9 @@
 
 import ast
 import bdb
-import sys
 import logging
+import os
+import sys
 import traceback
 from collections import namedtuple
 
@@ -522,7 +523,7 @@ def user_return(self, frame, return_value):
             if (self.mainpyfile != self.canonic(frame.f_code.co_filename)
                     or frame.f_lineno <= 0):
                 return
-            self._wait_for_mainpyfile = 0
+            self._wait_for_mainpyfile = False
         super(SpyderPdb, self).user_return(frame, return_value)
 
     def _cmdloop(self):
@@ -710,3 +711,44 @@ def runcall(self, *args, **kwds):
         """
         with DebugWrapper(self):
             super(SpyderPdb, self).runcall(*args, **kwds)
+
+
+def enter_debugger(filename, continue_if_has_breakpoints, code_format):
+    """Enter debugger. Code format should be a format that accept filename."""
+    kernel = get_ipython().kernel
+    recursive = kernel.is_debugging()
+    if recursive:
+        parent_debugger = kernel._pdb_obj
+        sys.settrace(None)
+        globals = parent_debugger.curframe.f_globals
+        locals = parent_debugger.curframe_locals
+        # Create child debugger
+        debugger = SpyderPdb(
+            completekey=parent_debugger.completekey,
+            stdin=parent_debugger.stdin, stdout=parent_debugger.stdout)
+        debugger.use_rawinput = parent_debugger.use_rawinput
+        debugger.prompt = "(%s) " % parent_debugger.prompt.strip()
+    else:
+        debugger = SpyderPdb()
+
+    filename = debugger.canonic(filename)
+    debugger._wait_for_mainpyfile = True
+    debugger.mainpyfile = filename
+    debugger.continue_if_has_breakpoints = continue_if_has_breakpoints
+    debugger._user_requested_quit = False
+
+    if os.name == 'nt':
+        filename = filename.replace('\\', '/')
+
+    code = code_format.format(repr(filename))
+
+    if recursive:
+        # Enter recursive debugger
+        sys.call_tracing(debugger.run, (code, globals, locals))
+        # Reset parent debugger
+        sys.settrace(parent_debugger.trace_dispatch)
+        parent_debugger.lastcmd = debugger.lastcmd
+        kernel._register_pdb_session(parent_debugger)
+    else:
+        # The breakpoint might not be in the cell
+        debugger.run(code)


Test patch:
diff --git a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
--- a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
+++ b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
@@ -43,13 +43,6 @@
 TIMEOUT = 15
 SETUP_TIMEOUT = 60
 
-TKINTER_INSTALLED = False
-try:
-    import tkinter
-    TKINTER_INSTALLED = True
-except:
-    pass
-
 
 @contextmanager
 def setup_kernel(cmd):
@@ -605,9 +598,21 @@ def test_np_threshold(kernel):
         assert "{'float_kind': <built-in method format of str object" in content
 
 
+# We declare this constant immediately before the test, as determining
+# that TURTLE_ACTIVE is True will briefly pop up a window, similar to the
+# windows that will pop up during the test itself.
+TURTLE_ACTIVE = False
+try:
+    import turtle
+    turtle.Screen()
+    turtle.bye()
+    TURTLE_ACTIVE = True
+except:
+    pass
+
 @flaky(max_runs=3)
-@pytest.mark.skipif(not TKINTER_INSTALLED,
-                    reason="Doesn't work on Python installations without Tk")
+@pytest.mark.skipif(not TURTLE_ACTIVE,
+                    reason="Doesn't work on non-interactive settings or Python installations without Tk")
 def test_turtle_launch(tmpdir):
     """Test turtle scripts running in the same kernel."""
     # Command to start the kernel