Skip to content

Commit ec53483

Browse files
committed
Merge from 3.x: PR #5341
2 parents b772b2d + c65e94e commit ec53483

File tree

13 files changed

+112
-96
lines changed

13 files changed

+112
-96
lines changed

Diff for: appveyor.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ environment:
1111
CONDA_DEPENDENCIES_FLAGS: "--quiet"
1212
CONDA_DEPENDENCIES: >
1313
rope pyflakes sphinx pygments pylint pycodestyle psutil nbconvert
14-
qtawesome pickleshare pyzmq chardet mock pandas pytest
14+
qtawesome cloudpickle pickleshare pyzmq chardet mock pandas pytest
1515
pytest-cov numpydoc scipy pillow qtconsole matplotlib jedi pywin32
1616
PIP_DEPENDENCIES: "pytest-qt pytest-mock pytest-timeout flaky"
1717

Diff for: conda.recipe/meta.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ requirements:
3333
- pyzmq
3434
- chardet >=2.0.0
3535
- numpydoc
36+
- cloudpickle
3637

3738
about:
3839
home: https://github.com/spyder-ide/spyder

Diff for: continuous_integration/circle/install.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
export CONDA_DEPENDENCIES_FLAGS="--quiet"
44
export CONDA_DEPENDENCIES="rope pyflakes sphinx pygments pylint psutil nbconvert \
55
qtawesome pickleshare qtpy pyzmq chardet mock nomkl pandas \
6-
pytest pytest-cov numpydoc scipy cython pillow"
6+
pytest pytest-cov numpydoc scipy cython pillow cloudpickle"
77
export PIP_DEPENDENCIES="coveralls pytest-qt pytest-mock pytest-xvfb flaky jedi pycodestyle"
88

99
# Download and install miniconda and conda/pip dependencies

Diff for: continuous_integration/travis/install.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ if [ "$TRAVIS_PYTHON_VERSION" = "3.5" ] && [ "$USE_PYQT" = "pyqt5" ]; then
88
else
99
export CONDA_DEPENDENCIES_FLAGS="--quiet"
1010
export CONDA_DEPENDENCIES="rope pyflakes sphinx pygments pylint psutil nbconvert \
11-
qtawesome pickleshare qtpy pyzmq chardet mock nomkl pandas \
11+
qtawesome cloudpickle pickleshare qtpy pyzmq chardet mock nomkl pandas \
1212
pytest pytest-cov numpydoc scipy cython pillow jedi pycodestyle"
1313
export PIP_DEPENDENCIES="coveralls pytest-qt pytest-mock pytest-timeout flaky"
1414
fi

Diff for: requirements/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
cloudpickle
12
rope>=0.9.4
23
jedi>=0.9.0
34
pyflakes

Diff for: setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ def run(self):
272272
import setuptools # analysis:ignore
273273

274274
install_requires = [
275+
'cloudpickle',
275276
'rope>=0.10.5',
276277
'jedi>=0.9.0',
277278
'pyflakes',

Diff for: spyder/plugins/ipythonconsole.py

+14-10
Original file line numberDiff line numberDiff line change
@@ -993,20 +993,24 @@ def create_new_client(self, give_focus=True, filename=''):
993993
# Else we won't be able to create a client
994994
if not CONF.get('main_interpreter', 'default'):
995995
pyexec = CONF.get('main_interpreter', 'executable')
996-
ipykernel_present = programs.is_module_installed('ipykernel',
997-
interpreter=pyexec)
998-
if not ipykernel_present:
996+
has_ipykernel = programs.is_module_installed('ipykernel',
997+
interpreter=pyexec)
998+
has_cloudpickle = programs.is_module_installed('cloudpickle',
999+
interpreter=pyexec)
1000+
if not (has_ipykernel and has_cloudpickle):
9991001
client.show_kernel_error(_("Your Python environment or "
10001002
"installation doesn't "
1001-
"have the <tt>ipykernel</tt> module "
1002-
"installed on it. Without this module is "
1003-
"not possible for Spyder to create a "
1003+
"have the <tt>ipykernel</tt> and "
1004+
"<tt>cloudpickle</tt> modules "
1005+
"installed on it. Without these modules "
1006+
"is not possible for Spyder to create a "
10041007
"console for you.<br><br>"
1005-
"You can install <tt>ipykernel</tt> by "
1006-
"running in a terminal:<br><br>"
1007-
"<tt>pip install ipykernel</tt><br><br>"
1008+
"You can install them by running "
1009+
"in a system terminal:<br><br>"
1010+
"<tt>pip install ipykernel cloudpickle</tt>"
1011+
"<br><br>"
10081012
"or<br><br>"
1009-
"<tt>conda install ipykernel</tt>"))
1013+
"<tt>conda install ipykernel cloudpickle</tt>"))
10101014
return
10111015

