From 33cd813eaf0a9f968db39be97c703f9cc27ea3a4 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 10 Mar 2017 17:15:07 -0500 Subject: [PATCH 1/4] IPython console: Simplify set backend for Mayavi functionality --- spyder/widgets/ipythonconsole/client.py | 20 ++------------------ spyder/widgets/ipythonconsole/shell.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/spyder/widgets/ipythonconsole/client.py b/spyder/widgets/ipythonconsole/client.py index 151d53c813e..dd4015cbfbf 100644 --- a/spyder/widgets/ipythonconsole/client.py +++ b/spyder/widgets/ipythonconsole/client.py @@ -158,7 +158,8 @@ def configure_shellwidget(self, give_focus=True): self.shellwidget.executing.connect(self.add_to_history) # For Mayavi to run correctly - self.shellwidget.executing.connect(self.set_backend_for_mayavi) + self.shellwidget.executing.connect( + self.shellwidget.set_backend_for_mayavi) # To update history after execution self.shellwidget.executed.connect(self.update_history) @@ -405,23 +406,6 @@ def reset_namespace(self): def update_history(self): self.history = self.shellwidget._history - def set_backend_for_mayavi(self, command): - """ - Mayavi plots require the Qt backend, so we try to detect if one is - generated to change backends - """ - calling_mayavi = False - lines = command.splitlines() - for l in lines: - if not l.startswith('#'): - if 'import mayavi' in l or 'from mayavi' in l: - calling_mayavi = True - break - if calling_mayavi: - message = _("Changing backend to Qt for Mayavi") - self.shellwidget._append_plain_text(message + '\n') - self.shellwidget.execute("%gui inline\n%gui qt") - #------ Private API ------------------------------------------------------- def _create_loading_page(self): """Create html page to show while the kernel is starting""" diff --git a/spyder/widgets/ipythonconsole/shell.py b/spyder/widgets/ipythonconsole/shell.py index 0c36329a117..8b1acdb22cb 100644 --- a/spyder/widgets/ipythonconsole/shell.py +++ b/spyder/widgets/ipythonconsole/shell.py @@ -265,6 +265,23 @@ def handle_exec_method(self, msg): # Remove method after being processed self._kernel_methods.pop(expression) + def set_backend_for_mayavi(self, command): + """ + Mayavi plots require the Qt backend, so we try to detect if one is + generated to change backends + """ + calling_mayavi = False + lines = command.splitlines() + for l in lines: + if not l.startswith('#'): + if 'import mayavi' in l or 'from mayavi' in l: + calling_mayavi = True + break + if calling_mayavi: + message = _("Changing backend to Qt for Mayavi") + self._append_plain_text(message + '\n') + self.silent_execute("%gui inline\n%gui qt") + #---- Private methods (overrode by us) --------------------------------- def _context_menu_make(self, pos): """Reimplement the IPython context menu""" From ea187ba34f00e071d2a44a3524873023845f198f Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 11 Mar 2017 12:09:06 -0500 Subject: [PATCH 2/4] IPython console: Rerun %matplotlib calls to correctly change backend interactively --- spyder/widgets/ipythonconsole/client.py | 4 ++++ spyder/widgets/ipythonconsole/shell.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/spyder/widgets/ipythonconsole/client.py b/spyder/widgets/ipythonconsole/client.py index dd4015cbfbf..f9c5d254e1f 100644 --- a/spyder/widgets/ipythonconsole/client.py +++ b/spyder/widgets/ipythonconsole/client.py @@ -183,6 +183,10 @@ def configure_shellwidget(self, give_focus=True): self.shellwidget.sig_dbg_kernel_restart.connect( self.restart_kernel) + # To correctly change Matplotlib backend interactively + self.shellwidget.executing.connect( + self.shellwidget.change_mpl_backend) + def enable_stop_button(self): self.stop_button.setEnabled(True) diff --git a/spyder/widgets/ipythonconsole/shell.py b/spyder/widgets/ipythonconsole/shell.py index 8b1acdb22cb..589ea3622c3 100644 --- a/spyder/widgets/ipythonconsole/shell.py +++ b/spyder/widgets/ipythonconsole/shell.py @@ -282,6 +282,18 @@ def set_backend_for_mayavi(self, command): self._append_plain_text(message + '\n') self.silent_execute("%gui inline\n%gui qt") + def change_mpl_backend(self, command): + """ + If the user is trying to change Matplotlib backends with + %matplotlib, send the same command again to the kernel to + correctly change it. + + Fixes issue 4002 + """ + if command.startswith('%matplotlib') and \ + len(command.splitlines()) == 1: + self.silent_execute(command) + #---- Private methods (overrode by us) --------------------------------- def _context_menu_make(self, pos): """Reimplement the IPython context menu""" From 5b42c2ce0cf3fb35f59cbaebeea0c68f5dae467b Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 11 Mar 2017 12:14:28 -0500 Subject: [PATCH 3/4] Testing: Verify that changing backends with %matplotlib is working as expected --- spyder/plugins/tests/test_ipythonconsole.py | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py index c7a07db5288..44413e6fb8d 100644 --- a/spyder/plugins/tests/test_ipythonconsole.py +++ b/spyder/plugins/tests/test_ipythonconsole.py @@ -59,6 +59,37 @@ def close_widget(): #============================================================================== # Tests #============================================================================== +@flaky(max_runs=10) +@pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows") +def test_mpl_backend_change(ipyconsole, qtbot): + """ + Test that Matplotlib backend is changed correctly when + using the %matplotlib magic + """ + shell = ipyconsole.get_current_shellwidget() + client = ipyconsole.get_current_client() + qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) + + # Import Matplotlib + with qtbot.waitSignal(shell.executed): + shell.execute('import matplotlib.pyplot as plt') + + # Generate an inline plot + with qtbot.waitSignal(shell.executed): + shell.execute('plt.plot(range(10))') + + # Change backends + with qtbot.waitSignal(shell.executed): + shell.execute('%matplotlib tk') + + # Generate another plot + with qtbot.waitSignal(shell.executed): + shell.execute('plt.plot(range(10))') + + # Assert that there's a single inline plot in the console + assert shell._control.toHtml().count('img src') == 1 + + @flaky(max_runs=10) @pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows") def test_forced_restart_kernel(ipyconsole, qtbot): From faa8efd3805d268c181dcd711078f110577f1d53 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 11 Mar 2017 12:22:29 -0500 Subject: [PATCH 4/4] Testing: Use executed signal to wait until an execution is finished --- spyder/plugins/tests/test_ipythonconsole.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py index 44413e6fb8d..5888d67884a 100644 --- a/spyder/plugins/tests/test_ipythonconsole.py +++ b/spyder/plugins/tests/test_ipythonconsole.py @@ -102,12 +102,13 @@ def test_forced_restart_kernel(ipyconsole, qtbot): 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) + with qtbot.waitSignal(shell.executed): + shell.execute('a = 10') # Generate a traceback and enter debugging mode - shell.execute('1/0') - qtbot.wait(500) + with qtbot.waitSignal(shell.executed): + shell.execute('1/0') + shell.execute('%debug') qtbot.wait(500) @@ -130,8 +131,8 @@ def test_restart_kernel(ipyconsole, qtbot): 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) + with qtbot.waitSignal(shell.executed): + shell.execute('a = 10') # Restart kernel and wait until it's up again shell._prompt_html = None @@ -207,8 +208,8 @@ def test_load_kernel_file(ipyconsole, qtbot): new_client = ipyconsole.get_clients()[1] new_shell = new_client.shellwidget - new_shell.execute('a = 10') - qtbot.wait(500) + with qtbot.waitSignal(new_shell.executed): + new_shell.execute('a = 10') assert new_client.name == '1/B' assert shell.get_value('a') == new_shell.get_value('a')