Repository: spyder-ide/spyder

Issue:
Keyboard events leak from console to editor while debugging
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

create a file `tmp.py`:
```
def a():
    1/0
```
And a file `tmp2.py`:
```
from tmp import a
a()
```
 1. Run tmp2.py
 2. type in the console `%debug`
 3. Type `u+<Return>` and close `tmp.py`
 4. Type `d` without return
 5. Press return as many times and as quickly as possible.
 
I see 1-2 new lines being added to `tmp2.py` before `tmp.py` has time to load and the focus returns to the console.


## Versions

* Spyder version: 4.0.0.dev0 e5f2892d9
* Python version: 3.7.5
* Qt version: 5.12.6
* PyQt5 version: 5.12.3
* Operating System: Darwin 19.0.0

### Dependencies

```
cloudpickle >=0.5.0          :  1.2.2 (OK)
pygments >=2.0               :  2.4.2 (OK)
qtconsole >=4.6.0            :  4.7.0.dev0 (OK)
nbconvert >=4.0              :  5.6.1 (OK)
sphinx >=0.6.6               :  2.2.1 (OK)
pylint >=0.25                :  2.4.4 (OK)
psutil >=0.3                 :  5.6.5 (OK)
qtawesome >=0.5.7            :  0.6.0 (OK)
qtpy >=1.5.0                 :  1.9.0 (OK)
pickleshare >=0.4            :  0.7.5 (OK)
zmq >=17                     :  18.1.1 (OK)
chardet >=2.0.0              :  3.0.4 (OK)
numpydoc >=0.6.0             :  0.9.1 (OK)
spyder_kernels >=1.8.0;<2.0.0:  1.9.0.dev0 (OK)
qdarkstyle >=2.7             :  2.7 (OK)
atomicwrites >=1.2.0         :  1.3.0 (OK)
diff_match_patch >=20181111  :  20181111 (OK)
intervaltree                 :  None (OK)
watchdog                     :  None (OK)
keyring                      :  None (OK)
pexpect >=4.4.0              :  4.7.0 (OK)
pympler                      :  None (OK)
sympy >=0.7.3                :  None (NOK)
cython >=0.21                :  None (NOK)
IPython >=4.0                :  7.9.0 (OK)
matplotlib >=2.0.0           :  3.1.2 (OK)
pandas >=0.13.1              :  0.25.3 (OK)
numpy >=1.7                  :  1.17.4 (OK)
scipy >=0.17.0               :  1.3.3 (OK)
pyls >=0.31.0;<0.32.0        :  0.31.1 (OK)
applaunchservices >=0.1.7    :  0.2.1 (OK)
```



Discussion:

Resolving patch:
diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py
--- a/spyder/plugins/editor/plugin.py
+++ b/spyder/plugins/editor/plugin.py
@@ -1918,7 +1918,8 @@ def _convert(fname):
                 # (the one that can't be destroyed), then cloning this
                 # editor widget in all other editorstacks:
                 finfo = self.editorstacks[0].load(
-                    filename, set_current=False, add_where=add_where)
+                    filename, set_current=False, add_where=add_where,
+                    processevents=processevents)
                 finfo.path = self.main.get_spyder_pythonpath()
                 self._clone_file_everywhere(finfo)
                 current_editor = current_es.set_current_filename(filename,
diff --git a/spyder/plugins/editor/widgets/editor.py b/spyder/plugins/editor/widgets/editor.py
--- a/spyder/plugins/editor/widgets/editor.py
+++ b/spyder/plugins/editor/widgets/editor.py
@@ -2641,7 +2641,8 @@ def new(self, filename, encoding, text, default_content=False,
             finfo.editor.document().setModified(False)
         return finfo
 
-    def load(self, filename, set_current=True, add_where='end'):
+    def load(self, filename, set_current=True, add_where='end',
+             processevents=True):
         """
         Load filename, create an editor instance and return it
 
@@ -2652,14 +2653,16 @@ def load(self, filename, set_current=True, add_where='end'):
         plugin (in case multiple editorstack instances are handled)
         """
         filename = osp.abspath(to_text_string(filename))
-        self.starting_long_process.emit(_("Loading %s...") % filename)
+        if processevents:
+            self.starting_long_process.emit(_("Loading %s...") % filename)
         text, enc = encoding.read(filename)
         self.autosave.file_hashes[filename] = hash(text)
         finfo = self.create_new_editor(filename, enc, text, set_current,
                                        add_where=add_where)
         index = self.data.index(finfo)
         self._refresh_outlineexplorer(index, update=True)
-        self.ending_long_process.emit("")
+        if processevents:
+            self.ending_long_process.emit("")
         if self.isVisible() and self.checkeolchars_enabled \
            and sourcecode.has_mixed_eol_chars(text):
             name = osp.basename(filename)


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
@@ -2756,5 +2756,71 @@ def test_path_manager_updates_clients(qtbot, main_window, tmpdir):
     assert count >= 1
 
 
+@pytest.mark.slow
+@flaky(max_runs=3)
+@pytest.mark.skipif(os.name == 'nt' or sys.platform == 'darwin',
+                    reason="It times out on macOS and Windows")
+def test_pbd_key_leak(main_window, qtbot, tmpdir):
+    """
+    Check that pdb notify spyder doesn't call
+    QApplication.processEvents(). If it does there might be keystoke leakage.
+    see #10834
+    """
+    # Wait until the window is fully up
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+    control = shell._control
+
+    # Write code to a file
+    code1 = ("def a():\n"
+             "    1/0")
+    code2 = ("from tmp import a\n"
+             "a()")
+    folder = tmpdir.join('tmp_folder')
+    test_file = folder.join('tmp.py')
+    test_file.write(code1, ensure=True)
+
+    test_file2 = folder.join('tmp2.py')
+    test_file2.write(code2)
+
+    # Run tmp2 and get an error
+    with qtbot.waitSignal(shell.executed):
+        shell.execute('runfile("' + str(test_file2).replace("\\", "/") +
+                      '", wdir="' + str(folder).replace("\\", "/") + '")')
+    assert '1/0' in control.toPlainText()
+
+    # Replace QApplication.processEvents to make sure it is not called
+    super_processEvents = QApplication.processEvents
+
+    def processEvents():
+        processEvents.called = True
+        return super_processEvents()
+
+    processEvents.called = False
+    try:
+        QApplication.processEvents = processEvents
+        # Debug and open both files
+        shell.execute('%debug')
+        qtbot.waitUntil(lambda: control.toPlainText().split()[-1] == 'ipdb>')
+        qtbot.keyClick(control, 'u')
+        qtbot.keyClick(control, Qt.Key_Enter)
+        qtbot.waitUntil(lambda: control.toPlainText().split()[-1] == 'ipdb>')
+
+        # Wait until both files are open
+        qtbot.waitUntil(
+            lambda: osp.normpath(str(test_file)) in [
+                osp.normpath(p) for p in main_window.editor.get_filenames()])
+        qtbot.waitUntil(
+            lambda: str(test_file2) in [
+                osp.normpath(p) for p in main_window.editor.get_filenames()])
+
+        # Make sure the events are not processed.
+        assert not processEvents.called
+
+    finally:
+        QApplication.processEvents = super_processEvents
+
+
 if __name__ == "__main__":
     pytest.main()