Repository: spyder-ide/spyder

Issue:
Debugging seems to break with recent versions of Spyder and tqdm
Debugging seems to break with recent versions of Spyder and tqdm. The internal console gives the following output:

```
Traceback (most recent call last):
  File "D:\code\thirdparty\spyder\spyder\widgets\ipythonconsole\namespacebrowser.py", line 49, in <lambda>
    self.namespacebrowser.process_remote_view(data))
  File "D:\code\thirdparty\spyder\spyder\widgets\variableexplorer\namespacebrowser.py", line 232, in process_remote_view
    self.set_data(remote_view)
  File "D:\code\thirdparty\spyder\spyder\widgets\variableexplorer\namespacebrowser.py", line 243, in set_data
    self.editor.set_data(data)
  File "D:\code\thirdparty\spyder\spyder\widgets\variableexplorer\collectionseditor.py", line 830, in set_data
    self.model.set_data(data, self.dictfilter)
  File "D:\code\thirdparty\spyder\spyder\widgets\variableexplorer\collectionseditor.py", line 170, in set_data
    self.set_size_and_type()
  File "D:\code\thirdparty\spyder\spyder\widgets\variableexplorer\collectionseditor.py", line 185, in set_size_and_type
    for index in range(start, stop) ]
TypeError: 'int' object has no attribute '__getitem__'
```

**What steps will reproduce the problem?**

1. Make a simple script (e.g.):

```
import tqdm

1 + 1

b = 3 

print "Awesome"

c = 4

print "Awesome as well"
```

2. Put a breakpoint anywhere (e.g. print "Awesome")
3. Try to debug and press continue.

**What is the expected output? What do you see instead?**
Expect output would be to stop at the line and show a short print of the code:

```
     12 b = 3
     13 
2--> 14 print "Awesome"
     15 
     16 c = 4
```

instead you get:

```
{'e': {'color': '#0000ff', 'view': '2.718281828459045', 'type': u'float', 'size': 1}, 'euler_gamma': {'color': '#0000ff', 'view': '0.5772156649015329', 'type': u'float', 'size': 1}, 'pi': {'color': '#0000ff', 'view': '3.141592653589793', 'type': u'float', 'size': 1}, 'b': {'color': '#0000ff', 'view': '3', 'type': u'int', 'size': 1}}
```

## Versions and main components

* Spyder Version: 3.1.0
* Python Version: 2.7.13
* Operating system: Windows 10 x64


## Dependencies

Please go to the menu entry `Help > Optional Dependencies` (or
`Help > Dependencies`), press the button `Copy to clipboard`
and paste the contents below:

jedi >=0.8.1     :  0.9.0 (OK)
matplotlib >=1.0 :  2.0.0 (OK)
nbconvert >=4.0  :  5.0.0 (OK)
numpy >=1.7      :  1.12.0 (OK)
pandas >=0.13.1  :  0.19.2 (OK)
pep8 >=0.6       :  1.7.0 (OK)
pyflakes >=0.5.0 :  1.5.0 (OK)
pygments >=2.0   :  2.1.3 (OK)
pylint >=0.25    :  1.6.4 (OK)
qtconsole >=4.2.0:  4.2.1 (OK)
rope >=0.9.4     :  0.10.3 (OK)
sphinx >=0.6.6   :  1.5.1 (OK)
sympy >=0.7.3    :  1.0 (OK)




Discussion:
Thanks for reporting and for the reproducible example. We'll try to fix this error in our next release.

@dalthviz, please take a look at this one and create a PR for it as soon as possible.

Resolving patch:
diff --git a/spyder/widgets/ipythonconsole/client.py b/spyder/widgets/ipythonconsole/client.py
--- a/spyder/widgets/ipythonconsole/client.py
+++ b/spyder/widgets/ipythonconsole/client.py
@@ -177,6 +177,11 @@ def configure_shellwidget(self, give_focus=True):
         self.shellwidget.sig_kernel_restarted.connect(
             self.kernel_restarted_message)
 
+        # To restart the kernel when errors happened while debugging
+        # See issue 4003
+        self.shellwidget.sig_dbg_kernel_restart.connect(
+                self.restart_kernel)
+
     def enable_stop_button(self):
         self.stop_button.setEnabled(True)
 