10121016
self.connect_client_to_kernel(client)

Diff for: spyder/plugins/tests/test_ipythonconsole.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
import tempfile
1212
from textwrap import dedent
1313

14+
import cloudpickle
1415
from flaky import flaky
15-
from ipykernel.serialize import serialize_object
1616
from pygments.token import Name
1717
import pytest
1818
from qtpy import PYQT4, PYQT5, PYQT_VERSION
@@ -323,7 +323,7 @@ def test_unicode_vars(ipyconsole, qtbot):
323323
assert shell.get_value('д') == 10
324324

325325
# Change its value and verify
326-
shell.set_value('д', serialize_object(20))
326+
shell.set_value('д', [cloudpickle.dumps(20, protocol=2)])
327327
qtbot.wait(1000)
328328
assert shell.get_value('д') == 20
329329

@@ -373,7 +373,7 @@ def test_values_dbg(ipyconsole, qtbot):
373373
assert shell.get_value('aa') == 10
374374

375375
# Set value
376-
shell.set_value('aa', serialize_object(20))
376+
shell.set_value('aa', [cloudpickle.dumps(20, protocol=2)])
377377
qtbot.wait(1000)
378378
assert shell.get_value('aa') == 20
379379

Diff for: spyder/utils/ipython/kernelspec.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
class SpyderKernelSpec(KernelSpec):
2525
"""Kernel spec for Spyder kernels"""
2626

27-
default_interpreter = CONF.get('main_interpreter', 'default')
2827
spy_path = get_module_source_path('spyder')
2928

3029
def __init__(self, **kwargs):
@@ -37,7 +36,7 @@ def __init__(self, **kwargs):
3736
def argv(self):
3837
"""Command to start kernels"""
3938
# Python interpreter used to start kernels
40-
if self.default_interpreter:
39+
if CONF.get('main_interpreter', 'default'):
4140
pyexec = get_python_executable()
4241
else:
4342
# Avoid IPython adding the virtualenv on which Spyder is running

Diff for: spyder/utils/ipython/spyder_kernel.py

+50-23
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@
1515
import traceback
1616

1717
# Third-party imports
18-
from ipykernel.datapub import publish_data
18+
import cloudpickle
1919
from ipykernel.ipkernel import IPythonKernel
20-
import ipykernel.pickleutil
21-
from ipykernel.pickleutil import CannedObject
22-
from ipykernel.serialize import deserialize_object
2320

2421
# Check if we are running under an external interpreter
2522
IS_EXT_INTERPRETER = os.environ.get('EXTERNAL_INTERPRETER', '').lower() == "true"
@@ -43,20 +40,16 @@
4340
make_remote_view)
4441

4542

46-
# XXX --- Disable canning for Numpy arrays for now ---
47-
# This allows getting values between a Python 3 frontend
48-
# and a Python 2 kernel, and viceversa, for several types of
49-
# arrays.
50-
# See this link for interesting ideas on how to solve this
51-
# in the future:
52-
# http://stackoverflow.com/q/30698004/438386
53-
ipykernel.pickleutil.can_map.pop('numpy.ndarray')
43+
PY2 = sys.version[0] == '2'
5444

5545

5646
# Excluded variables from the Variable Explorer (i.e. they are not
5747
# shown at all there)
5848
EXCLUDED_NAMES = ['In', 'Out', 'exit', 'get_ipython', 'quit']
5949

