From 2cd3562fa392a1889399834cfe90d90749692931 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 17 May 2024 16:14:30 -0700 Subject: [PATCH 1/9] When activating graphics support, send all inline configurations to the kernel. --- .../ipythonconsole/widgets/main_widget.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py index d5bb43d9099..8ed51d4234b 100644 --- a/spyder/plugins/ipythonconsole/widgets/main_widget.py +++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py @@ -1040,31 +1040,36 @@ def change_possible_restart_and_mpl_conf(self, option, value): (pylab_restart and clients_backend_require_restart[idx]) or restart_needed ) + _options = options.copy() - if not (restart and restart_all) or no_restart: + if autoload_n in options: # Autoload can't be applied without a restart. This avoids an # incorrect message in the console too. - if autoload_n in options: - options.pop(autoload_n) + _options.pop(autoload_n) + + if pylab_n in _options and pylab_o: + # Activating support requires sending all inline configs + _options = None + if not (restart and restart_all) or no_restart: sw = client.shellwidget if sw.is_debugging() and sw._executing: # Apply conf when the next Pdb prompt is available - def change_client_mpl_conf(o=options, c=client): + def change_client_mpl_conf(o=_options, c=client): self._change_client_mpl_conf(o, c) sw.sig_pdb_prompt_ready.disconnect( change_client_mpl_conf) sw.sig_pdb_prompt_ready.connect(change_client_mpl_conf) else: - self._change_client_mpl_conf(options, client) + self._change_client_mpl_conf(_options, client) elif restart and restart_all: self.restart_kernel(client, ask_before_restart=False) if ( ( (pylab_restart and current_client_backend_require_restart) - or restart_needed + or restart_needed ) and restart_current and current_client @@ -1984,7 +1989,7 @@ def _current_client_print(self): # This makes the print dialog have the same style as the rest of # the app. printer = SpyderPrinter(mode=QPrinter.HighResolution) - if(QPrintDialog(printer, self).exec_() != QPrintDialog.Accepted): + if (QPrintDialog(printer, self).exec_() != QPrintDialog.Accepted): return client.shellwidget._control.print_(printer) From f70f90e9d02415d1fff8f90ab5bbba8556137fde Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 17 May 2024 16:25:38 -0700 Subject: [PATCH 2/9] Do not set a default InlineBackend configuration in spyder kernels. This prevents user's matplotlib rc parameters from being clobbered when Spyder's "Activate support" is disabled. This makes sense because when disabled, a user should expect that Spyder does not interfere in any way. --- .../spyder_kernels/console/start.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/external-deps/spyder-kernels/spyder_kernels/console/start.py b/external-deps/spyder-kernels/spyder_kernels/console/start.py index eb910305957..600b656c02e 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/start.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/start.py @@ -93,28 +93,6 @@ def kernel_config(): "del sys; del pdb" ) - # Default inline backend configuration. - # This is useful to have when people doesn't - # use our config system to configure the - # inline backend but want to use - # '%matplotlib inline' at runtime - spy_cfg.InlineBackend.rc = { - # The typical default figure size is too large for inline use, - # so we shrink the figure size to 6x4, and tweak fonts to - # make that fit. - 'figure.figsize': (6.0, 4.0), - # 72 dpi matches SVG/qtconsole. - # This only affects PNG export, as SVG has no dpi setting. - 'figure.dpi': 72, - # 12pt labels get cutoff on 6x4 logplots, so use 10pt. - 'font.size': 10, - # 10pt still needs a little more room on the xlabel - 'figure.subplot.bottom': .125, - # Play nicely with any background color. - 'figure.facecolor': 'white', - 'figure.edgecolor': 'white' - } - if is_module_installed('matplotlib'): spy_cfg.IPKernelApp.matplotlib = "inline" From 6bdb94dc976a9452d87d11012d74edaed9c6a128 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 17 May 2024 16:45:26 -0700 Subject: [PATCH 3/9] Require kernel restart if deactivating graphics support. --- spyder/plugins/ipythonconsole/widgets/main_widget.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py index 8ed51d4234b..62cd047b1f9 100644 --- a/spyder/plugins/ipythonconsole/widgets/main_widget.py +++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py @@ -938,7 +938,11 @@ def change_possible_restart_and_mpl_conf(self, option, value): symbolic_math_n, hide_cmd_windows_n, ] - restart_needed = option in restart_options + restart_needed = ( + option in restart_options + # Deactivating graphics support + or (option == pylab_n and not value) + ) inline_backend = 'inline' pylab_restart = False From f1fcefd112b3e7093981d76d08a5327b34c6c81d Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 17 May 2024 17:54:31 -0700 Subject: [PATCH 4/9] Only set kernel configuration for matplotlib if graphics support is activated. --- spyder/plugins/ipythonconsole/widgets/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py index 06cff0acc47..abaea8ea0e4 100644 --- a/spyder/plugins/ipythonconsole/widgets/shell.py +++ b/spyder/plugins/ipythonconsole/widgets/shell.py @@ -640,7 +640,7 @@ def send_mpl_backend(self, option=None): if option is None or pylab_autoload_n in option: matplotlib_conf[pylab_autoload_n] = autoload_pylab_o - if matplotlib_conf: + if matplotlib_conf and pylab_o: self.set_kernel_configuration("matplotlib", matplotlib_conf) def get_cwd(self): From 2ffcf4cb24f1d248c0aa8d90747158a531b6a717 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Wed, 29 May 2024 21:19:28 -0700 Subject: [PATCH 5/9] PEP8 --- .../tests/test_ipythonconsole.py | 83 +++++++++++-------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index 7e8bb992531..ea1e4364c52 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -104,7 +104,7 @@ def test_banners(ipyconsole, qtbot): ("foo", # Check we display the right tooltip for interactive objects ["x", "y"], ["My function"]) - ] + ] ) @pytest.mark.skipif(running_in_ci() and not os.name == 'nt', reason="Times out on macOS and fails on Linux") @@ -504,7 +504,7 @@ def test_request_env(ipyconsole, qtbot): # Add a new entry to os.environ with qtbot.waitSignal(shell.executed): - shell.execute("import os; os.environ['FOO'] = 'bar'" ) + shell.execute("import os; os.environ['FOO'] = 'bar'") # Ask for os.environ contents with qtbot.waitSignal(shell.sig_show_env) as blocker: @@ -749,10 +749,7 @@ def test_execute_events_dbg(ipyconsole, qtbot): # Set processing events to True ipyconsole.set_conf('pdb_execute_events', True, section='debugger') - shell.set_kernel_configuration( - "pdb", { - 'pdb_execute_events': True - }) + shell.set_kernel_configuration("pdb", {'pdb_execute_events': True}) # Test reset magic qtbot.keyClicks(control, 'plt.plot(range(10))') @@ -764,10 +761,7 @@ def test_execute_events_dbg(ipyconsole, qtbot): # Set processing events to False ipyconsole.set_conf('pdb_execute_events', False, section='debugger') - shell.set_kernel_configuration( - "pdb", { - 'pdb_execute_events': False - }) + shell.set_kernel_configuration("pdb", {'pdb_execute_events': False}) # Test reset magic qtbot.keyClicks(control, 'plt.plot(range(10))') @@ -851,7 +845,6 @@ def test_mpl_backend_change(ipyconsole, qtbot): assert shell._control.toHtml().count('img src') == 1 - @flaky(max_runs=10) @pytest.mark.skipif( os.name == 'nt' or sys.platform == "darwin", @@ -1513,19 +1506,33 @@ def test_stderr_poll(ipyconsole, qtbot): 'import sys; print("test_" + "test", file=sys.__stderr__)') # Wait for the poll - qtbot.waitUntil(lambda: "test_test" in ipyconsole.get_widget( - ).get_focus_widget().toPlainText()) - assert "test_test" in ipyconsole.get_widget( - ).get_focus_widget().toPlainText() + qtbot.waitUntil( + lambda: "test_test" in ipyconsole.get_widget() + .get_focus_widget() + .toPlainText() + ) + assert ( + "test_test" in ipyconsole.get_widget() + .get_focus_widget() + .toPlainText() + ) # Write a second time, makes sure it is not duplicated with qtbot.waitSignal(shell.executed): shell.execute( 'import sys; print("test_" + "test", file=sys.__stderr__)') # Wait for the poll - qtbot.waitUntil(lambda: ipyconsole.get_widget().get_focus_widget( - ).toPlainText().count("test_test") == 2) - assert ipyconsole.get_widget().get_focus_widget().toPlainText( - ).count("test_test") == 2 + qtbot.waitUntil( + lambda: ipyconsole.get_widget() + .get_focus_widget() + .toPlainText() + .count("test_test") == 2 + ) + assert ( + ipyconsole.get_widget() + .get_focus_widget() + .toPlainText() + .count("test_test") == 2 + ) @flaky(max_runs=3) @@ -1536,8 +1543,12 @@ def test_stdout_poll(ipyconsole, qtbot): shell.execute('import sys; print("test_test", file=sys.__stdout__)') # Wait for the poll - qtbot.waitUntil(lambda: "test_test" in ipyconsole.get_widget( - ).get_focus_widget().toPlainText(), timeout=5000) + qtbot.waitUntil( + lambda: "test_test" in ipyconsole.get_widget() + .get_focus_widget() + .toPlainText(), + timeout=5000 + ) @flaky(max_runs=10) @@ -1893,17 +1904,18 @@ def test_pdb_comprehension_namespace(ipyconsole, qtbot, tmpdir): assert "test 11" in control.toPlainText() settings = { - 'check_all': False, - 'exclude_callables_and_modules': True, - 'exclude_capitalized': False, - 'exclude_private': True, - 'exclude_unsupported': False, - 'exclude_uppercase': True, - 'excluded_names': [], - 'minmax': False, - 'show_callable_attributes': True, - 'show_special_attributes': False, - 'filter_on': True} + 'check_all': False, + 'exclude_callables_and_modules': True, + 'exclude_capitalized': False, + 'exclude_private': True, + 'exclude_unsupported': False, + 'exclude_uppercase': True, + 'excluded_names': [], + 'minmax': False, + 'show_callable_attributes': True, + 'show_special_attributes': False, + 'filter_on': True + } shell.set_kernel_configuration("namespace_view_settings", settings) namespace = shell.call_kernel(blocking=True).get_namespace_view() @@ -2000,8 +2012,10 @@ def get_cwd_of_new_client(): ipyconsole.create_new_client() shell = ipyconsole.get_current_shellwidget() qtbot.waitUntil( - lambda: shell.spyder_kernel_ready and shell._prompt_html is not None, - timeout=SHELL_TIMEOUT) + lambda: shell.spyder_kernel_ready + and shell._prompt_html is not None, + timeout=SHELL_TIMEOUT + ) with qtbot.waitSignal(shell.executed): shell.execute('import os; cwd = os.getcwd()') @@ -2123,7 +2137,6 @@ def test_varexp_magic_dbg_locals(ipyconsole, qtbot): with qtbot.waitSignal(shell.executed): shell.execute("%debug f()") - # Get to an object that can be plotted for _ in range(4): with qtbot.waitSignal(shell.executed): From 9ac8ac9d1f9ba756f60482f6ffa559e685c91ff0 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Thu, 30 May 2024 18:00:32 -0700 Subject: [PATCH 6/9] Add test for rcParams when backend changes between interactive and inline --- .../plugins/ipythonconsole/tests/conftest.py | 26 ++++++- .../tests/test_ipythonconsole.py | 76 +++++++++++++++++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/ipythonconsole/tests/conftest.py b/spyder/plugins/ipythonconsole/tests/conftest.py index 9ae6f8ba94c..5a9694be416 100644 --- a/spyder/plugins/ipythonconsole/tests/conftest.py +++ b/spyder/plugins/ipythonconsole/tests/conftest.py @@ -248,8 +248,7 @@ def get_plugin(name): timeout=SHELL_TIMEOUT) except Exception: # Print content of shellwidget and close window - print(console.get_current_shellwidget( - )._control.toPlainText()) + print(console.get_current_shellwidget()._control.toPlainText()) client = console.get_current_client() if client.info_page != client.blank_page: print('info_page') @@ -275,8 +274,7 @@ def get_plugin(name): if request.node.rep_setup.passed: if request.node.rep_call.failed: # Print content of shellwidget and close window - print(console.get_current_shellwidget( - )._control.toPlainText()) + print(console.get_current_shellwidget()._control.toPlainText()) client = console.get_current_client() if client.info_page != client.blank_page: print('info_page') @@ -342,3 +340,23 @@ def threads_condition(): files = [repr(f) for f in proc.open_files()] show_diff(init_files, files, "files") raise + + +@pytest.fixture +def mpl_rc_file(): + """Create matplotlibrc file""" + file_contents = """\ +figure.dpi: 99 +figure.figsize: 9, 9 +figure.subplot.bottom: 0.9 +font.size: 9 +""" + rc_file = osp.join(TEMP_DIRECTORY, 'matplotlibrc') + with open(rc_file, 'w') as f: + f.write(file_contents) + os.environ['MATPLOTLIBRC'] = rc_file + + yield + + os.environ.pop('MATPLOTLIBRC') + os.remove(rc_file) diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index ea1e4364c52..6fb452bc1cf 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -1995,6 +1995,82 @@ def test_mpl_conf(ipyconsole, qtbot): mock.assert_called_once_with({'pylab/inline/bottom': 0.314}) +def test_rc_params(mpl_rc_file, ipyconsole, qtbot): + """ + Test that rcParams are correctly set/reset when changing backends and + setting inline preferences. + """ + # import pdb; pdb.set_trace() + rc_file = os.getenv('MATPLOTLIBRC') + assert rc_file is not None + assert osp.exists(rc_file) + + main_widget = ipyconsole.get_widget() + shell = ipyconsole.get_current_shellwidget() + assert shell.get_matplotlib_backend().lower() == 'inline' + + with qtbot.waitSignal(shell.executed): + shell.execute( + 'import matplotlib as mpl;' + 'rcp = mpl.rcParams;' + 'rc_file = mpl.matplotlib_fname()' + ) + + # Test that the rc file is loaded + assert rc_file == shell.get_value('rc_file') + + # Test that inline preferences are at defaults + rcp = shell.get_value('rcp') + assert rcp['figure.dpi'] == 144 + assert rcp['figure.figsize'] == [6, 4] + assert rcp['figure.subplot.bottom'] == 0.11 + assert rcp['font.size'] == 10 + + # Test that changing inline preferences are effective immediately + def set_conf(option, value): + main_widget.set_conf(option, value) + # Allow time for the %config magic to run for each config change + qtbot.wait(20) + + set_conf('pylab/inline/resolution', 112) + set_conf('pylab/inline/width', 12) + set_conf('pylab/inline/height', 12) + set_conf('pylab/inline/bottom', 0.12) + set_conf('pylab/inline/fontsize', 12) + rcp = shell.get_value('rcp') + assert rcp['figure.dpi'] == 112 + assert rcp['figure.figsize'] == [12, 12] + assert rcp['figure.subplot.bottom'] == 0.12 + assert rcp['font.size'] == 12 + + # Test that setting explicitly does not persist on backend change + with qtbot.waitSignal(shell.executed): + shell.execute('mpl.rcParams["font.size"] = 15') + + # Test that file defaults are restored on backend change + with qtbot.waitSignal(shell.executed): + shell.execute('%matplotlib qt') + assert shell.get_matplotlib_backend().lower() == 'qt' + rcp = shell.get_value('rcp') + assert rcp['figure.dpi'] == 99 + assert rcp['figure.figsize'] == [9, 9] + assert rcp['figure.subplot.bottom'] == 0.9 + assert rcp['font.size'] == 9 + + # Test that setting explicitly does not persist on backend change + with qtbot.waitSignal(shell.executed): + shell.execute('mpl.rcParams["font.size"] = 15') + + # Test that inline preferences are restored on backend change + with qtbot.waitSignal(shell.executed): + shell.execute('%matplotlib inline') + rcp = shell.get_value('rcp') + assert rcp['figure.dpi'] == 112 + assert rcp['figure.figsize'] == [12, 12] + assert rcp['figure.subplot.bottom'] == 0.12 + assert rcp['font.size'] == 12 + + @flaky(max_runs=3) @pytest.mark.no_web_widgets def test_no_infowidget(ipyconsole): From bdda9ff2d218574e6ee6f350f93d95e5acc76b20 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 31 May 2024 11:41:36 -0700 Subject: [PATCH 7/9] Apply suggestions from code review Co-authored-by: Carlos Cordoba --- .../plugins/ipythonconsole/tests/conftest.py | 6 +-- .../tests/test_ipythonconsole.py | 37 +++++++++---------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/spyder/plugins/ipythonconsole/tests/conftest.py b/spyder/plugins/ipythonconsole/tests/conftest.py index 5a9694be416..be9cf9ed3d9 100644 --- a/spyder/plugins/ipythonconsole/tests/conftest.py +++ b/spyder/plugins/ipythonconsole/tests/conftest.py @@ -343,15 +343,15 @@ def threads_condition(): @pytest.fixture -def mpl_rc_file(): +def mpl_rc_file(tmp_path): """Create matplotlibrc file""" - file_contents = """\ + file_contents = """ figure.dpi: 99 figure.figsize: 9, 9 figure.subplot.bottom: 0.9 font.size: 9 """ - rc_file = osp.join(TEMP_DIRECTORY, 'matplotlibrc') + rc_file = str(tmp_path / 'matplotlibrc') with open(rc_file, 'w') as f: f.write(file_contents) os.environ['MATPLOTLIBRC'] = rc_file diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index 6fb452bc1cf..6191a2110f6 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -1507,14 +1507,11 @@ def test_stderr_poll(ipyconsole, qtbot): # Wait for the poll qtbot.waitUntil( - lambda: "test_test" in ipyconsole.get_widget() - .get_focus_widget() - .toPlainText() + lambda: "test_test" + in ipyconsole.get_widget().get_focus_widget().toPlainText() ) assert ( - "test_test" in ipyconsole.get_widget() - .get_focus_widget() - .toPlainText() + "test_test" in ipyconsole.get_widget().get_focus_widget().toPlainText() ) # Write a second time, makes sure it is not duplicated with qtbot.waitSignal(shell.executed): @@ -1523,15 +1520,17 @@ def test_stderr_poll(ipyconsole, qtbot): # Wait for the poll qtbot.waitUntil( lambda: ipyconsole.get_widget() - .get_focus_widget() - .toPlainText() - .count("test_test") == 2 + .get_focus_widget() + .toPlainText() + .count("test_test") + == 2 ) assert ( ipyconsole.get_widget() - .get_focus_widget() - .toPlainText() - .count("test_test") == 2 + .get_focus_widget() + .toPlainText() + .count("test_test") + == 2 ) @@ -1544,10 +1543,9 @@ def test_stdout_poll(ipyconsole, qtbot): # Wait for the poll qtbot.waitUntil( - lambda: "test_test" in ipyconsole.get_widget() - .get_focus_widget() - .toPlainText(), - timeout=5000 + lambda: "test_test" + in ipyconsole.get_widget().get_focus_widget().toPlainText(), + timeout=5000, ) @@ -1995,12 +1993,11 @@ def test_mpl_conf(ipyconsole, qtbot): mock.assert_called_once_with({'pylab/inline/bottom': 0.314}) -def test_rc_params(mpl_rc_file, ipyconsole, qtbot): +def test_matplotlib_rc_params(mpl_rc_file, ipyconsole, qtbot): """ - Test that rcParams are correctly set/reset when changing backends and - setting inline preferences. + Test that Matplotlib rcParams are correctly set/reset when changing + backends and setting inline preferences. """ - # import pdb; pdb.set_trace() rc_file = os.getenv('MATPLOTLIBRC') assert rc_file is not None assert osp.exists(rc_file) From 94d0aa102f5039422ce3e6d29460e91b3f6cd621 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 31 May 2024 11:48:16 -0700 Subject: [PATCH 8/9] git subrepo pull (merge) --branch=master --remote=https://github.com/spyder-ide/spyder-kernels.git --update --force external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "ea047533c" upstream: origin: "https://github.com/spyder-ide/spyder-kernels.git" branch: "master" commit: "ea047533c" git-subrepo: version: "0.4.6" origin: "???" commit: "???" --- external-deps/spyder-kernels/.gitrepo | 6 +- .../spyder_kernels/console/kernel.py | 140 ++++++++++-------- .../spyder_kernels/console/shell.py | 4 + 3 files changed, 86 insertions(+), 64 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 09b91543241..cc3a2068561 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git branch = master - commit = cf597289f1ee3f09fdda5a162228848fc3cf8453 - parent = ef54914c81fc81214493e08e646af44d7f82de35 + commit = ea047533c4907e4a1bb432c1efa15be84eca1593 + parent = bdda9ff2d218574e6ee6f350f93d95e5acc76b20 method = merge - cmdver = 0.4.5 + cmdver = 0.4.6 diff --git a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py index da595371d7a..925cf429052 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py @@ -25,7 +25,7 @@ # Third-party imports from ipykernel.ipkernel import IPythonKernel from ipykernel import get_connection_info -from traitlets.config.loader import LazyConfigValue +from traitlets.config.loader import Config, LazyConfigValue import zmq from zmq.utils.garbage import gc @@ -554,67 +554,41 @@ def set_matplotlib_conf(self, conf): fontsize_n = 'pylab/inline/fontsize' bottom_n = 'pylab/inline/bottom' bbox_inches_n = 'pylab/inline/bbox_inches' - inline_backend = 'inline' - - if pylab_autoload_n in conf or pylab_backend_n in conf: - self._set_mpl_backend( - conf.get(pylab_backend_n, inline_backend), - pylab=conf.get(pylab_autoload_n, False) - ) if figure_format_n in conf: - self._set_config_option( - 'InlineBackend.figure_format', - conf[figure_format_n] + self._set_inline_config_option( + 'figure_formats', conf[figure_format_n] ) + inline_rc = {} if resolution_n in conf: - self._set_mpl_inline_rc_config('figure.dpi', conf[resolution_n]) - - if width_n in conf and height_n in conf: - self._set_mpl_inline_rc_config( - 'figure.figsize', - (conf[width_n], conf[height_n]) + inline_rc.update({'figure.dpi': conf[resolution_n]}) + if width_n in conf or height_n in conf: + inline_rc.update( + {'figure.figsize': (conf[width_n], conf[height_n])} ) - if fontsize_n in conf: - self._set_mpl_inline_rc_config('font.size', conf[fontsize_n]) - + inline_rc.update({'font.size': conf[fontsize_n]}) if bottom_n in conf: - self._set_mpl_inline_rc_config( - 'figure.subplot.bottom', - conf[bottom_n] - ) + inline_rc.update({'figure.subplot.bottom': conf[bottom_n]}) - if bbox_inches_n in conf: - self.set_mpl_inline_bbox_inches(conf[bbox_inches_n]) + # Update Inline backend parameters, if available. + if inline_rc: + self._set_inline_config_option('rc', inline_rc) + if bbox_inches_n in conf: + bbox_inches = 'tight' if conf[bbox_inches_n] else None + self._set_inline_config_option( + 'print_figure_kwargs', {'bbox_inches': bbox_inches} + ) - def set_mpl_inline_bbox_inches(self, bbox_inches): - """ - Set inline print figure bbox inches. - - The change is done by updating the 'print_figure_kwargs' config dict. - """ - config = self.config - inline_config = ( - config['InlineBackend'] if 'InlineBackend' in config else {}) - print_figure_kwargs = ( - inline_config['print_figure_kwargs'] - if 'print_figure_kwargs' in inline_config else {}) - bbox_inches_dict = { - 'bbox_inches': 'tight' if bbox_inches else None} - print_figure_kwargs.update(bbox_inches_dict) - - # This seems to be necessary for newer versions of Traitlets because - # print_figure_kwargs doesn't return a dict. - if isinstance(print_figure_kwargs, LazyConfigValue): - figure_kwargs_dict = print_figure_kwargs.to_dict().get('update') - if figure_kwargs_dict: - print_figure_kwargs = figure_kwargs_dict - - self._set_config_option( - 'InlineBackend.print_figure_kwargs', print_figure_kwargs) + # Only update backend if it has changed or if autoloading pylab. + pylab_autoload_o = conf.get(pylab_autoload_n, False) + current_backend = self.get_matplotlib_backend() + pylab_backend_o = conf.get(pylab_backend_n, current_backend) + backend_changed = current_backend != pylab_backend_o + if pylab_autoload_o or backend_changed: + self._set_mpl_backend(pylab_backend_o, pylab_autoload_o) # -- For completions def set_jedi_completer(self, use_jedi): @@ -901,9 +875,9 @@ def _set_mpl_backend(self, backend, pylab=False): return generic_error = ( - "\n" + "="*73 + "\n" + "\n" + "=" * 73 + "\n" "NOTE: The following error appeared when setting " - "your Matplotlib backend!!\n" + "="*73 + "\n\n" + "your Matplotlib backend!!\n" + "=" * 73 + "\n\n" "{0}" ) @@ -925,7 +899,7 @@ def _set_mpl_backend(self, backend, pylab=False): # trying to set a backend. See issue 5541 if "GUI eventloops" in str(err): previous_backend = matplotlib.get_backend() - if not backend in previous_backend.lower(): + if backend not in previous_backend.lower(): # Only inform about an error if the user selected backend # and the one set by Matplotlib are different. Else this # message is very confusing. @@ -958,7 +932,7 @@ def _set_config_option(self, option, value): Set config options using the %config magic. As parameters: - option: config option, for example 'InlineBackend.figure_format'. + option: config option, for example 'InlineBackend.figure_formats'. value: value of the option, for example 'SVG', 'Retina', etc. """ try: @@ -972,16 +946,60 @@ def _set_config_option(self, option, value): except Exception: pass - def _set_mpl_inline_rc_config(self, option, value): + def _set_inline_config_option(self, option, value): """ - Update any of the Matplolib rcParams given an option and value. + Update InlineBackend given an option and value. + + Parameters + ---------- + option: str + Configuration option. One of 'close_figures', 'figure_formats', + 'print_figure_kwargs', or 'rc'. + value: str | dict + Value of the option. """ + if ( + 'InlineBackend' in self.config + and option in self.config['InlineBackend'] + and isinstance(value, dict) + ): + self.config['InlineBackend'][option].update(value) + elif 'InlineBackend' in self.config: + self.config['InlineBackend'].update({option: value}) + else: + self.config.update({'InlineBackend': Config({option: value})}) + + value = self.config['InlineBackend'][option] + + if isinstance(value, LazyConfigValue): + value = value.to_dict().get('update') or value + + self._set_config_option(f'InlineBackend.{option}', value) + + if option == 'rc' and self.get_matplotlib_backend() == 'inline': + # Explicitly update rcParams if already in inline mode so that + # new settings are effective immediately. + try: + import matplotlib + matplotlib.rcParams.update(value) + except Exception: + pass + + def restore_rc_file_defaults(self): + """Restore inline rcParams to file defaults""" try: - from matplotlib import rcParams - rcParams[option] = value + import matplotlib except Exception: - # Needed in case matplolib isn't installed - pass + return + + if ( + 'InlineBackend' in self.config + and 'rc' in self.config['InlineBackend'] + ): + # Only restore keys that may have been set explicitly by + # _set_inline_config_option + for k in self.config['InlineBackend']['rc'].keys(): + matplotlib.rcParams[k] = matplotlib.rcParamsOrig[k] def set_sympy_forecolor(self, background_color='dark'): """Set SymPy forecolor depending on console background.""" diff --git a/external-deps/spyder-kernels/spyder_kernels/console/shell.py b/external-deps/spyder-kernels/spyder_kernels/console/shell.py index 5a9c1a29a40..d2d32a85f11 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/shell.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/shell.py @@ -89,6 +89,10 @@ def enable_matplotlib(self, gui=None): if gui is None or gui.lower() == "auto": gui = automatic_backend() + # Before activating the backend, restore to file default those + # InlineBackend settings that may have been set explicitly. + self.kernel.restore_rc_file_defaults() + enabled_gui, backend = super().enable_matplotlib(gui) # This is necessary for IPython 8.24+, which returns None after From 60c9af4b74637c01539b3df3ed5db882427ea969 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 31 May 2024 13:40:45 -0700 Subject: [PATCH 9/9] Use tmp_path fixture instead of TEMP_DIRECTORY constant --- spyder/plugins/ipythonconsole/tests/conftest.py | 1 - .../ipythonconsole/tests/test_ipythonconsole.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/spyder/plugins/ipythonconsole/tests/conftest.py b/spyder/plugins/ipythonconsole/tests/conftest.py index be9cf9ed3d9..3e2fb638f0e 100644 --- a/spyder/plugins/ipythonconsole/tests/conftest.py +++ b/spyder/plugins/ipythonconsole/tests/conftest.py @@ -35,7 +35,6 @@ # ---- Constants # ============================================================================= SHELL_TIMEOUT = 40000 if os.name == 'nt' else 20000 -TEMP_DIRECTORY = tempfile.gettempdir() NEW_DIR = 'new_workingdir' PY312_OR_GREATER = sys.version_info[:2] >= (3, 12) diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index 6191a2110f6..e28819f13ee 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -40,7 +40,7 @@ from spyder.plugins.help.tests.test_plugin import check_text from spyder.plugins.ipythonconsole.tests.conftest import ( get_conda_test_env, get_console_background_color, get_console_font_color, - NEW_DIR, SHELL_TIMEOUT, TEMP_DIRECTORY, PY312_OR_GREATER) + NEW_DIR, SHELL_TIMEOUT, PY312_OR_GREATER) from spyder.plugins.ipythonconsole.widgets import ShellWidget from spyder.utils.conda import get_list_conda_envs @@ -390,18 +390,18 @@ def test_console_import_namespace(ipyconsole, qtbot): @flaky(max_runs=3) -def test_console_disambiguation(ipyconsole, qtbot): +def test_console_disambiguation(tmp_path, ipyconsole, qtbot): """Test the disambiguation of dedicated consoles.""" - # Create directories and file for TEMP_DIRECTORY/a/b/c.py - # and TEMP_DIRECTORY/a/d/c.py - dir_b = osp.join(TEMP_DIRECTORY, 'a', 'b') + # Create directories and file for tmp_path/a/b/c.py + # and tmp_path/a/d/c.py + dir_b = osp.join(tmp_path, 'a', 'b') filename_b = osp.join(dir_b, 'c.py') if not osp.isdir(dir_b): os.makedirs(dir_b) if not osp.isfile(filename_b): file_c = open(filename_b, 'w+') file_c.close() - dir_d = osp.join(TEMP_DIRECTORY, 'a', 'd') + dir_d = osp.join(tmp_path, 'a', 'd') filename_d = osp.join(dir_d, 'c.py') if not osp.isdir(dir_d): os.makedirs(dir_d)