@@ -323,12 +328,20 @@ def restart_kernel(self):
         Took this code from the qtconsole project
         Licensed under the BSD license
         """
-        message = _('Are you sure you want to restart the kernel?')
-        buttons = QMessageBox.Yes | QMessageBox.No
-        result = QMessageBox.question(self, _('Restart kernel?'),
-                                      message, buttons)
-        if result == QMessageBox.Yes:
-            sw = self.shellwidget
+        sw = self.shellwidget
+
+        # This is needed to restart the kernel without a prompt
+        # when an error in stdout corrupts the debugging process.
+        # See issue 4003
+        if not sw._input_reply_failed:
+            message = _('Are you sure you want to restart the kernel?')
+            buttons = QMessageBox.Yes | QMessageBox.No
+            result = QMessageBox.question(self, _('Restart kernel?'),
+                                          message, buttons)
+        else:
+            result = None
+
+        if result == QMessageBox.Yes or sw._input_reply_failed:
             if sw.kernel_manager:
                 if self.infowidget.isVisible():
                     self.infowidget.hide()
@@ -341,10 +354,16 @@ def restart_kernel(self):
                         before_prompt=True
                     )
                 else:
-                    sw.reset(clear=True)
-                    sw._append_html(_("<br>Restarting kernel...\n<hr><br>"),
-                        before_prompt=False,
-                    )
+                    sw.reset(clear=not sw._input_reply_failed)
+                    if sw._input_reply_failed:
+                        sw._append_html(_("<br>Restarting kernel because "
+                                        "an error occurred while "
+                                        "debugging\n<hr><br>"),
+                                        before_prompt=False)
+                        sw._input_reply_failed = False
+                    else:
+                        sw._append_html(_("<br>Restarting kernel...\n<hr><br>"),
+                                before_prompt=False)
             else:
                 sw._append_plain_text(
                     _('Cannot restart a kernel not started by Spyder\n'),
diff --git a/spyder/widgets/ipythonconsole/debugging.py b/spyder/widgets/ipythonconsole/debugging.py
--- a/spyder/widgets/ipythonconsole/debugging.py
+++ b/spyder/widgets/ipythonconsole/debugging.py
@@ -25,7 +25,8 @@ class DebuggingWidget(RichJupyterWidget):
     Spyder
     """
 
-    _input_reply = None
+    _input_reply = {}
+    _input_reply_failed = False
 
     # --- Public API --------------------------------------------------
     def silent_exec_input(self, code):
@@ -47,20 +48,33 @@ def silent_exec_input(self, code):
         self._hidden = False
 
         # Emit signal
-        if 'pdb_step' in code and self._input_reply is not None:
-            fname = self._input_reply['fname']
-            lineno = self._input_reply['lineno']
-            self.sig_pdb_step.emit(fname, lineno)
-        elif 'get_namespace_view' in code:
-            view = self._input_reply
-            self.sig_namespace_view.emit(view)
-        elif 'get_var_properties' in code:
-            properties = self._input_reply
-            self.sig_var_properties.emit(properties)
+        if isinstance(self._input_reply, dict):
+            if 'pdb_step' in code and 'fname' in self._input_reply:
+                fname = self._input_reply['fname']
+                lineno = self._input_reply['lineno']
+                self.sig_pdb_step.emit(fname, lineno)
+            elif 'get_namespace_view' in code:
+                if 'fname' not in self._input_reply:
+                    view = self._input_reply
+                else:
+                    view = None
+                self.sig_namespace_view.emit(view)
+            elif 'get_var_properties' in code:
+                if 'fname' not in self._input_reply:
+                    properties = self._input_reply
+                else:
+                    properties = None
+                self.sig_var_properties.emit(properties)
+        else:
+            self.kernel_client.iopub_channel.flush()
+            self._input_reply = {}
+            self._input_reply_failed = True
+            self.sig_dbg_kernel_restart.emit()
 
     def write_to_stdin(self, line):
         """Send raw characters to the IPython kernel through stdin"""
         wait_loop = QEventLoop()
+        self.kernel_client.iopub_channel.flush()
         self.sig_prompt_ready.connect(wait_loop.quit)
         self.kernel_client.input(line)
         wait_loop.exec_()
@@ -136,7 +150,10 @@ def _handle_stream(self, msg):
                         reply = ast.literal_eval(text)
                     except:
                         reply = None
-                    self._input_reply = reply
+                    if not isinstance(reply, dict):
+                        self._input_reply = None
+                    else:
+                        self._input_reply = reply
                     self.sig_input_reply.emit()
                 else:
                     self._input_reply = None
diff --git a/spyder/widgets/ipythonconsole/shell.py b/spyder/widgets/ipythonconsole/shell.py
--- a/spyder/widgets/ipythonconsole/shell.py
+++ b/spyder/widgets/ipythonconsole/shell.py
@@ -40,6 +40,7 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget):
     sig_input_reply = Signal()
     sig_pdb_step = Signal(str, int)
     sig_prompt_ready = Signal()