50+
# To be able to get and set variables between Python 2 and 3
51+
PICKLE_PROTOCOL = 2
52+
6053

6154
class SpyderKernel(IPythonKernel):
6255
"""Spyder kernel for Jupyter"""
@@ -150,28 +143,62 @@ def get_var_properties(self):
150143
else:
151144
return repr(None)
152145

146+
def send_spyder_msg(self, spyder_msg_type, content=None, data=None):
147+
"""publish custom messages to the spyder frontend
148+
149+
Parameters
150+
----------
151+
152+
spyder_msg_type: str
153+
The spyder message type
154+
content: dict
155+
The (JSONable) content of the message
156+
data: any
157+
Any object that is serializable by cloudpickle (should be most
158+
things). Will arrive as cloudpickled bytes in `.buffers[0]`.
159+
"""
160+
if content is None:
161+
content = {}
162+
content['spyder_msg_type'] = spyder_msg_type
163+
self.session.send(
164+
self.iopub_socket,
165+
'spyder_msg',
166+
content=content,
167+
buffers=[cloudpickle.dumps(data, protocol=PICKLE_PROTOCOL)],
168+
parent=self._parent_header,
169+
)
170+
153171
def get_value(self, name):
154172
"""Get the value of a variable"""
155173
ns = self._get_current_namespace()
156174
value = ns[name]
157175
try:
158-
publish_data({'__spy_data__': value})
176+
self.send_spyder_msg('data', data=value)
159177
except:
160178
# * There is no need to inform users about
161179
# these errors.
162180
# * value = None makes Spyder to ignore
163181
# petitions to display a value
164-
value = None
165-
publish_data({'__spy_data__': value})
182+
self.send_spyder_msg('data', data=None)
166183
self._do_publish_pdb_state = False
167184

168-
def set_value(self, name, value):
185+
def set_value(self, name, value, PY2_frontend):
169186
"""Set the value of a variable"""
170187
ns = self._get_reference_namespace(name)
171-
value = deserialize_object(value)[0]
172-
if isinstance(value, CannedObject):
173-
value = value.get_object()
174-
ns[name] = value
188+
189+
# We send serialized values in a list of one element
190+
# from Spyder to the kernel, to be able to send them
191+
# at all in Python 2
192+
svalue = value[0]
193+
194+
# We need to convert svalue to bytes if the frontend
195+
# runs in Python 2 and the kernel runs in Python 3
196+
if PY2_frontend and not PY2:
197+
svalue = bytes(svalue, 'latin-1')
198+
199+
# Deserialize and set value in namespace
200+
dvalue = cloudpickle.loads(svalue)
201+
ns[name] = dvalue
175202

176203
def remove_value(self, name):
177204
"""Remove a variable"""
@@ -217,13 +244,13 @@ def save_namespace(self, filename):
217244
def publish_pdb_state(self):
218245
"""
219246
Publish Variable Explorer state and Pdb step through
220-
publish_data.
247+
send_spyder_msg.
221248
"""
222249
if self._pdb_obj and self._do_publish_pdb_state:
223250
state = dict(namespace_view = self.get_namespace_view(),
224251
var_properties = self.get_var_properties(),
225252
step = self._pdb_step)
226-
publish_data({'__spy_pdb_state__': state})
253+
self.send_spyder_msg('pdb_state', content={'pdb_state': state})
227254
self._do_publish_pdb_state = True
228255

