Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Improve validation for the right Spyder-kernels version #19074

Merged
merged 16 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions external-deps/spyder-kernels/.gitrepo

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions external-deps/spyder-kernels/spyder_kernels/console/shell.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -2387,5 +2387,39 @@ def test_varexp_magic_dbg_locals(ipyconsole, qtbot):
assert shell._control.toHtml().count('img src') == 1


@pytest.mark.skipif(os.name == 'nt', reason="Fails on windows")
def test_old_kernel_version(ipyconsole, qtbot):
"""
Check that an error is shown when an version of spyder-kernels is used.
"""
# Set a false _spyder_kernels_version in the cached kernel
w = ipyconsole.get_widget()
# create new client so PYTEST_CURRENT_TEST is the same
w.create_new_client()
# Wait until the window is fully up
shell = ipyconsole.get_current_shellwidget()
qtbot.waitUntil(
lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)

kc = w._cached_kernel_properties[-1][2]
kc.start_channels()
kc.execute("get_ipython()._spyder_kernels_version = '1.0.0'")
# Cleanup the kernel_client so it can be used again
kc.stop_channels()
kc._shell_channel = None
kc._iopub_channel = None
kc._stdin_channel = None
kc._hb_channel = None
kc._control_channel = None

# Create new client
w.create_new_client()
client = w.get_current_client()

# Make sure an error is shown
qtbot.waitUntil(lambda: client.error_text is not None)
assert 'version 1.0.0' in client.error_text


if __name__ == "__main__":
pytest.main()
12 changes: 10 additions & 2 deletions spyder/plugins/ipythonconsole/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@
from .kernelconnect import KernelConnectionDialog
from .restartdialog import ConsoleRestartDialog

# Constant for different types of completion available
COMPLETION_WIDGET_TYPE = {0: "droplist", 1: "ncurses", 2: "plain"}

# Required version of Spyder-kernels
# Define before ShellWidget
SPYDER_KERNELS_MIN_VERSION = '2.3.0'
SPYDER_KERNELS_MAX_VERSION = '2.4.0'
SPYDER_KERNELS_VERSION = (
f'>={SPYDER_KERNELS_MIN_VERSION};<{SPYDER_KERNELS_MAX_VERSION}')

# ShellWidget contains the other widgets and ClientWidget
# contains it
from .shell import ShellWidget
from .client import ClientWidget

# Constant for different types of completion available
COMPLETION_WIDGET_TYPE = {0: "droplist", 1: "ncurses", 2: "plain"}
17 changes: 6 additions & 11 deletions spyder/plugins/ipythonconsole/widgets/main_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
from spyder.plugins.ipythonconsole.utils.style import create_qss_style
from spyder.plugins.ipythonconsole.widgets import (
ClientWidget, ConsoleRestartDialog, COMPLETION_WIDGET_TYPE,
KernelConnectionDialog, PageControlWidget, ShellWidget)
KernelConnectionDialog, PageControlWidget, ShellWidget,
SPYDER_KERNELS_MIN_VERSION, SPYDER_KERNELS_MAX_VERSION,
SPYDER_KERNELS_VERSION)
from spyder.py3compat import PY38_OR_MORE
from spyder.utils import encoding, programs, sourcecode
from spyder.utils.misc import get_error_match, remove_backslashes
Expand All @@ -58,10 +60,6 @@
# ---- Constants
# =============================================================================
MAIN_BG_COLOR = QStylePalette.COLOR_BACKGROUND_1
SPYDER_KERNELS_MIN_VERSION = '2.3.0'
SPYDER_KERNELS_MAX_VERSION = '2.4.0'
SPYDER_KERNELS_VERSION = (
f'>={SPYDER_KERNELS_MIN_VERSION};<{SPYDER_KERNELS_MAX_VERSION}')
SPYDER_KERNELS_VERSION_MSG = _(
'>= {0} and < {1}').format(
SPYDER_KERNELS_MIN_VERSION, SPYDER_KERNELS_MAX_VERSION)
Expand Down Expand Up @@ -1644,16 +1642,13 @@ def create_client_for_kernel(self, connection_file, hostname, sshkey,
# Assign kernel manager and client to shellwidget
kernel_client.start_channels()
shellwidget = client.shellwidget
if not known_spyder_kernel:
shellwidget.sig_is_spykernel.connect(
self.connect_external_spyder_kernel)
shellwidget.set_kernel_client_and_manager(
kernel_client, kernel_manager)
shellwidget.sig_exception_occurred.connect(
self.sig_exception_occurred)

if not known_spyder_kernel:
shellwidget.sig_is_spykernel.connect(
self.connect_external_spyder_kernel)
shellwidget.check_spyder_kernel()

self.sig_shellwidget_created.emit(shellwidget)
kernel_client.stopped_channels.connect(
lambda: self.sig_shellwidget_deleted.emit(shellwidget))
Expand Down
37 changes: 33 additions & 4 deletions spyder/plugins/ipythonconsole/widgets/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

# Standard library imports
import ast
import os
import os.path as osp
import time
Expand All @@ -30,18 +31,22 @@
from spyder.utils.palette import SpyderPalette
from spyder.utils.clipboard_helper import CLIPBOARD_HELPER
from spyder.utils import syntaxhighlighters as sh
from spyder.utils.programs import check_version_range
from spyder.plugins.ipythonconsole.utils.style import (
create_qss_style, create_style_class)
from spyder.widgets.helperwidgets import MessageCheckBox
from spyder.plugins.ipythonconsole.comms.kernelcomm import KernelComm
from spyder.plugins.ipythonconsole.widgets import (
ControlWidget, DebuggingWidget, FigureBrowserWidget, HelpWidget,
NamepaceBrowserWidget, PageControlWidget)
NamepaceBrowserWidget, PageControlWidget, SPYDER_KERNELS_VERSION)


