Skip to content

Commit 58b11df

Browse files
authored
Merge pull request #708 from martinRenou/fallback_json_clean
Fallback to the old ipykernel "json_clean" if we are not able to serialize a JSON message
2 parents 123d3ef + e9ae299 commit 58b11df

File tree

2 files changed

+93
-6
lines changed

2 files changed

+93
-6
lines changed

jupyter_client/jsonutil.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Utilities to manipulate JSON objects."""
22
# Copyright (c) Jupyter Development Team.
33
# Distributed under the terms of the Modified BSD License.
4+
import math
45
import numbers
56
import re
7+
import types
68
import warnings
79
from binascii import b2a_base64
810
from collections.abc import Iterable
@@ -122,3 +124,69 @@ def json_default(obj):
122124
return float(obj)
123125

124126
raise TypeError("%r is not JSON serializable" % obj)
127+
128+
129+
# Copy of the old ipykernel's json_clean
130+
# This is temporary, it should be removed when we deprecate support for
131+
# non-valid JSON messages
132+
def json_clean(obj):
133+
# types that are 'atomic' and ok in json as-is.
134+
atomic_ok = (str, type(None))
135+
136+
# containers that we need to convert into lists
137+
container_to_list = (tuple, set, types.GeneratorType)
138+
139+
# Since bools are a subtype of Integrals, which are a subtype of Reals,
140+
# we have to check them in that order.
141+
142+
if isinstance(obj, bool):
143+
return obj
144+
145+
if isinstance(obj, numbers.Integral):
146+
# cast int to int, in case subclasses override __str__ (e.g. boost enum, #4598)
147+
return int(obj)
148+
149+
if isinstance(obj, numbers.Real):
150+
# cast out-of-range floats to their reprs
151+
if math.isnan(obj) or math.isinf(obj):
152+
return repr(obj)
153+
return float(obj)
154+
155+
if isinstance(obj, atomic_ok):
156+
return obj
157+
158+
if isinstance(obj, bytes):
159+
# unanmbiguous binary data is base64-encoded
160+
# (this probably should have happened upstream)
161+
return b2a_base64(obj).decode('ascii')
162+
163+
if isinstance(obj, container_to_list) or (
164+
hasattr(obj, '__iter__') and hasattr(obj, next_attr_name)
165+
):
166+
obj = list(obj)
167+
168+
if isinstance(obj, list):
169+
return [json_clean(x) for x in obj]
170+
171+
if isinstance(obj, dict):
172+
# First, validate that the dict won't lose data in conversion due to
173+
# key collisions after stringification. This can happen with keys like
174+
# True and 'true' or 1 and '1', which collide in JSON.
175+
nkeys = len(obj)
176+
nkeys_collapsed = len(set(map(str, obj)))
177+
if nkeys != nkeys_collapsed:
178+
raise ValueError(
179+
'dict cannot be safely converted to JSON: '
180+
'key collision would lead to dropped values'
181+
)
182+
# If all OK, proceed by making the new dict that will be json-safe
183+
out = {}
184+
for k, v in obj.items():
185+
out[str(k)] = json_clean(v)
186+
return out
187+
188+
if isinstance(obj, datetime):
189+
return obj.strftime(ISO8601)
190+
191+
# we don't understand it, it's probably an unserializable object
192+
raise ValueError("Can't clean for JSON: %r" % obj)

jupyter_client/session.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from jupyter_client import protocol_version
5151
from jupyter_client.adapter import adapt
5252
from jupyter_client.jsonutil import extract_dates
53+
from jupyter_client.jsonutil import json_clean
5354
from jupyter_client.jsonutil import json_default
5455
from jupyter_client.jsonutil import squash_dates
5556

@@ -92,12 +93,30 @@ def squash_unicode(obj):
9293

9394

9495
def json_packer(obj):
95-
return json.dumps(
96-
obj,
97-
default=json_default,
98-
ensure_ascii=False,
99-
allow_nan=False,
100-
).encode("utf8")
96+
try:
97+
return json.dumps(
98+
obj,
99+
default=json_default,
100+
ensure_ascii=False,
101+
allow_nan=False,
102+
).encode("utf8")
103+
except ValueError as e:
104+
# Fallback to trying to clean the json before serializing
105+
packed = json.dumps(
106+
json_clean(obj),
107+
default=json_default,
108+
ensure_ascii=False,
109+
allow_nan=False,
110+
).encode("utf8")
111+
112+
warnings.warn(
113+
f"Message serialization failed with:\n{e}\n"
114+
"Supporting this message is deprecated in jupyter-client 7, please make "
115+
"sure your message is JSON-compliant",
116+
stacklevel=2,
117+
)
118+
119+
return packed
101120

102121

103122
def json_unpacker(s):

0 commit comments

Comments
 (0)