From 437ebdf0e78f3b44af0a90670d9144dcd9f27324 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 17 May 2024 15:57:46 -0700 Subject: [PATCH 01/38] Update InlineBackend.rc IPython configuration settings and toggle backend if necessary. Do not directly set rcParams or re-apply inline backend since doing either while in inline mode will prevent restoring rcParams when toggling to interactive mode. Rather, toggle the backend to interactive and back to inline. Note that when adding InlineBackend to kernel.config, it must be a Config object. --- spyder_kernels/console/kernel.py | 59 ++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index da595371..7ac23e10 100644 --- a/spyder_kernels/console/kernel.py +++ b/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,13 +554,6 @@ 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( @@ -589,6 +582,30 @@ def set_matplotlib_conf(self, conf): if bbox_inches_n in conf: self.set_mpl_inline_bbox_inches(conf[bbox_inches_n]) + # To update rcParams in inline mode we can either do so directly or + # re-assert inline mode. However, either will prevent restoring + # rcParams when toggling to interactive mode. The workaround is to + # first toggle to interactive mode, then back to inline. This updates + # the rcParams and restores rcParams when switching to interactive. + interactive_backend = self.get_mpl_interactive_backend() + current_backend = self.get_matplotlib_backend() + pylab_changed = pylab_autoload_n in conf + backend_changed = pylab_backend_n in conf + if ( + current_backend == 'inline' + and not backend_changed + and interactive_backend not in ('inline', -1) + ): + self._set_mpl_backend(interactive_backend) + if ( + current_backend == 'inline' # toggle back to inline for rcParams + or pylab_changed + or backend_changed + ): + self._set_mpl_backend( + conf.get(pylab_backend_n, current_backend), + pylab=conf.get(pylab_autoload_n, False) + ) def set_mpl_inline_bbox_inches(self, bbox_inches): """ @@ -974,14 +991,26 @@ def _set_config_option(self, option, value): def _set_mpl_inline_rc_config(self, option, value): """ - Update any of the Matplolib rcParams given an option and value. + Update InlineBackend.rc given an option and value. """ - try: - from matplotlib import rcParams - rcParams[option] = value - except Exception: - # Needed in case matplolib isn't installed - pass + _rc = {option: value} + if ( + 'InlineBackend' in self.config + and 'rc' in self.config['InlineBackend'] + ): + self.config['InlineBackend']['rc'].update({_rc}) + elif 'InlineBackend' in self.config: + self.config['InlineBackend'].update({'rc': _rc}) + else: + self.config.update({'InlineBackend': Config({'rc': _rc})}) + rc = self.config['InlineBackend']['rc'] + + # This seems to be necessary for newer versions of Traitlets because + # print_figure_kwargs doesn't return a dict. + if isinstance(rc, LazyConfigValue): + rc = rc.to_dict().get('update') or rc + + self._set_config_option('InlineBackend.rc', rc) def set_sympy_forecolor(self, background_color='dark'): """Set SymPy forecolor depending on console background.""" From c427941aff0c179ed373e413be4b2341212d14b8 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 24 May 2024 20:39:41 -0700 Subject: [PATCH 02/38] Merge set_mpl_inline_bbox_inches and _set_mpl_inline_rc_config into one method _set_inline_config_option. --- spyder_kernels/console/kernel.py | 87 +++++++++++--------------------- 1 file changed, 30 insertions(+), 57 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index 7ac23e10..3bae9be8 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -556,31 +556,27 @@ def set_matplotlib_conf(self, conf): bbox_inches_n = 'pylab/inline/bbox_inches' if figure_format_n in conf: - self._set_config_option( - 'InlineBackend.figure_format', - conf[figure_format_n] + self._set_inline_config_option( + 'figure_format', conf[figure_format_n] ) + 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]) - ) - + rc.update({'figure.dpi': conf[resolution_n]}) + if width_n in conf or height_n in conf: + 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]) - + rc.update({'font.size': conf[fontsize_n]}) if bottom_n in conf: - self._set_mpl_inline_rc_config( - 'figure.subplot.bottom', - conf[bottom_n] - ) + rc.update({'figure.subplot.bottom': conf[bottom_n]}) + if rc: + self._set_inline_config_option('rc', rc) if bbox_inches_n in conf: - self.set_mpl_inline_bbox_inches(conf[bbox_inches_n]) + bbox_inches = 'tight' if conf[bbox_inches_n] else None + self._set_inline_config_option( + 'print_figure_kwargs', {'bbox_inches': bbox_inches} + ) # To update rcParams in inline mode we can either do so directly or # re-assert inline mode. However, either will prevent restoring @@ -607,32 +603,6 @@ def set_matplotlib_conf(self, conf): pylab=conf.get(pylab_autoload_n, False) ) - 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) - # -- For completions def set_jedi_completer(self, use_jedi): """Enable/Disable jedi as the completer for the kernel.""" @@ -989,28 +959,31 @@ 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 InlineBackend.rc given an option and value. + Update InlineBackend given an option and value. + + As parameters: + option: config option; one of 'close_figures', 'figure_formats', + 'print_figure_kwargs', or 'rc'. + value: value of the option """ - _rc = {option: value} if ( 'InlineBackend' in self.config - and 'rc' in self.config['InlineBackend'] + and option in self.config['InlineBackend'] + and isinstance(value, dict) ): - self.config['InlineBackend']['rc'].update({_rc}) + self.config['InlineBackend'][option].update(value) elif 'InlineBackend' in self.config: - self.config['InlineBackend'].update({'rc': _rc}) + self.config['InlineBackend'].update({option: value}) else: - self.config.update({'InlineBackend': Config({'rc': _rc})}) - rc = self.config['InlineBackend']['rc'] + self.config.update({'InlineBackend': Config({option: value})}) + value = self.config['InlineBackend'][option] - # This seems to be necessary for newer versions of Traitlets because - # print_figure_kwargs doesn't return a dict. - if isinstance(rc, LazyConfigValue): - rc = rc.to_dict().get('update') or rc + if isinstance(value, LazyConfigValue): + value = value.to_dict().get('update') or value - self._set_config_option('InlineBackend.rc', rc) + self._set_config_option(f'InlineBackend.{option}', value) def set_sympy_forecolor(self, background_color='dark'): """Set SymPy forecolor depending on console background.""" From 097260b30937180a7c3ea51fa9d1c47e83a63779 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Thu, 16 May 2024 11:45:09 -0700 Subject: [PATCH 03/38] Use figure_formats instead of deprecated figure_format --- spyder_kernels/console/kernel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index 3bae9be8..f630bbe3 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -557,7 +557,7 @@ def set_matplotlib_conf(self, conf): if figure_format_n in conf: self._set_inline_config_option( - 'figure_format', conf[figure_format_n] + 'figure_formats', conf[figure_format_n] ) rc = {} @@ -945,7 +945,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: From d756c9aac0e6af31fff0da3cd9484b67112f34a3 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Thu, 16 May 2024 10:51:07 -0700 Subject: [PATCH 04/38] PEP8 --- spyder_kernels/console/kernel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index f630bbe3..06dde085 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -888,9 +888,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}" ) @@ -912,7 +912,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. From db3769c731685e7c9a929422eaec352cab3080e2 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Fri, 17 May 2024 12:52:59 -0700 Subject: [PATCH 05/38] Conditions that current backend is inline should also include whether there were any rc changes. Rather than condition on pylab autoload in the configuration, condition on it being true. --- spyder_kernels/console/kernel.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index 06dde085..d3e9219f 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -585,23 +585,21 @@ def set_matplotlib_conf(self, conf): # the rcParams and restores rcParams when switching to interactive. interactive_backend = self.get_mpl_interactive_backend() current_backend = self.get_matplotlib_backend() - pylab_changed = pylab_autoload_n in conf - backend_changed = pylab_backend_n in conf + pylab_autoload_o = conf.get(pylab_autoload_n, False) + pylab_backend_o = conf.get(pylab_backend_n, current_backend) + backend_changed = current_backend != pylab_backend_o if ( - current_backend == 'inline' + current_backend == 'inline' and rc and not backend_changed and interactive_backend not in ('inline', -1) ): self._set_mpl_backend(interactive_backend) if ( - current_backend == 'inline' # toggle back to inline for rcParams - or pylab_changed + (current_backend == 'inline' and rc) # toggle back to inline + or pylab_autoload_o or backend_changed ): - self._set_mpl_backend( - conf.get(pylab_backend_n, current_backend), - pylab=conf.get(pylab_autoload_n, False) - ) + self._set_mpl_backend(pylab_backend_o, pylab_autoload_o) # -- For completions def set_jedi_completer(self, use_jedi): From 7b8dc11d487b7f94cf94a9546ea29b65c64e3c84 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 06/38] 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/spyder_kernels/console/start.py b/spyder_kernels/console/start.py index eb910305..600b656c 100644 --- a/spyder_kernels/console/start.py +++ b/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 83ce61e2e8b82251e3f95ce45f01cf3a1d43e521 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Tue, 28 May 2024 12:02:31 -0700 Subject: [PATCH 07/38] Apply suggestions from code review Co-authored-by: Carlos Cordoba --- spyder_kernels/console/kernel.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index d3e9219f..a41ab6ca 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -560,17 +560,21 @@ def set_matplotlib_conf(self, conf): 'figure_formats', conf[figure_format_n] ) - rc = {} + inline_rc = {} if resolution_n in conf: - rc.update({'figure.dpi': conf[resolution_n]}) + inline_rc.update({'figure.dpi': conf[resolution_n]}) if width_n in conf or height_n in conf: - rc.update({'figure.figsize': (conf[width_n], conf[height_n])}) + inline_rc.update( + {'figure.figsize': (conf[width_n], conf[height_n])} + ) if fontsize_n in conf: - rc.update({'font.size': conf[fontsize_n]}) + inline_rc.update({'font.size': conf[fontsize_n]}) if bottom_n in conf: - rc.update({'figure.subplot.bottom': conf[bottom_n]}) - if rc: - self._set_inline_config_option('rc', rc) + inline_rc.update({'figure.subplot.bottom': conf[bottom_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 @@ -589,13 +593,13 @@ def set_matplotlib_conf(self, conf): pylab_backend_o = conf.get(pylab_backend_n, current_backend) backend_changed = current_backend != pylab_backend_o if ( - current_backend == 'inline' and rc + current_backend == 'inline' and inline_rc and not backend_changed and interactive_backend not in ('inline', -1) ): self._set_mpl_backend(interactive_backend) if ( - (current_backend == 'inline' and rc) # toggle back to inline + (current_backend == 'inline' and inline_rc) # toggle back to inline or pylab_autoload_o or backend_changed ): @@ -976,6 +980,7 @@ def _set_inline_config_option(self, option, value): self.config['InlineBackend'].update({option: value}) else: self.config.update({'InlineBackend': Config({option: value})}) + value = self.config['InlineBackend'][option] if isinstance(value, LazyConfigValue): From 37f7ec035768f9638298c6ffa72ad01ff033a346 Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Wed, 29 May 2024 13:06:54 -0700 Subject: [PATCH 08/38] Remove hack to apply rcParams while in inline mode. Apply rcParams explicitly while in inline mode. Always reset rcParams to file defaults before applying backend. This will ensure correct rcParams in interactive backends if rcParams are explicitly set in inline mode. When changing to inline mode, InlineBackend settings will be applied to rcParams. --- spyder_kernels/console/kernel.py | 43 +++++++++++++++++++------------- spyder_kernels/console/shell.py | 4 +++ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index a41ab6ca..e6103b30 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -582,27 +582,12 @@ def set_matplotlib_conf(self, conf): 'print_figure_kwargs', {'bbox_inches': bbox_inches} ) - # To update rcParams in inline mode we can either do so directly or - # re-assert inline mode. However, either will prevent restoring - # rcParams when toggling to interactive mode. The workaround is to - # first toggle to interactive mode, then back to inline. This updates - # the rcParams and restores rcParams when switching to interactive. - interactive_backend = self.get_mpl_interactive_backend() - current_backend = self.get_matplotlib_backend() + # 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 ( - current_backend == 'inline' and inline_rc - and not backend_changed - and interactive_backend not in ('inline', -1) - ): - self._set_mpl_backend(interactive_backend) - if ( - (current_backend == 'inline' and inline_rc) # toggle back to inline - or pylab_autoload_o - or backend_changed - ): + if pylab_autoload_o or backend_changed: self._set_mpl_backend(pylab_backend_o, pylab_autoload_o) # -- For completions @@ -988,6 +973,28 @@ def _set_inline_config_option(self, option, 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. + import matplotlib + matplotlib.rcParams.update(value) + + def _restore_rc_file_defaults(self): + """Restore inline rcParams to file defaults""" + try: + import matplotlib + except Exception: + 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.""" if self.shell.special != "sympy": diff --git a/spyder_kernels/console/shell.py b/spyder_kernels/console/shell.py index 5a9c1a29..ad93ad7e 100644 --- a/spyder_kernels/console/shell.py +++ b/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 75fa13bbc67ed0a9371b12ea062b3b23ab7bbbee Mon Sep 17 00:00:00 2001 From: Ryan Clary <9618975+mrclary@users.noreply.github.com> Date: Wed, 29 May 2024 20:45:22 -0700 Subject: [PATCH 09/38] Apply suggestions from code review Co-authored-by: Carlos Cordoba --- spyder_kernels/console/kernel.py | 20 +++++++++++++------- spyder_kernels/console/shell.py | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index e6103b30..925cf429 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -950,10 +950,13 @@ def _set_inline_config_option(self, option, value): """ Update InlineBackend given an option and value. - As parameters: - option: config option; one of 'close_figures', 'figure_formats', - 'print_figure_kwargs', or 'rc'. - value: value of the option + 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 @@ -976,10 +979,13 @@ def _set_inline_config_option(self, 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. - import matplotlib - matplotlib.rcParams.update(value) + try: + import matplotlib + matplotlib.rcParams.update(value) + except Exception: + pass - def _restore_rc_file_defaults(self): + def restore_rc_file_defaults(self): """Restore inline rcParams to file defaults""" try: import matplotlib diff --git a/spyder_kernels/console/shell.py b/spyder_kernels/console/shell.py index ad93ad7e..d2d32a85 100644 --- a/spyder_kernels/console/shell.py +++ b/spyder_kernels/console/shell.py @@ -91,7 +91,7 @@ def enable_matplotlib(self, gui=None): # Before activating the backend, restore to file default those # InlineBackend settings that may have been set explicitly. - self.kernel._restore_rc_file_defaults() + self.kernel.restore_rc_file_defaults() enabled_gui, backend = super().enable_matplotlib(gui) From 53a2b90d8db354d82b8bcb8e69f37591bda84186 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 11 Jun 2024 11:02:17 -0500 Subject: [PATCH 10/38] Update Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb2216b..08cb1928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # History of changes +## Version 2.5.2 (2024-06-11) + +### Pull Requests Merged + +* [PR 489](https://github.com/spyder-ide/spyder-kernels/pull/489) - PR: Fix detecting Matplotlib backend for its 3.9.0 version, by [@mrclary](https://github.com/mrclary) + +In this release 1 pull request was closed. + + +---- + + ## Version 2.5.1 (2024-02-28) ### Pull Requests Merged From cebe9b57cf3e5680f75b6772085f499aea3da681 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 11 Jun 2024 11:03:30 -0500 Subject: [PATCH 11/38] Release 2.5.2 --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index f4932aee..45549c9e 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (2, 6, 0, 'dev0') +VERSION_INFO = (2, 5, 2) __version__ = '.'.join(map(str, VERSION_INFO)) From 649a619879a872b954f1dff69ebe18876b1fecad Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 11 Jun 2024 11:06:17 -0500 Subject: [PATCH 12/38] Back to work --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 45549c9e..f4932aee 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (2, 5, 2) +VERSION_INFO = (2, 6, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO)) From 2a20ea9976258eb6e10c212be00ba23ced7d2e6d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 18 Jun 2024 11:48:43 -0500 Subject: [PATCH 13/38] Update Changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18887c4a..ae743032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # History of changes +## Version 3.0.0b7 (2024-06-18) + +### Issues Closed + +* [Issue 491](https://github.com/spyder-ide/spyder-kernels/issues/491) - Debugger misses breakpoint when a file can have several canonic paths ([PR 490](https://github.com/spyder-ide/spyder-kernels/pull/490) by [@impact27](https://github.com/impact27)) +* [Issue 468](https://github.com/spyder-ide/spyder-kernels/issues/468) - Debugger not working for environments with different Python versions ([PR 492](https://github.com/spyder-ide/spyder-kernels/pull/492) by [@impact27](https://github.com/impact27)) + +In this release 2 issues were closed. + +### Pull Requests Merged + +* [PR 492](https://github.com/spyder-ide/spyder-kernels/pull/492) - PR: Enable comms to work across different Python versions, by [@impact27](https://github.com/impact27) ([468](https://github.com/spyder-ide/spyder-kernels/issues/468)) +* [PR 490](https://github.com/spyder-ide/spyder-kernels/pull/490) - PR: Use inodes for single canonic file path (Debugger), by [@impact27](https://github.com/impact27) ([491](https://github.com/spyder-ide/spyder-kernels/issues/491)) +* [PR 487](https://github.com/spyder-ide/spyder-kernels/pull/487) - PR: Fix issue where Spyder's inline graphics preferences were not applied, by [@mrclary](https://github.com/mrclary) + +In this release 3 pull requests were closed. + +---- + ## Version 3.0.0b6 (2024-05-15) ### Issues Closed From 220a75d9500c91dde1557202a646452677ec2e6f Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 18 Jun 2024 12:45:03 -0500 Subject: [PATCH 14/38] Release 3.0.0b7 --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 788cf5f4..00f6d06c 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, 0, 'dev0') +VERSION_INFO = (3, 0, '0b7') __version__ = '.'.join(map(str, VERSION_INFO)) From 67954aef9106fccd327f3c803f081076c4b8364b Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 18 Jun 2024 12:47:48 -0500 Subject: [PATCH 15/38] Back to work --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 00f6d06c..788cf5f4 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, '0b7') +VERSION_INFO = (3, 0, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO)) From bbf65e19c6c041ecea8f66c5e81e454722e8e403 Mon Sep 17 00:00:00 2001 From: dalthviz <16781833+dalthviz@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:57:14 -0500 Subject: [PATCH 16/38] Add filtering logic for stream data --- spyder_kernels/console/outstream.py | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/spyder_kernels/console/outstream.py b/spyder_kernels/console/outstream.py index 0477ab29..2357320b 100644 --- a/spyder_kernels/console/outstream.py +++ b/spyder_kernels/console/outstream.py @@ -9,6 +9,8 @@ """ Custom Spyder Outstream class. """ +import os +import sys from ipykernel.iostream import OutStream @@ -20,3 +22,46 @@ def __init__(self, session, pub_thread, name, pipe=None, echo=None, *, watchfd=True): super().__init__(session, pub_thread, name, pipe, echo=echo, watchfd=watchfd, isatty=True) + + def _flush(self): + """This is where the actual send happens. + + _flush should generally be called in the IO thread, + unless the thread has been destroyed (e.g. forked subprocess). + + NOTE: Overrided method to be able to filter messages. + See spyder-ide/spyder#22181 + """ + self._flush_pending = False + self._subprocess_flush_pending = False + + if self.echo is not None: + try: + self.echo.flush() + except OSError as e: + if self.echo is not sys.__stderr__: + print(f"Flush failed: {e}", file=sys.__stderr__) + + for parent, data in self._flush_buffers(): + filter_messages = ["Parent poll failed."] + if data and not any([message in data for message in filter_messages]): + # FIXME: this disables Session's fork-safe check, + # since pub_thread is itself fork-safe. + # There should be a better way to do this. + self.session.pid = os.getpid() + content = {"name": self.name, "text": data} + msg = self.session.msg("stream", content, parent=parent) + + # Each transform either returns a new + # message or None. If None is returned, + # the message has been 'used' and we return. + for hook in self._hooks: + msg = hook(msg) + if msg is None: + return + + self.session.send( + self.pub_thread, + msg, + ident=self.topic, + ) From bd7495212e02c276f818bdf768829cb81716f87b Mon Sep 17 00:00:00 2001 From: dalthviz <16781833+dalthviz@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:39:02 -0500 Subject: [PATCH 17/38] Testing --- requirements/posix.txt | 1 + requirements/windows.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements/posix.txt b/requirements/posix.txt index 0c22fa57..e27732a1 100644 --- a/requirements/posix.txt +++ b/requirements/posix.txt @@ -5,3 +5,4 @@ jupyter_client>=7.4.9,<9 pyzmq>=24.0.0 wurlitzer>=1.0.3 pyxdg>=0.26 +setuptools<71.0 diff --git a/requirements/windows.txt b/requirements/windows.txt index 9d6370b6..9b8ed909 100644 --- a/requirements/windows.txt +++ b/requirements/windows.txt @@ -3,3 +3,4 @@ ipykernel>=6.29.3,<7 ipython>=8.12.2,<9 jupyter_client>=7.4.9,<9 pyzmq>=24.0.0 +setuptools<71.0 From 9e46b15c99adb74690561dcd58cf3a275d8ea227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Althviz=20Mor=C3=A9?= <16781833+dalthviz@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:02:43 -0500 Subject: [PATCH 18/38] Apply suggestions from code review Co-authored-by: Carlos Cordoba --- spyder_kernels/console/outstream.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spyder_kernels/console/outstream.py b/spyder_kernels/console/outstream.py index 2357320b..ea6c9ea2 100644 --- a/spyder_kernels/console/outstream.py +++ b/spyder_kernels/console/outstream.py @@ -43,7 +43,10 @@ def _flush(self): print(f"Flush failed: {e}", file=sys.__stderr__) for parent, data in self._flush_buffers(): + # Messages that will not be printed to the console. This allows us + # to deal with issues such as spyder-ide/spyder#22181 filter_messages = ["Parent poll failed."] + if data and not any([message in data for message in filter_messages]): # FIXME: this disables Session's fork-safe check, # since pub_thread is itself fork-safe. From 4ad9b7e02e29532480f14d96f48ad547249b1d58 Mon Sep 17 00:00:00 2001 From: dalthviz <16781833+dalthviz@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:58:51 -0500 Subject: [PATCH 19/38] Fix code style --- spyder_kernels/console/outstream.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spyder_kernels/console/outstream.py b/spyder_kernels/console/outstream.py index ea6c9ea2..78ce38b4 100644 --- a/spyder_kernels/console/outstream.py +++ b/spyder_kernels/console/outstream.py @@ -47,7 +47,9 @@ def _flush(self): # to deal with issues such as spyder-ide/spyder#22181 filter_messages = ["Parent poll failed."] - if data and not any([message in data for message in filter_messages]): + if data and not any( + [message in data for message in filter_messages] + ): # FIXME: this disables Session's fork-safe check, # since pub_thread is itself fork-safe. # There should be a better way to do this. From 11a2e2de1c2a13078715f59e324718ecde720fad Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 8 Aug 2024 10:22:32 -0500 Subject: [PATCH 20/38] Update Changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae743032..9d356fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # History of changes +## Version 3.0.0b8 (2024-08-08) + +### Pull Requests Merged + +* [PR 496](https://github.com/spyder-ide/spyder-kernels/pull/496) - PR: Add filtering logic for stream data and constraint `setuptools` version, by [@dalthviz](https://github.com/dalthviz) + +In this release 1 pull request was closed. + +---- + ## Version 3.0.0b7 (2024-06-18) ### Issues Closed From 608bc7b3b0bc539d5ed436a1f40778ed62e6b796 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 8 Aug 2024 10:24:01 -0500 Subject: [PATCH 21/38] Release 3.0.0b8 --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 788cf5f4..e1891d22 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, 0, 'dev0') +VERSION_INFO = (3, 0, '0b8') __version__ = '.'.join(map(str, VERSION_INFO)) From c0c785cb85a302c8659ab057038192640fdf90c0 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 8 Aug 2024 10:28:00 -0500 Subject: [PATCH 22/38] Back to work --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index e1891d22..788cf5f4 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, '0b8') +VERSION_INFO = (3, 0, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO)) From 0eee30376042e2c9ddfebc3f6d701d4546b4cfa1 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 22 Jul 2024 10:16:13 -0500 Subject: [PATCH 23/38] Add utilities to get info from Python environments Most of these functions were added to Spyder first but we need to have them here now. --- spyder_kernels/utils/pythonenv.py | 75 ++++++++++++++++++++ spyder_kernels/utils/tests/test_pythonenv.py | 66 +++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 spyder_kernels/utils/pythonenv.py create mode 100644 spyder_kernels/utils/tests/test_pythonenv.py diff --git a/spyder_kernels/utils/pythonenv.py b/spyder_kernels/utils/pythonenv.py new file mode 100644 index 00000000..fbcc59a6 --- /dev/null +++ b/spyder_kernels/utils/pythonenv.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +"""Utilities to get information about Python environments.""" + +# Standard library imports +import os +from pathlib import Path + + +def add_quotes(path): + """Return quotes if needed for spaces on path.""" + quotes = '"' if ' ' in path and '"' not in path else '' + return '{quotes}{path}{quotes}'.format(quotes=quotes, path=path) + + +def get_conda_env_path(pyexec, quote=False): + """ + Return the full path to the conda environment from a given python + executable. + + If `quote` is True, then quotes are added if spaces are found in the path. + """ + pyexec = pyexec.replace('\\', '/') + if os.name == 'nt': + conda_env = os.path.dirname(pyexec) + else: + conda_env = os.path.dirname(os.path.dirname(pyexec)) + + if quote: + conda_env = add_quotes(conda_env) + + return conda_env + + +def is_conda_env(prefix=None, pyexec=None): + """Check if prefix or python executable are in a conda environment.""" + if pyexec is not None: + pyexec = pyexec.replace('\\', '/') + + if (prefix is None and pyexec is None) or (prefix and pyexec): + raise ValueError('Only `prefix` or `pyexec` should be provided!') + + if pyexec and prefix is None: + prefix = get_conda_env_path(pyexec).replace('\\', '/') + + return os.path.exists(os.path.join(prefix, 'conda-meta')) + + +def is_pyenv_env(pyexec): + """Check if a python executable is a Pyenv environment.""" + path = Path(pyexec) + return "pyenv" in path.parts[:-1] + + +def get_env_dir(interpreter, only_dir=False): + """Get the environment directory from the interpreter executable.""" + path = Path(interpreter) + + if os.name == 'nt': + # This is enough for Conda and Pyenv envs + env_dir = path.parent + + # This is necessary for envs created with `python -m venv` + if env_dir.parts[-1].lower() == "scripts": + env_dir = path.parents[1] + else: + env_dir = path.parents[1] + + return env_dir.parts[-1] if only_dir else str(env_dir) diff --git a/spyder_kernels/utils/tests/test_pythonenv.py b/spyder_kernels/utils/tests/test_pythonenv.py new file mode 100644 index 00000000..025a7f40 --- /dev/null +++ b/spyder_kernels/utils/tests/test_pythonenv.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2009- Spyder Kernels Contributors +# +# Licensed under the terms of the MIT License +# (see spyder_kernels/__init__.py for details) +# ----------------------------------------------------------------------------- + +""" +Tests for utilities in the pythonenv module +""" + +# Standard library imports +import os + +# Third-party imports +import pytest + +# Local imports +from spyder_kernels.utils.pythonenv import ( + add_quotes, + get_conda_env_path, + get_env_dir, +) + + +if os.name == 'nt': + TEST_PYEXEC = 'c:/miniconda/envs/foobar/python.exe' +else: + TEST_PYEXEC = '/miniconda/envs/foobar/bin/python' + + +def test_add_quotes(): + output = add_quotes('/some path/with spaces') + assert output == '"/some path/with spaces"' + + output = add_quotes('/some-path/with-no-spaces') + assert output == '/some-path/with-no-spaces' + + +def test_get_conda_env_path(): + output = get_conda_env_path(TEST_PYEXEC) + if os.name == 'nt': + assert output == 'c:/miniconda/envs/foobar' + else: + assert output == '/miniconda/envs/foobar' + + +def test_get_env_dir(): + output_dir = get_env_dir(TEST_PYEXEC, only_dir=False) + if os.name == "nt": + assert output_dir == 'c:\\miniconda\\envs\\foobar' + else: + assert output_dir == '/miniconda/envs/foobar' + + output = get_env_dir(TEST_PYEXEC, only_dir=True) + assert output == "foobar" + + if os.name == "nt": + venv_pyexec = 'C:\\Miniconda3\\envs\\foobar\\Scripts\\python.exe' + output = get_env_dir(venv_pyexec, only_dir=True) + assert output == "foobar" + + +if __name__ == "__main__": + pytest.main() From a46d8f29c77e5eca602d09af7ce5cacaf5fcfe51 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 22 Jul 2024 11:32:21 -0500 Subject: [PATCH 24/38] Kernel: Add comm handler to get Python env info - This will be used from Spyder for a new status bar widget which will display this info. - Also add a test for it. --- spyder_kernels/console/kernel.py | 33 +++++++++++++++++++ .../console/tests/test_console_kernel.py | 26 +++++++++++++-- spyder_kernels/utils/pythonenv.py | 19 +++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index 925cf429..bc0a7bcb 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -35,6 +35,13 @@ from spyder_kernels.comms.frontendcomm import FrontendComm from spyder_kernels.comms.decorators import ( register_comm_handlers, comm_handler) +from spyder_kernels.utils.pythonenv import ( + get_env_dir, + is_conda_env, + is_pyenv_env, + PythonEnvInfo, + PythonEnvType, +) from spyder_kernels.utils.iofuncs import iofunctions from spyder_kernels.utils.mpl import automatic_backend, MPL_BACKENDS_TO_SPYDER from spyder_kernels.utils.nsview import ( @@ -81,6 +88,9 @@ def __init__(self, *args, **kwargs): # To track the interactive backend self.interactive_backend = None + # To save the python env info + self.pythonenv_info: PythonEnvInfo = {} + @property def kernel_info(self): # Used for checking correct version by spyder @@ -756,6 +766,29 @@ def update_syspath(self, path_dict, new_path_dict): else: os.environ.pop('PYTHONPATH', None) + @comm_handler + def get_pythonenv_info(self): + """Get the Python env info in which this kernel is installed.""" + # We only need to compute this once + if not self.pythonenv_info: + path = sys.executable.replace("pythonw.exe", "python.exe") + + if is_conda_env(pyexec=path): + env_type = PythonEnvType.Conda + elif is_pyenv_env(path): + env_type = PythonEnvType.PyEnv + else: + env_type = PythonEnvType.Custom + + self.pythonenv_info = PythonEnvInfo( + path=path, + env_type=env_type, + name=get_env_dir(path, only_dir=True), + py_version='.'.join([str(n) for n in sys.version_info[:3]]), + ) + + return self.pythonenv_info + # -- Private API --------------------------------------------------- # --- For the Variable Explorer def _get_len(self, var): diff --git a/spyder_kernels/console/tests/test_console_kernel.py b/spyder_kernels/console/tests/test_console_kernel.py index 60f240d3..c49f66b1 100644 --- a/spyder_kernels/console/tests/test_console_kernel.py +++ b/spyder_kernels/console/tests/test_console_kernel.py @@ -31,10 +31,11 @@ import numpy as np # Local imports +from spyder_kernels.comms.commbase import CommBase +from spyder_kernels.customize.spyderpdb import SpyderPdb from spyder_kernels.utils.iofuncs import iofunctions +from spyder_kernels.utils.pythonenv import PythonEnvType from spyder_kernels.utils.test_utils import get_kernel, get_log_text -from spyder_kernels.customize.spyderpdb import SpyderPdb -from spyder_kernels.comms.commbase import CommBase # ============================================================================= # Constants and utility functions @@ -1410,5 +1411,26 @@ def test_hard_link_pdb(tmpdir): assert pdb_obj.canonic(str(d)) == pdb_obj.canonic(str(hard_link)) +@pytest.mark.skipif(not os.environ.get('CI'), reason="Only works on CIs") +def test_get_pythonenv_info(kernel): + """Test the output we get from this method.""" + output = kernel.get_pythonenv_info() + assert output["path"] == sys.executable + + if os.environ.get('USE_CONDA'): + assert output["name"] == "test" + assert output["env_type"] == PythonEnvType.Conda + else: + assert output["env_type"] in [ + # This Custom here accounts for Linux packagers that run our tests + # in their CIs + PythonEnvType.Custom, + PythonEnvType.Conda, + ] + + # Check this key is present. Otherwise we'll break Spyder. + assert output["py_version"] + + if __name__ == "__main__": pytest.main() diff --git a/spyder_kernels/utils/pythonenv.py b/spyder_kernels/utils/pythonenv.py index fbcc59a6..0565e19b 100644 --- a/spyder_kernels/utils/pythonenv.py +++ b/spyder_kernels/utils/pythonenv.py @@ -9,8 +9,27 @@ """Utilities to get information about Python environments.""" # Standard library imports +from __future__ import annotations import os from pathlib import Path +from typing import TypedDict + + +class PythonEnvType: + """Enum with the different types of Python environments we can detect.""" + + Conda = "conda" + PyEnv = "pyenv" + Custom = "custom" # Nor Conda or Pyenv + + +class PythonEnvInfo(TypedDict): + """Schema for Python environment information.""" + + path: str + env_type: PythonEnvType + name: str + py_version: str def add_quotes(path): From abc045ff12f924c8ce0fa5e31a5695cc373988d0 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 22 Jul 2024 11:33:08 -0500 Subject: [PATCH 25/38] Testing: Remove unnecessary skip from test_get_interactive_backend Also, fix small linting and code issues --- .../console/tests/test_console_kernel.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/spyder_kernels/console/tests/test_console_kernel.py b/spyder_kernels/console/tests/test_console_kernel.py index c49f66b1..fd577af2 100644 --- a/spyder_kernels/console/tests/test_console_kernel.py +++ b/spyder_kernels/console/tests/test_console_kernel.py @@ -278,7 +278,7 @@ def test_get_namespace_view(kernel): """ Test the namespace view of the kernel. """ - execute = asyncio.run(kernel.do_execute('a = 1', True)) + asyncio.run(kernel.do_execute('a = 1', True)) nsview = repr(kernel.get_namespace_view()) assert "'a':" in nsview @@ -294,7 +294,7 @@ def test_get_namespace_view_filter_on(kernel, filter_on): """ Test the namespace view of the kernel with filters on and off. """ - execute = asyncio.run(kernel.do_execute('a = 1', True)) + asyncio.run(kernel.do_execute('a = 1', True)) asyncio.run(kernel.do_execute('TestFilterOff = 1', True)) settings = kernel.namespace_view_settings @@ -986,7 +986,7 @@ def test_namespaces_in_pdb(kernel): Test namespaces in pdb """ # Define get_ipython for timeit - get_ipython = lambda: kernel.shell + get_ipython = lambda: kernel.shell # noqa kernel.shell.user_ns["test"] = 0 pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() @@ -1062,7 +1062,7 @@ def test_functions_with_locals_in_pdb_2(kernel): This is another regression test for spyder-ide/spyder-kernels#345 """ - baba = 1 + baba = 1 # noqa pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() pdb_obj.curframe_locals = pdb_obj.curframe.f_locals @@ -1099,7 +1099,7 @@ def test_locals_globals_in_pdb(kernel): """ Test thal locals and globals work properly in Pdb. """ - a = 1 + a = 1 # noqa pdb_obj = SpyderPdb() pdb_obj.curframe = inspect.currentframe() pdb_obj.curframe_locals = pdb_obj.curframe.f_locals @@ -1141,10 +1141,8 @@ def test_locals_globals_in_pdb(kernel): @pytest.mark.parametrize("backend", [None, 'inline', 'tk', 'qt']) @pytest.mark.skipif( os.environ.get('USE_CONDA') != 'true', - reason="Doesn't work with pip packages") -@pytest.mark.skipif( - sys.version_info[:2] < (3, 9), - reason="Too flaky in Python 3.8 and doesn't work in older versions") + reason="Doesn't work with pip packages" +) def test_get_interactive_backend(backend): """ Test that we correctly get the interactive backend set in the kernel. @@ -1158,14 +1156,17 @@ def test_get_interactive_backend(backend): # Set backend if backend is not None: client.execute_interactive( - "%matplotlib {}".format(backend), timeout=TIMEOUT) + "%matplotlib {}".format(backend), timeout=TIMEOUT + ) client.execute_interactive( - "import time; time.sleep(.1)", timeout=TIMEOUT) + "import time; time.sleep(.1)", timeout=TIMEOUT + ) # Get backend code = "backend = get_ipython().kernel.get_mpl_interactive_backend()" reply = client.execute_interactive( - code, user_expressions={'output': 'backend'}, timeout=TIMEOUT) + code, user_expressions={'output': 'backend'}, timeout=TIMEOUT + ) # Get value obtained through user_expressions user_expressions = reply['content']['user_expressions'] @@ -1240,7 +1241,7 @@ def test_debug_namespace(tmpdir): d.write('def func():\n bb = "hello"\n breakpoint()\nfunc()') # Run code file `d` - msg_id = client.execute("%runfile {}".format(repr(str(d)))) + client.execute("%runfile {}".format(repr(str(d)))) # make sure that 'bb' returns 'hello' client.get_stdin_msg(timeout=TIMEOUT) @@ -1371,8 +1372,7 @@ def test_non_strings_in_locals(kernel): This is a regression test for issue spyder-ide/spyder#19145 """ - execute = asyncio.run(kernel.do_execute('locals().update({1:2})', True)) - + asyncio.run(kernel.do_execute('locals().update({1:2})', True)) nsview = repr(kernel.get_namespace_view()) assert "1:" in nsview @@ -1383,9 +1383,7 @@ def test_django_settings(kernel): This is a regression test for issue spyder-ide/spyder#19516 """ - execute = asyncio.run(kernel.do_execute( - 'from django.conf import settings', True)) - + asyncio.run(kernel.do_execute('from django.conf import settings', True)) nsview = repr(kernel.get_namespace_view()) assert "'settings':" in nsview From 972f19642a3b12ac6902d61787de352d6ef1136c Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 12 Aug 2024 11:49:36 -0500 Subject: [PATCH 26/38] CI: Add missing system dependency on Linux - Some tests were failing without it. - Also, add code to enable remote SSH connection to all workflows and rename them for clarity. --- .github/workflows/linux-pip-tests.yml | 7 +++++-- .github/workflows/linux-tests.yml | 7 +++++-- .github/workflows/macos-tests.yml | 5 ++++- .github/workflows/windows-tests.yml | 5 ++++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/linux-pip-tests.yml b/.github/workflows/linux-pip-tests.yml index e88b9e6f..e8d0de90 100644 --- a/.github/workflows/linux-pip-tests.yml +++ b/.github/workflows/linux-pip-tests.yml @@ -16,7 +16,7 @@ concurrency: jobs: linux: - name: Py${{ matrix.PYTHON_VERSION }} + name: Linux (pip) - Py${{ matrix.PYTHON_VERSION }} runs-on: ubuntu-latest env: CI: True @@ -34,7 +34,7 @@ jobs: - name: Install System Packages run: | sudo apt-get update - sudo apt-get install libegl1-mesa + sudo apt-get install libegl1-mesa libopengl0 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: @@ -51,6 +51,9 @@ jobs: run: | conda info conda list + # - name: Setup Remote SSH Connection + # uses: mxschmitt/action-tmate@v3 + # timeout-minutes: 60 - name: Run tests shell: bash -l {0} run: | diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml index f0900308..27d22081 100644 --- a/.github/workflows/linux-tests.yml +++ b/.github/workflows/linux-tests.yml @@ -16,7 +16,7 @@ concurrency: jobs: linux: - name: Py${{ matrix.PYTHON_VERSION }} + name: Linux - Py${{ matrix.PYTHON_VERSION }} runs-on: ubuntu-latest env: CI: True @@ -34,7 +34,7 @@ jobs: - name: Install System Packages run: | sudo apt-get update - sudo apt-get install libegl1-mesa + sudo apt-get install libegl1-mesa libopengl0 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: @@ -59,6 +59,9 @@ jobs: run: | conda info conda list + # - name: Setup Remote SSH Connection + # uses: mxschmitt/action-tmate@v3 + # timeout-minutes: 60 - name: Run tests shell: bash -l {0} run: | diff --git a/.github/workflows/macos-tests.yml b/.github/workflows/macos-tests.yml index 819f9a1e..98eb2780 100644 --- a/.github/workflows/macos-tests.yml +++ b/.github/workflows/macos-tests.yml @@ -16,7 +16,7 @@ concurrency: jobs: macos: - name: Py${{ matrix.PYTHON_VERSION }} + name: macOS - Py${{ matrix.PYTHON_VERSION }} runs-on: macos-latest env: CI: True @@ -55,6 +55,9 @@ jobs: run: | conda info conda list + # - name: Setup Remote SSH Connection + # uses: mxschmitt/action-tmate@v3 + # timeout-minutes: 60 - name: Run tests shell: bash -l {0} run: | diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index b402cb71..18afee46 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -16,7 +16,7 @@ concurrency: jobs: windows: - name: Py${{ matrix.PYTHON_VERSION }} + name: Windows - Py${{ matrix.PYTHON_VERSION }} runs-on: windows-latest env: CI: True @@ -55,6 +55,9 @@ jobs: run: | conda info conda list + # - name: Setup Remote SSH Connection + # uses: mxschmitt/action-tmate@v3 + # timeout-minutes: 60 - name: Run tests shell: bash -l {0} run: | From 4189fbf5b474c4e6697cc068b2cbb69901dd2053 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 15 Aug 2024 21:06:52 -0500 Subject: [PATCH 27/38] Kernel: Add more entries to Python env info - That extra info is necessary to build the associated console banner. - Expand tests to cover that. --- spyder_kernels/console/kernel.py | 9 ++++++++- spyder_kernels/console/tests/test_console_kernel.py | 9 ++++++--- spyder_kernels/utils/pythonenv.py | 6 +++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py index bc0a7bcb..ad16c084 100644 --- a/spyder_kernels/console/kernel.py +++ b/spyder_kernels/console/kernel.py @@ -25,6 +25,7 @@ # Third-party imports from ipykernel.ipkernel import IPythonKernel from ipykernel import get_connection_info +from IPython.core import release as ipython_release from traitlets.config.loader import Config, LazyConfigValue import zmq from zmq.utils.garbage import gc @@ -784,7 +785,13 @@ def get_pythonenv_info(self): path=path, env_type=env_type, name=get_env_dir(path, only_dir=True), - py_version='.'.join([str(n) for n in sys.version_info[:3]]), + python_version=".".join( + [str(n) for n in sys.version_info[:3]] + ), + # These keys are necessary to build the console banner in + # Spyder + ipython_version=ipython_release.version, + sys_version=sys.version, ) return self.pythonenv_info diff --git a/spyder_kernels/console/tests/test_console_kernel.py b/spyder_kernels/console/tests/test_console_kernel.py index fd577af2..f6f58c39 100644 --- a/spyder_kernels/console/tests/test_console_kernel.py +++ b/spyder_kernels/console/tests/test_console_kernel.py @@ -24,11 +24,12 @@ from collections import namedtuple # Test imports -import pytest from flaky import flaky +from IPython.core import release as ipython_release from jupyter_core import paths from jupyter_client import BlockingKernelClient import numpy as np +import pytest # Local imports from spyder_kernels.comms.commbase import CommBase @@ -1426,8 +1427,10 @@ def test_get_pythonenv_info(kernel): PythonEnvType.Conda, ] - # Check this key is present. Otherwise we'll break Spyder. - assert output["py_version"] + # Check these keys are present. Otherwise we'll break Spyder. + assert output["python_version"] == sys.version.split()[0] + assert output["ipython_version"] == ipython_release.version + assert output["sys_version"] == sys.version if __name__ == "__main__": diff --git a/spyder_kernels/utils/pythonenv.py b/spyder_kernels/utils/pythonenv.py index 0565e19b..06e90811 100644 --- a/spyder_kernels/utils/pythonenv.py +++ b/spyder_kernels/utils/pythonenv.py @@ -29,7 +29,11 @@ class PythonEnvInfo(TypedDict): path: str env_type: PythonEnvType name: str - py_version: str + python_version: str + + # These keys are necessary to build the console banner in Spyder + ipython_version: str + sys_version: str def add_quotes(path): From c09aa621bb377c79430900d92275963a171a262d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 17 Aug 2024 20:59:56 -0500 Subject: [PATCH 28/38] Comms: Skip more benign messages --- spyder_kernels/comms/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spyder_kernels/comms/utils.py b/spyder_kernels/comms/utils.py index 00332349..a1c73c51 100644 --- a/spyder_kernels/comms/utils.py +++ b/spyder_kernels/comms/utils.py @@ -59,7 +59,10 @@ def is_benign_message(self, message): "Warning: Cannot change to a different GUI toolkit", "%pylab is deprecated", "Populating the interactive namespace", - "\n" + "\n", + # Fixes spyder-ide/spyder#21652 + "WARNING", + "Active device does not have an attribute", ] return any([msg in message for msg in benign_messages]) From bccf07373fca8f535b7771eb23ab4316c3ace49c Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 21 Aug 2024 12:12:55 -0500 Subject: [PATCH 29/38] Update Changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d356fc4..458b9b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # History of changes +## Version 3.0.0b9 (2024-08-21) + +### Pull Requests Merged + +* [PR 495](https://github.com/spyder-ide/spyder-kernels/pull/495) - PR: Add comm handler to get information about the Python environment associated to the kernel, by [@ccordoba12](https://github.com/ccordoba12) + +In this release 1 pull request was closed. + +---- + ## Version 3.0.0b8 (2024-08-08) ### Pull Requests Merged From fcd536c13dde6fd5bb78620eb506564953dcc265 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 21 Aug 2024 12:14:35 -0500 Subject: [PATCH 30/38] Release 3.0.0b9 --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 788cf5f4..040022c2 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, 0, 'dev0') +VERSION_INFO = (3, 0, '0b9') __version__ = '.'.join(map(str, VERSION_INFO)) From 4e60ca9ebedd0c7fbabce84b4c75b09b929a0e87 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 21 Aug 2024 12:18:19 -0500 Subject: [PATCH 31/38] Back to work --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 040022c2..788cf5f4 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, '0b9') +VERSION_INFO = (3, 0, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO)) From 3e5998a1b4ed26efcffeb8b1bdaece847b4286f1 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 25 Aug 2024 15:00:14 -0500 Subject: [PATCH 32/38] Remove TMPDIR env var in case it was set by Spyder --- spyder_kernels/customize/spydercustomize.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/spyder_kernels/customize/spydercustomize.py b/spyder_kernels/customize/spydercustomize.py index 99c20898..2ac975ae 100644 --- a/spyder_kernels/customize/spydercustomize.py +++ b/spyder_kernels/customize/spydercustomize.py @@ -11,7 +11,6 @@ # Spyder consoles sitecustomize # -import logging import os import pdb import sys @@ -239,7 +238,7 @@ def _patched_preparation_data(name): # ============================================================================= -# os adjustments +# OS adjustments # ============================================================================= # This is necessary to have better support for Rich and Colorama. def _patched_get_terminal_size(fd=None): @@ -253,6 +252,17 @@ def _patched_get_terminal_size(fd=None): # ============================================================================= pdb.Pdb = SpyderPdb + +# ============================================================================= +# Remove TMPDIR env var in case it was set by Spyder +# ============================================================================= +# See spyder-ide/spyder#22382 for the details. +try: + os.environ.pop("TMPDIR") +except KeyError: + pass + + # ============================================================================= # PYTHONPATH and sys.path Adjustments # ============================================================================= From b6ffbb42b5b531cfea9c0a9fedc94f9548c44f9e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 28 Aug 2024 22:18:08 -0500 Subject: [PATCH 33/38] Restore TMPDIR env var if it was available on the Spyder side --- spyder_kernels/customize/spydercustomize.py | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/spyder_kernels/customize/spydercustomize.py b/spyder_kernels/customize/spydercustomize.py index 2ac975ae..f7a1ddd0 100644 --- a/spyder_kernels/customize/spydercustomize.py +++ b/spyder_kernels/customize/spydercustomize.py @@ -256,12 +256,25 @@ def _patched_get_terminal_size(fd=None): # ============================================================================= # Remove TMPDIR env var in case it was set by Spyder # ============================================================================= -# See spyder-ide/spyder#22382 for the details. -try: - os.environ.pop("TMPDIR") -except KeyError: - pass - +# See spyder-ide/spyder#22382 and spyder-ide/spyder#22394 for the details. +def restore_tmpdir(): + # This check is necessary for external/remote kernels because SPY_TMPDIR + # is not available for them. + if os.environ.get("SPY_TMPDIR") is not None: + spy_tmpdir = os.environ.get("SPY_TMPDIR") + + if spy_tmpdir != "": + # This means TMPDIR was available in the system when the kernel + # started, so we need to restore it. + os.environ.update({"TMPDIR": spy_tmpdir}) + else: + # Otherwise, we simply need to remove it + try: + os.environ.pop("TMPDIR") + except KeyError: + pass + +restore_tmpdir() # ============================================================================= # PYTHONPATH and sys.path Adjustments From a776745fd9536926c0d1743baa5a93f5eb7d5493 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 29 Aug 2024 10:06:19 -0500 Subject: [PATCH 34/38] Update release instructions --- RELEASE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index aa70aa14..c2aab89f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,9 +2,9 @@ To release a new version of spyder-kernels on PyPI: * Close the respective milestone on Github -* git checkout 2.x +* git checkout 3.x -* git fetch upstream && get merge upstream/2.x +* git fetch upstream && get merge upstream/3.x * git clean -xfdi @@ -18,7 +18,7 @@ To release a new version of spyder-kernels on PyPI: * python setup.py bdist_wheel -* twine check dist/* +* twine check --strict dist/* * twine upload dist/* @@ -30,10 +30,10 @@ To release a new version of spyder-kernels on PyPI: * git checkout master -* git merge 2.x +* git merge 3.x * git push upstream master -* git push upstream 2.x +* git push upstream 3.x * git push upstream --tags From 6c6d5e36f001aa53381132056e604c5c97c83190 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 29 Aug 2024 10:22:00 -0500 Subject: [PATCH 35/38] Update Changelog --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 458b9b45..e5912824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # History of changes +## Version 3.0.0 (2024-08-29) + +### New features + +* Speed up debugger execution. +* Notify Spyder when Matplotlib backend changes. +* Use control channel for comms instead of a special one. +* Update variable explorer from the kernel. +* Simplify kernel configuration from Spyder. +* Add a `comm_handler` decorator. +* Transform `runfile`, `debugfile` and `runcell` commands to IPython magics. +* Add comm handlers to interrupt executions and enter the debugger after that. +* Publish Pdb stack frames to Spyder. +* Drop support for Python 2 and support Python 3.8+ + +### Pull Requests Merged + +* [PR 500](https://github.com/spyder-ide/spyder-kernels/pull/500) - PR: Restore `TMPDIR` env var if it was available on the Spyder side, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 498](https://github.com/spyder-ide/spyder-kernels/pull/498) - PR: Remove `TMPDIR` env var after initialization, by [@ccordoba12](https://github.com/ccordoba12) + +In this release 2 pull requests were closed. + +---- + ## Version 3.0.0b9 (2024-08-21) ### Pull Requests Merged From e484917ca28686099dfc0fffff7b2ac4f89dd2e8 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 29 Aug 2024 10:23:32 -0500 Subject: [PATCH 36/38] Release 3.0.0 --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 788cf5f4..20a68cfd 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, 0, 'dev0') +VERSION_INFO = (3, 0, 0) __version__ = '.'.join(map(str, VERSION_INFO)) From efae0389f4c2b9e50eec55cac3e22960119e5f03 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 29 Aug 2024 10:29:52 -0500 Subject: [PATCH 37/38] Back to work --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 20a68cfd..2d20379b 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, 0) +VERSION_INFO = (3, 1, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO)) From b9022db96115d98e4ad2ae320d94ceffa4a96b00 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 29 Aug 2024 10:33:43 -0500 Subject: [PATCH 38/38] Bump version to 4.0 --- spyder_kernels/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder_kernels/_version.py b/spyder_kernels/_version.py index 788cf5f4..1d3a5157 100644 --- a/spyder_kernels/_version.py +++ b/spyder_kernels/_version.py @@ -8,5 +8,5 @@ """Version File.""" -VERSION_INFO = (3, 0, 0, 'dev0') +VERSION_INFO = (4, 0, 0, 'dev0') __version__ = '.'.join(map(str, VERSION_INFO))