MODULES_FAQ_URL = (
"https://docs.spyder-ide.org/5/faq.html#using-packages-installer")

ERROR_SPYDER_KERNEL_VERSION = _(
"Spyder kernel version {} not in required versions {}")


class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget,
FigureBrowserWidget):
Expand Down Expand Up @@ -247,6 +252,8 @@ def set_kernel_client_and_manager(self, kernel_client, kernel_manager):
"""Set the kernel client and manager"""
self.kernel_manager = kernel_manager
self.kernel_client = kernel_client
# Send message to kernel to check status
self.check_spyder_kernel()
if self.is_spyder_kernel:
# For completion
kernel_client.control_channel.message_received.connect(
Expand Down Expand Up @@ -302,7 +309,7 @@ def is_running(self):

def check_spyder_kernel(self):
"""Determine if the kernel is from Spyder."""
code = u"getattr(get_ipython().kernel, 'set_value', False)"
code = "getattr(get_ipython(), '_spyder_kernels_version', False)"
if self._reading:
return
else:
Expand All @@ -318,8 +325,30 @@ def check_spyder_kernel_callback(self, reply):
# Process kernel reply
data = reply.get('data')
if data is not None and 'text/plain' in data:
is_spyder_kernel = data['text/plain']
if 'SpyderKernel' in is_spyder_kernel:
version = ast.literal_eval(data['text/plain'])
if not version:
# The running_under_pytest() part can be removed when
# spyder-kernels 3 makes it into conda. This is needed for
# the test_conda_env_activation test
if running_under_pytest():
return

if self.is_spyder_kernel:
self.ipyclient.show_kernel_error(
_("spyder_kernels version must be updated"))
return

if not check_version_range(version, SPYDER_KERNELS_VERSION):
if "dev0" not in version:
# Development versions are acceptable
self.ipyclient.show_kernel_error(
ERROR_SPYDER_KERNEL_VERSION.format(
version,
SPYDER_KERNELS_VERSION
))
return

if not self.is_spyder_kernel:
self.is_spyder_kernel = True
self.sig_is_spykernel.emit(self)

Expand Down
41 changes: 24 additions & 17 deletions spyder/utils/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,29 @@ def run_terminal_thread():
raise NotImplementedError


def check_version_range(module_version, version):
"""
Check version string of a module against a required version.
"""
if ';' in version:
versions = version.split(';')
else:
versions = [version]

output = True
for _ver in versions:
match = re.search(r'[0-9]', _ver)
assert match is not None, "Invalid version number"
symb = _ver[:match.start()]
if not symb:
symb = '='
assert symb in ('>=', '>', '=', '<', '<='),\
"Invalid version condition '%s'" % symb
ver = _ver[match.start():]
output = output and check_version(module_version, ver, symb)
return output


def check_version(actver, version, cmp_op):
"""
Check version string of an active module against a required version.
Expand Down Expand Up @@ -924,23 +947,7 @@ def is_module_installed(module_name, version=None, interpreter=None,
if version is None:
return True
else:
if ';' in version:
versions = version.split(';')
else:
versions = [version]

output = True
for _ver in versions:
match = re.search(r'[0-9]', _ver)
assert match is not None, "Invalid version number"
symb = _ver[:match.start()]
if not symb:
symb = '='
assert symb in ('>=', '>', '=', '<', '<='),\
"Invalid version condition '%s'" % symb
ver = _ver[match.start():]
output = output and check_version(module_version, ver, symb)
return output
return check_version_range(module_version, version)


def is_python_interpreter_valid_name(filename):
Expand Down