229256
def pdb_continue(self):
@@ -234,7 +261,7 @@ def pdb_continue(self):
234261
Fixes issue 2034
235262
"""
236263
if self._pdb_obj:
237-
publish_data({'__spy_pdb_continue__': True})
264+
self.send_spyder_msg('pdb_continue')
238265

239266
# --- For the Help plugin
240267
def is_defined(self, obj, force_import=False):

Diff for: spyder/utils/site/sitecustomize.py

-7
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,6 @@ def __init__(self, *args, **kwargs):
280280
TestProgram.__init__(self, *args, **kwargs)
281281
unittest.main = IPyTesProgram
282282

283-
# Filter warnings that appear for ipykernel when interacting with
284-
# the Variable explorer (i.e trying to see a variable)
285-
# Fixes Issue 5591
286-
warnings.filterwarnings(action='ignore', category=DeprecationWarning,
287-
module='ipykernel.datapub',
288-
message=".*ipykernel.datapub is deprecated.*")
289-
290283

291284
#==============================================================================
292285
# Pandas adjustments

Diff for: spyder/widgets/ipythonconsole/namespacebrowser.py

+30-35
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@
1212
from qtpy.QtCore import QEventLoop
1313
from qtpy.QtWidgets import QMessageBox
1414

15-
from ipykernel.pickleutil import CannedObject
16-
from ipykernel.serialize import deserialize_object
15+
import cloudpickle
1716
from qtconsole.rich_jupyter_widget import RichJupyterWidget
1817

19-
from spyder.config.base import _
20-
from spyder.py3compat import to_text_string
18+
from spyder.config.base import _, debug_print
19+
from spyder.py3compat import PY2, to_text_string
2120

2221

2322
class NamepaceBrowserWidget(RichJupyterWidget):
@@ -98,7 +97,9 @@ def get_value(self, name):
9897
def set_value(self, name, value):
9998
"""Set value for a variable"""
10099
value = to_text_string(value)
101-
code = u"get_ipython().kernel.set_value('%s', %s)" % (name, value)
100+
code = u"get_ipython().kernel.set_value('%s', %s, %s)" % (name, value,
101+
PY2)
102+
102103
if self._reading:
103104
self.kernel_client.input(u'!' + code)
104105
else:
@@ -159,41 +160,35 @@ def save_namespace(self, filename):
159160
return self._kernel_reply
160161

161162
# ---- Private API (defined by us) ------------------------------
162-
def _handle_data_message(self, msg):
163+
def _handle_spyder_msg(self, msg):
163164
"""
164-
Handle raw (serialized) data sent by the kernel
165-
166-
We only handle data asked by Spyder, in case people use
167-
publish_data for other purposes.
165+
Handle internal spyder messages
168166
"""
169-
# Deserialize data
170-
try:
171-
data = deserialize_object(msg['buffers'])[0]
172-
except Exception as msg:
173-
self._kernel_value = None
174-
self._kernel_reply = repr(msg)
175-
self.sig_got_reply.emit()
176-
return
177-
178-
# Receive values asked for Spyder
179-
value = data.get('__spy_data__', None)
180-
if value is not None:
181-
if isinstance(value, CannedObject):
182-
value = value.get_object()
183-
self._kernel_value = value
167+
spyder_msg_type = msg['content'].get('spyder_msg_type')
168+
if spyder_msg_type == 'data':
169+
# Deserialize data
170+
try:
171+
if PY2:
172+
value = cloudpickle.loads(msg['buffers'][0])
173+
else:
174+
value = cloudpickle.loads(bytes(msg['buffers'][0]))
175+
except Exception as msg:
176+
self._kernel_value = None
177+
self._kernel_reply = repr(msg)
178+
else:
179+
self._kernel_value = value
184180
self.sig_got_reply.emit()
185181
return
186-
187-
# Receive Pdb state and dispatch it
188-
pdb_state = data.get('__spy_pdb_state__', None)
189-
if pdb_state is not None and isinstance(pdb_state, dict):
190-
self.refresh_from_pdb(pdb_state)
191-
192-
# Run Pdb continue to get to the first breakpoint
193-
# Fixes 2034
194-
pdb_continue = data.get('__spy_pdb_continue__', None)
195-
if pdb_continue:
182+
elif spyder_msg_type == 'pdb_state':
183+
pdb_state = msg['content']['pdb_state']
184+
if pdb_state is not None and isinstance(pdb_state, dict):
185+
self.refresh_from_pdb(pdb_state)
186+
elif spyder_msg_type == 'pdb_continue':
187+
# Run Pdb continue to get to the first breakpoint
188+
# Fixes 2034
196189
self.write_to_stdin('continue')
190+
else:
191+
debug_print("No such spyder message type: %s" % spyder_msg_type)
197192

198193
# ---- Private API (overrode by us) ----------------------------
199194
def _handle_execute_reply(self, msg):

0 commit comments

Comments
 (0)