+    sig_dbg_kernel_restart = Signal()
 
     # For ShellWidget
     focus_changed = Signal()


Test patch:
diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py
--- a/spyder/plugins/tests/test_ipythonconsole.py
+++ b/spyder/plugins/tests/test_ipythonconsole.py
@@ -12,7 +12,7 @@
 from flaky import flaky
 import pytest
 from qtpy.QtCore import Qt, QTimer
-from qtpy.QtWidgets import QApplication
+from qtpy.QtWidgets import QApplication, QMessageBox
 
 from spyder.plugins.ipythonconsole import (IPythonConsole,
                                            KernelConnectionDialog)
@@ -35,6 +35,13 @@ def open_client_from_connection_info(connection_info, qtbot):
             qtbot.keyClick(w, Qt.Key_Enter)
 
 
+def restart_kernel(qtbot):
+    top_level_widgets = QApplication.topLevelWidgets()
+    for w in top_level_widgets:
+        if isinstance(w, QMessageBox):
+            qtbot.keyClick(w, Qt.Key_Enter)
+
+
 #==============================================================================
 # Qt Test Fixtures
 #==============================================================================
@@ -52,7 +59,59 @@ def close_widget():
 #==============================================================================
 # Tests
 #==============================================================================
-@flaky(max_runs=3)
+@flaky(max_runs=10)
+@pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows")
+def test_forced_restart_kernel(ipyconsole, qtbot):
+    """
+    Test that kernel is restarted if we force it do it
+    during debugging
+    """
+    shell = ipyconsole.get_current_shellwidget()
+    client = ipyconsole.get_current_client()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    # Do an assigment to verify that it's not there after restarting
+    shell.execute('a = 10')
+    qtbot.wait(500)
+
+    # Generate a traceback and enter debugging mode
+    shell.execute('1/0')
+    qtbot.wait(500)
+    shell.execute('%debug')
+    qtbot.wait(500)
+
+    # Force a kernel restart and wait until it's up again
+    shell._prompt_html = None
+    shell.silent_exec_input("1+1")  # The return type of this must be a dict!!
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    assert not shell.is_defined('a')
+
+
+@flaky(max_runs=10)
+@pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows")
+def test_restart_kernel(ipyconsole, qtbot):
+    """
+    Test that kernel is restarted correctly
+    """
+    shell = ipyconsole.get_current_shellwidget()
+    client = ipyconsole.get_current_client()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    # Do an assigment to verify that it's not there after restarting
+    shell.execute('a = 10')
+    qtbot.wait(500)
+
+    # Restart kernel and wait until it's up again
+    shell._prompt_html = None
+    QTimer.singleShot(1000, lambda: restart_kernel(qtbot))
+    client.restart_kernel()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    assert not shell.is_defined('a')
+
+
+@flaky(max_runs=10)
 @pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows")
 def test_load_kernel_file_from_id(ipyconsole, qtbot):
     """
diff --git a/spyder/utils/tests/test_programs.py b/spyder/utils/tests/test_programs.py
--- a/spyder/utils/tests/test_programs.py
+++ b/spyder/utils/tests/test_programs.py
@@ -23,7 +23,8 @@
     INVALID_INTERPRETER = os.path.join(home_dir, 'miniconda', 'bin', 'ipython')
 
 
-@pytest.mark.skipif(os.name == 'nt', reason='gets stuck on Windows') # FIXME
+@pytest.mark.skipif(os.name == 'nt' or os.environ.get('CI', None) is None,
+                    reason='gets stuck on Windows and fails sometimes locally') # FIXME
 def test_run_python_script_in_terminal(tmpdir, qtbot):
     scriptpath = tmpdir.join('write-done.py')
     outfilepath = tmpdir.join('out.txt')
@@ -37,7 +38,8 @@ def test_run_python_script_in_terminal(tmpdir, qtbot):
     assert res == 'done'
 
 
-@pytest.mark.skipif(os.name == 'nt', reason='gets stuck on Windows') # FIXME
+@pytest.mark.skipif(os.name == 'nt' or os.environ.get('CI', None) is None,
+                    reason='gets stuck on Windows and fails sometimes locally') # FIXME
 def test_run_python_script_in_terminal_with_wdir_empty(tmpdir, qtbot):
     scriptpath = tmpdir.join('write-done.py')
     outfilepath = tmpdir.join('out.txt')