Skip to content

Commit d3b80be

Browse files
committed
Use cloudpickle instead of JSON serialization.
1 parent 62d7bd9 commit d3b80be

File tree

5 files changed

+31
-53
lines changed

5 files changed

+31
-53
lines changed

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- replaced JSON serialization with cloudpickle. This allows extracting a much wider range of objects from the notebook subprocess.
6+
37
## 0.4.2
48

59
- Documentation and CoC updates to improve developer access (Thank you PyLadies Vancouver!)

docs/usage/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ my_list = ['list', 'from', 'notebook']
6363

6464
Reference objects to functions can be called with,
6565

66-
- explicit JSON serializable values (like `dict`, `list`, `int`, `float`, `str`, `bool`, etc)
66+
- explicit cloudpickle-serializable values (almost all Python objects)
6767
- other reference objects
6868

6969
```{code-block} python

testbook/client.py

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from nbclient import NotebookClient
77
from nbclient.exceptions import CellExecutionError
88
from nbformat.v4 import new_code_cell
9+
import cloudpickle
910

1011
from .exceptions import (
1112
TestbookCellTagNotFoundError,
@@ -39,7 +40,7 @@ def ref(self, name: str) -> Union[TestbookObjectReference, Any]:
3940
# Check if exists
4041
self.inject(name, pop=True)
4142
try:
42-
self.inject(f"import json; json.dumps({name})", pop=True)
43+
self.inject(f"import cloudpickle; cloudpickle.dumps({name})", pop=True)
4344
return self.value(name)
4445
except Exception:
4546
return TestbookObjectReference(self, name)
@@ -248,9 +249,8 @@ def inject(
248249

249250
def value(self, code: str) -> Any:
250251
"""
251-
Execute given code in the kernel and return JSON serializeable result.
252+
Execute given code in the kernel and returns the serializeable result.
252253
253-
If the result is not JSON serializeable, it raises `TestbookAttributeError`.
254254
This error object will also contain an attribute called `save_varname` which
255255
can be used to create a reference object with :meth:`ref`.
256256
@@ -277,34 +277,24 @@ def value(self, code: str) -> Any:
277277
'code provided does not produce execute_result'
278278
)
279279

280-
save_varname = random_varname()
281-
282-
inject_code = f"""
283-
import json
284-
from IPython import get_ipython
285-
from IPython.display import JSON
286-
287-
{save_varname} = get_ipython().last_execution_result.result
288-
289-
json.dumps({save_varname})
290-
JSON({{"value" : {save_varname}}})
291-
"""
292-
293-
try:
294-
outputs = self.inject(inject_code, pop=True).outputs
295-
296-
if outputs[0].output_type == "error":
297-
# will receive error when `allow_errors` is set to True
298-
raise TestbookRuntimeError(
299-
outputs[0].evalue, outputs[0].traceback, outputs[0].ename
300-
)
301-
302-
return outputs[0].data['application/json']['value']
303-
304-
except TestbookRuntimeError:
305-
e = TestbookSerializeError('could not JSON serialize output')
306-
e.save_varname = save_varname
307-
raise e
280+
import tempfile
281+
with tempfile.NamedTemporaryFile() as tmp:
282+
try:
283+
inject_code = f"""
284+
import cloudpickle
285+
with open('{tmp.name}', 'wb') as f:
286+
cloudpickle.dump(get_ipython().last_execution_result.result, f)
287+
"""
288+
outputs = self.inject(inject_code, pop=True).outputs
289+
if len(outputs) > 0 and outputs[0].output_type == "error":
290+
# will receive error when `allow_errors` is set to True
291+
raise TestbookRuntimeError(
292+
outputs[0].evalue, outputs[0].traceback, outputs[0].ename
293+
)
294+
with open(tmp.name, 'rb') as f:
295+
return cloudpickle.load(f)
296+
except TestbookRuntimeError:
297+
raise TestbookSerializeError('could not serialize output')
308298

309299
@contextmanager
310300
def patch(self, target, **kwargs):

testbook/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class TestbookCellTagNotFoundError(TestbookError):
1010

1111

1212
class TestbookSerializeError(TestbookError):
13-
"""Raised when output cannot be JSON serialized"""
13+
"""Raised when output cannot be serialized"""
1414

1515
pass
1616

testbook/tests/test_reference.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,8 @@ def test_function_call_with_ref_object(notebook):
4747
assert double(a) == [2, 4, 6]
4848

4949

50-
def test_reference(notebook):
50+
def test_nontrivial_pickling(notebook):
5151
Foo = notebook.ref("Foo")
52-
53-
# Check that when a non-serializeable object is returned, it returns
54-
# a reference to that object instead
55-
f = Foo('bar')
56-
57-
assert repr(f) == "\"<Foo value='bar'>\""
58-
59-
# Valid attribute access
60-
assert f.say_hello()
61-
62-
# Invalid attribute access
63-
with pytest.raises(TestbookAttributeError):
64-
f.does_not_exist
65-
66-
assert f.say_hello() == 'Hello bar!'
67-
68-
# non JSON-serializeable output
69-
with pytest.raises(TestbookSerializeError):
70-
f.resolve()
52+
f = Foo("bar")
53+
assert repr(f) == "<Foo value='bar'>"
54+
assert(f.say_hello() == "Hello bar!")

0 commit comments

Comments
 (0)