From 14797759fe94389b34357ced22da6a00217c5446 Mon Sep 17 00:00:00 2001 From: Erik Gaasedelen Date: Fri, 6 Mar 2026 09:44:12 -0800 Subject: [PATCH] remove clean_json --- ipykernel/comm/comm.py | 5 +- ipykernel/displayhook.py | 4 +- ipykernel/inprocess/ipkernel.py | 3 +- ipykernel/jsonutil.py | 97 +------------------------- ipykernel/kernelbase.py | 10 +-- ipykernel/zmqshell.py | 8 +-- tests/test_jsonutil.py | 120 -------------------------------- 7 files changed, 11 insertions(+), 236 deletions(-) delete mode 100644 tests/test_jsonutil.py diff --git a/ipykernel/comm/comm.py b/ipykernel/comm/comm.py index a1b659e9a..ade317b9c 100644 --- a/ipykernel/comm/comm.py +++ b/ipykernel/comm/comm.py @@ -11,7 +11,6 @@ import traitlets.config from traitlets import Bool, Bytes, Instance, Unicode, default -from ipykernel.jsonutil import json_clean from ipykernel.kernelbase import Kernel @@ -28,7 +27,7 @@ def publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys): data = {} if data is None else data metadata = {} if metadata is None else metadata - content = json_clean(dict(data=data, comm_id=self.comm_id, **keys)) + content = dict(data=data, comm_id=self.comm_id, **keys) if self.kernel is None: self.kernel = Kernel.instance() @@ -38,7 +37,7 @@ def publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys): self.kernel.iopub_socket, msg_type, content, - metadata=json_clean(metadata), + metadata=metadata, parent=self.kernel.get_parent(), ident=self.topic, buffers=buffers, diff --git a/ipykernel/displayhook.py b/ipykernel/displayhook.py index d5e136748..75479edcf 100644 --- a/ipykernel/displayhook.py +++ b/ipykernel/displayhook.py @@ -13,7 +13,7 @@ from jupyter_client.session import Session, extract_header from traitlets import Any, Instance -from ipykernel.jsonutil import encode_images, json_clean +from ipykernel.jsonutil import encode_images class ZMQDisplayHook: @@ -120,7 +120,7 @@ def write_output_prompt(self): def write_format_data(self, format_dict, md_dict=None): """Write format data to the message.""" if self.msg: - self.msg["content"]["data"] = json_clean(encode_images(format_dict)) + self.msg["content"]["data"] = encode_images(format_dict) self.msg["content"]["metadata"] = md_dict def finish_displayhook(self): diff --git a/ipykernel/inprocess/ipkernel.py b/ipykernel/inprocess/ipkernel.py index e61af4277..c5b6e8b79 100644 --- a/ipykernel/inprocess/ipkernel.py +++ b/ipykernel/inprocess/ipkernel.py @@ -12,7 +12,6 @@ from traitlets import Any, Enum, Instance, List, Type, default from ipykernel.ipkernel import IPythonKernel -from ipykernel.jsonutil import json_clean from ipykernel.zmqshell import ZMQInteractiveShell from ..iostream import BackgroundSocket, IOPubThread, OutStream @@ -98,7 +97,7 @@ def _input_request(self, prompt, ident, parent, password=False): sys.stderr.flush() # Send the input request. - content = json_clean(dict(prompt=prompt, password=password)) + content = dict(prompt=prompt, password=password) assert self.session is not None msg = self.session.msg("input_request", content, parent) for frontend in self.frontends: diff --git a/ipykernel/jsonutil.py b/ipykernel/jsonutil.py index d40661c74..4c49202ad 100644 --- a/ipykernel/jsonutil.py +++ b/ipykernel/jsonutil.py @@ -3,12 +3,8 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -import math -import numbers import re -import types -from binascii import b2a_base64 -from datetime import date, datetime +from datetime import datetime from jupyter_client._version import version_info as jupyter_client_version @@ -52,8 +48,6 @@ def encode_images(format_dict): """b64-encodes images in a displaypub format dict - Perhaps this should be handled in json_clean itself? - Parameters ---------- format_dict : dict @@ -72,92 +66,3 @@ def encode_images(format_dict): # where bytes objects always represent binary data and thus # base64-encoded. return format_dict - - -def json_clean(obj): # pragma: no cover - """Deprecated, this is a no-op for jupyter-client>=7. - - Clean an object to ensure it's safe to encode in JSON. - - Atomic, immutable objects are returned unmodified. Sets and tuples are - converted to lists, lists are copied and dicts are also copied. - - Note: dicts whose keys could cause collisions upon encoding (such as a dict - with both the number 1 and the string '1' as keys) will cause a ValueError - to be raised. - - Parameters - ---------- - obj : any python object - - Returns - ------- - out : object - A version of the input which will not cause an encoding error when - encoded as JSON. Note that this function does not *encode* its inputs, - it simply sanitizes it so that there will be no encoding errors later. - - """ - if int(JUPYTER_CLIENT_MAJOR_VERSION) >= 7: - return 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: - msg = ( - "dict cannot be safely converted to JSON: " - "key collision would lead to dropped values" - ) - raise ValueError(msg) - # 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 | date): - 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/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 7fa1fb9dc..14151be7c 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -56,8 +56,6 @@ ) from zmq.eventloop.zmqstream import ZMQStream -from ipykernel.jsonutil import json_clean - from ._version import kernel_protocol_version from .iostream import OutStream from .utils import LazyDict, _async_in_context @@ -851,7 +849,6 @@ async def execute_request(self, stream, ident, parent): time.sleep(self._execute_sleep) # Send the reply. - reply_content = json_clean(reply_content) metadata = self.finish_metadata(parent, metadata, reply_content) reply_msg: dict[str, t.Any] = self.session.send( # type:ignore[assignment] @@ -901,7 +898,6 @@ async def complete_request(self, stream, ident, parent): stacklevel=1, ) - matches = json_clean(matches) self.session.send(stream, "complete_reply", matches, parent, ident) async def do_complete(self, code, cursor_pos): @@ -936,7 +932,6 @@ async def inspect_request(self, stream, ident, parent): ) # Before we send this object over, we scrub it for JSON usage - reply_content = json_clean(reply_content) msg = self.session.send(stream, "inspect_reply", reply_content, parent, ident) self.log.debug("%s", msg) @@ -960,7 +955,6 @@ async def history_request(self, stream, ident, parent): stacklevel=1, ) - reply_content = json_clean(reply_content) msg = self.session.send(stream, "history_reply", reply_content, parent, ident) self.log.debug("%s", msg) @@ -1126,7 +1120,6 @@ async def is_complete_request(self, stream, ident, parent): PendingDeprecationWarning, stacklevel=1, ) - reply_content = json_clean(reply_content) reply_msg = self.session.send(stream, "is_complete_reply", reply_content, parent, ident) self.log.debug("%s", reply_msg) @@ -1150,7 +1143,6 @@ async def debug_request(self, stream, ident, parent): PendingDeprecationWarning, stacklevel=1, ) - reply_content = json_clean(reply_content) reply_msg = self.session.send(stream, "debug_reply", reply_content, parent, ident) self.log.debug("%s", reply_msg) @@ -1425,7 +1417,7 @@ def _input_request(self, prompt, ident, parent, password=False): # Send the input request. assert self.session is not None - content = json_clean(dict(prompt=prompt, password=password)) + content = dict(prompt=prompt, password=password) self.session.send(self.stdin_socket, "input_request", content, parent, ident=ident) # Await a response. diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index bd3f8ef41..69b8686eb 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -40,7 +40,7 @@ from ipykernel import connect_qtconsole, get_connection_file, get_connection_info from ipykernel.displayhook import ZMQShellDisplayHook -from ipykernel.jsonutil import encode_images, json_clean +from ipykernel.jsonutil import encode_images try: from IPython.core.history import HistoryOutput @@ -164,7 +164,7 @@ def publish( # type:ignore[override] # in order to put it through the transform # hooks before potentially sending. assert self.session is not None - msg = self.session.msg(msg_type, json_clean(content), parent=self.parent_header) + msg = self.session.msg(msg_type, content, parent=self.parent_header) # Each transform either returns a new # message or None. If None is returned, @@ -194,7 +194,7 @@ def clear_output(self, wait=False): content = dict(wait=wait) self._flush_streams() assert self.session is not None - msg = self.session.msg("clear_output", json_clean(content), parent=self.parent_header) + msg = self.session.msg("clear_output", content, parent=self.parent_header) # see publish() for details on how this works for hook in self._hooks: @@ -684,7 +684,7 @@ def _showtraceback(self, etype, evalue, stb): dh.session.send( # type:ignore[attr-defined] dh.pub_socket, # type:ignore[attr-defined] "error", - json_clean(exc_content), + exc_content, dh.parent_header, # type:ignore[attr-defined] ident=topic, ) diff --git a/tests/test_jsonutil.py b/tests/test_jsonutil.py deleted file mode 100644 index 2c6b95372..000000000 --- a/tests/test_jsonutil.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Test suite for our JSON utilities.""" - -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. - -import json -import numbers -from binascii import a2b_base64 -from datetime import date, datetime - -import pytest -from jupyter_client._version import version_info as jupyter_client_version - -from ipykernel import jsonutil -from ipykernel.jsonutil import encode_images, json_clean - -JUPYTER_CLIENT_MAJOR_VERSION: int = jupyter_client_version[0] # type:ignore - - -class MyInt: - def __int__(self): - return 389 - - -numbers.Integral.register(MyInt) - - -class MyFloat: - def __float__(self): - return 3.14 - - -numbers.Real.register(MyFloat) - - -@pytest.mark.skipif(JUPYTER_CLIENT_MAJOR_VERSION >= 7, reason="json_clean is a no-op") -def test(): - # list of input/expected output. Use None for the expected output if it - # can be the same as the input. - pairs = [ - (1, None), # start with scalars - (1.0, None), - ("a", None), - (True, None), - (False, None), - (None, None), - # Containers - ([1, 2], None), - ((1, 2), [1, 2]), - ({1, 2}, [1, 2]), - (dict(x=1), None), - ({"x": 1, "y": [1, 2, 3], "1": "int"}, None), - # More exotic objects - ((x for x in range(3)), [0, 1, 2]), - (iter([1, 2]), [1, 2]), - (datetime(1991, 7, 3, 12, 00), "1991-07-03T12:00:00.000000"), - (date(1991, 7, 3), "1991-07-03T00:00:00.000000"), - (MyFloat(), 3.14), - (MyInt(), 389), - ] - - for val, jval in pairs: - if jval is None: - jval = val # type:ignore - out = json_clean(val) - # validate our cleanup - assert out == jval - # and ensure that what we return, indeed encodes cleanly - json.loads(json.dumps(out)) - - -@pytest.mark.skipif(JUPYTER_CLIENT_MAJOR_VERSION >= 7, reason="json_clean is a no-op") -def test_encode_images(): - # invalid data, but the header and footer are from real files - pngdata = b"\x89PNG\r\n\x1a\nblahblahnotactuallyvalidIEND\xaeB`\x82" - jpegdata = b"\xff\xd8\xff\xe0\x00\x10JFIFblahblahjpeg(\xa0\x0f\xff\xd9" - pdfdata = b"%PDF-1.\ntrailer<>]>>>>>>" - bindata = b"\xff\xff\xff\xff" - - fmt = { - "image/png": pngdata, - "image/jpeg": jpegdata, - "application/pdf": pdfdata, - "application/unrecognized": bindata, - } - encoded = json_clean(encode_images(fmt)) - for key, value in fmt.items(): - # encoded has unicode, want bytes - decoded = a2b_base64(encoded[key]) - assert decoded == value - encoded2 = json_clean(encode_images(encoded)) - assert encoded == encoded2 - - for key, value in fmt.items(): - decoded = a2b_base64(encoded[key]) - assert decoded == value - - -@pytest.mark.skipif(JUPYTER_CLIENT_MAJOR_VERSION >= 7, reason="json_clean is a no-op") -def test_lambda(): - with pytest.raises(ValueError): # noqa: PT011 - json_clean(lambda: 1) - - -@pytest.mark.skipif(JUPYTER_CLIENT_MAJOR_VERSION >= 7, reason="json_clean is a no-op") -def test_exception(): - bad_dicts = [ - {1: "number", "1": "string"}, - {True: "bool", "True": "string"}, - ] - for d in bad_dicts: - with pytest.raises(ValueError): # noqa: PT011 - json_clean(d) - - -@pytest.mark.skipif(JUPYTER_CLIENT_MAJOR_VERSION >= 7, reason="json_clean is a no-op") -def test_unicode_dict(): - data = {"üniço∂e": "üniço∂e"} - clean = jsonutil.json_clean(data) - assert data == clean