From 9ea8ce19caeb55a7930493a044f85f515e52e611 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Thu, 30 Sep 2021 14:36:28 +0200 Subject: [PATCH 1/3] Fallback to the old ipykernel "json_clean" if we are not able to serialize a JSON message --- jupyter_client/jsonutil.py | 68 ++++++++++++++++++++++++++++++++++++++ jupyter_client/session.py | 28 ++++++++++++---- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/jupyter_client/jsonutil.py b/jupyter_client/jsonutil.py index 2ba0bcf4f..9903f70ec 100644 --- a/jupyter_client/jsonutil.py +++ b/jupyter_client/jsonutil.py @@ -1,8 +1,10 @@ """Utilities to manipulate JSON objects.""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +import math import numbers import re +import types import warnings from binascii import b2a_base64 from collections.abc import Iterable @@ -122,3 +124,69 @@ def json_default(obj): return float(obj) raise TypeError("%r is not JSON serializable" % obj) + + +# Copy of the old ipykernel's json_clean +# This is temporary, it should be removed when we deprecate support for +# non-valid JSON messages +def json_clean(obj): + # types that are 'atomic' and ok in json as-is. + atomic_ok = (str, type(None)) + + # containers that we need to convert into lists + container_to_list = (tuple, set, types.GeneratorType) + + # Since bools are a subtype of Integrals, which are a subtype of Reals, + # we have to check them in that order. + + if isinstance(obj, bool): + return obj + + if isinstance(obj, numbers.Integral): + # cast int to int, in case subclasses override __str__ (e.g. boost enum, #4598) + return int(obj) + + if isinstance(obj, numbers.Real): + # cast out-of-range floats to their reprs + if math.isnan(obj) or math.isinf(obj): + return repr(obj) + return float(obj) + + if isinstance(obj, atomic_ok): + return obj + + if isinstance(obj, bytes): + # unanmbiguous binary data is base64-encoded + # (this probably should have happened upstream) + return b2a_base64(obj).decode('ascii') + + if isinstance(obj, container_to_list) or ( + hasattr(obj, '__iter__') and hasattr(obj, next_attr_name) + ): + obj = list(obj) + + if isinstance(obj, list): + return [json_clean(x) for x in obj] + + if isinstance(obj, dict): + # First, validate that the dict won't lose data in conversion due to + # key collisions after stringification. This can happen with keys like + # True and 'true' or 1 and '1', which collide in JSON. + nkeys = len(obj) + nkeys_collapsed = len(set(map(str, obj))) + if nkeys != nkeys_collapsed: + raise ValueError( + 'dict cannot be safely converted to JSON: ' + 'key collision would lead to dropped values' + ) + # If all OK, proceed by making the new dict that will be json-safe + out = {} + for k, v in obj.items(): + out[str(k)] = json_clean(v) + return out + + if isinstance(obj, datetime): + return obj.strftime(ISO8601) + + # we don't understand it, it's probably an unserializable object + raise ValueError("Can't clean for JSON: %r" % obj) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index f0786a517..be9bbb507 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -50,6 +50,7 @@ from jupyter_client import protocol_version from jupyter_client.adapter import adapt from jupyter_client.jsonutil import extract_dates +from jupyter_client.jsonutil import json_clean from jupyter_client.jsonutil import json_default from jupyter_client.jsonutil import squash_dates @@ -92,12 +93,27 @@ def squash_unicode(obj): def json_packer(obj): - return json.dumps( - obj, - default=json_default, - ensure_ascii=False, - allow_nan=False, - ).encode("utf8") + try: + return json.dumps( + obj, + default=json_default, + ensure_ascii=False, + allow_nan=False, + ).encode("utf8") + except ValueError as e: + # Fallback to trying to clean the json before serializing + warnings.warn( + f"Message serialization failed with:\n{e}\n" + "Supporting this message is deprecated, please make " + "sure your message is JSON-compliant", + stacklevel=2, + ) + return json.dumps( + json_clean(obj), + default=json_default, + ensure_ascii=False, + allow_nan=False, + ).encode("utf8") def json_unpacker(s): From 79d9fe43f3b79aba203ecbd8e6d06e9fbe1461bb Mon Sep 17 00:00:00 2001 From: martinRenou Date: Fri, 1 Oct 2021 15:44:21 +0200 Subject: [PATCH 2/3] Fix warning message Co-authored-by: Min RK --- jupyter_client/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index be9bbb507..017061f95 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -104,7 +104,7 @@ def json_packer(obj): # Fallback to trying to clean the json before serializing warnings.warn( f"Message serialization failed with:\n{e}\n" - "Supporting this message is deprecated, please make " + "Supporting this message is deprecated in jupyter-client 7, please make " "sure your message is JSON-compliant", stacklevel=2, ) From e9ae299acc39934e3c2608aba6bc373779de2720 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Fri, 1 Oct 2021 15:45:50 +0200 Subject: [PATCH 3/3] Show warning after json_clean --- jupyter_client/session.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 017061f95..44f0a799d 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -102,18 +102,21 @@ def json_packer(obj): ).encode("utf8") except ValueError as e: # Fallback to trying to clean the json before serializing + packed = json.dumps( + json_clean(obj), + default=json_default, + ensure_ascii=False, + allow_nan=False, + ).encode("utf8") + warnings.warn( f"Message serialization failed with:\n{e}\n" "Supporting this message is deprecated in jupyter-client 7, please make " "sure your message is JSON-compliant", stacklevel=2, ) - return json.dumps( - json_clean(obj), - default=json_default, - ensure_ascii=False, - allow_nan=False, - ).encode("utf8") + + return packed def json_unpacker(s):