From 9b331e252620fb2835ade222bc65714960420885 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 9 Jun 2025 11:17:21 +1000 Subject: [PATCH 01/24] Add orjson_packer and orjson_unpacker to speed up serialization of session messages. --- CHANGELOG.md | 1 + jupyter_client/session.py | 98 ++++++++++++++++++--------------------- pyproject.toml | 1 + tests/test_session.py | 69 ++++++++++++++++++++------- 4 files changed, 100 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12caeba0..fed08eb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Enhancements made - Support psutil for finding network addresses [#1033](https://github.com/jupyter/jupyter_client/pull/1033) ([@juliangilbey](https://github.com/juliangilbey)) +- Add new functions `orjson_packer` and `orjson_unpacker`. ### Bugs fixed diff --git a/jupyter_client/session.py b/jupyter_client/session.py index c387cd06..92f8829d 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -32,6 +32,7 @@ from traitlets import ( Any, Bool, + Callable, CBytes, CUnicode, Dict, @@ -124,6 +125,28 @@ def json_unpacker(s: str | bytes) -> t.Any: return json.loads(s) +try: + import orjson # type:ignore[import-not-found] +except ModuleNotFoundError: + orjson = None + _default_packer_unpacker = "json", "json" + _default_pack_unpack = (json_packer, json_unpacker) +else: + + def orjson_packer(obj: t.Any) -> bytes: + """Convert a json object to a bytes using orjson with a fallback to json_packer.""" + try: + return orjson.dumps( + obj, default=json_default, option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z + ) + except (TypeError, ValueError): + return json_packer(obj) + + orjson_unpacker = orjson.loads + _default_packer_unpacker = "orjson", "orjson" + _default_pack_unpack = (orjson_packer, orjson_unpacker) + + def pickle_packer(o: t.Any) -> bytes: """Pack an object using the pickle module.""" return pickle.dumps(squash_dates(o), PICKLE_PROTOCOL) @@ -131,8 +154,6 @@ def pickle_packer(o: t.Any) -> bytes: pickle_unpacker = pickle.loads -default_packer = json_packer -default_unpacker = json_unpacker DELIM = b"" # singleton dummy tracker, which will always report as done @@ -350,48 +371,43 @@ class Session(Configurable): """, ) + # serialization traits: packer = DottedObjectName( - "json", + _default_packer_unpacker[0], config=True, help="""The name of the packer for serializing messages. Should be one of 'json', 'pickle', or an import name for a custom callable serializer.""", ) - - @observe("packer") - def _packer_changed(self, change: t.Any) -> None: - new = change["new"] - if new.lower() == "json": - self.pack = json_packer - self.unpack = json_unpacker - self.unpacker = new - elif new.lower() == "pickle": - self.pack = pickle_packer - self.unpack = pickle_unpacker - self.unpacker = new - else: - self.pack = import_item(str(new)) - unpacker = DottedObjectName( - "json", + _default_packer_unpacker[1], config=True, help="""The name of the unpacker for unserializing messages. Only used with custom functions for `packer`.""", ) + pack = Callable(_default_pack_unpack[0]) # the actual packer function + unpack = Callable(_default_pack_unpack[1]) # the actual unpacker function - @observe("unpacker") - def _unpacker_changed(self, change: t.Any) -> None: + @observe("packer", "unpacker") + def _packer_unpacker_changed(self, change: t.Any) -> None: new = change["new"] - if new.lower() == "json": + new_ = new.lower() + if new_ == "orjson" and orjson: + self.pack = orjson_packer + self.unpack = orjson_unpacker + self.packer = self.unpacker = new + elif new_ in ["json", "orjson"]: self.pack = json_packer self.unpack = json_unpacker - self.packer = new - elif new.lower() == "pickle": + self.packer = self.unpacker = new + elif new_ == "pickle": self.pack = pickle_packer self.unpack = pickle_unpacker - self.packer = new + self.packer = self.unpacker = new else: - self.unpack = import_item(str(new)) + obj = import_item(str(new)) + name = "pack" if change["name"] == "packer" else "unpack" + self.set_trait(name, obj) session = CUnicode("", config=True, help="""The UUID identifying this session.""") @@ -416,8 +432,7 @@ def _session_changed(self, change: t.Any) -> None: metadata = Dict( {}, config=True, - help="Metadata dictionary, which serves as the default top-level metadata dict for each " - "message.", + help="Metadata dictionary, which serves as the default top-level metadata dict for each message.", ) # if 0, no adapting to do. @@ -486,25 +501,6 @@ def _keyfile_changed(self, change: t.Any) -> None: # for protecting against sends from forks pid = Integer() - # serialization traits: - - pack = Any(default_packer) # the actual packer function - - @observe("pack") - def _pack_changed(self, change: t.Any) -> None: - new = change["new"] - if not callable(new): - raise TypeError("packer must be callable, not %s" % type(new)) - - unpack = Any(default_unpacker) # the actual packer function - - @observe("unpack") - def _unpack_changed(self, change: t.Any) -> None: - # unpacker is not checked - it is assumed to be - new = change["new"] - if not callable(new): - raise TypeError("unpacker must be callable, not %s" % type(new)) - # thresholds: copy_threshold = Integer( 2**16, @@ -514,8 +510,7 @@ def _unpack_changed(self, change: t.Any) -> None: buffer_threshold = Integer( MAX_BYTES, config=True, - help="Threshold (in bytes) beyond which an object's buffer should be extracted to avoid " - "pickling.", + help="Threshold (in bytes) beyond which an object's buffer should be extracted to avoid pickling.", ) item_threshold = Integer( MAX_ITEMS, @@ -625,10 +620,7 @@ def _check_packers(self) -> None: unpacked = unpack(packed) assert unpacked == msg_list except Exception as e: - msg = ( - f"unpacker '{self.unpacker}' could not handle output from packer" - f" '{self.packer}': {e}" - ) + msg = f"unpacker '{self.unpacker}' could not handle output from packer '{self.packer}': {e}" raise ValueError(msg) from e # check datetime support diff --git a/pyproject.toml b/pyproject.toml index 27503497..36f3d281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ requires-python = ">=3.9" dependencies = [ "importlib_metadata>=4.8.3;python_version<\"3.10\"", "jupyter_core>=4.12,!=5.0.*", + "orjson>=3.10.18; implementation_name == 'cpython'", "python-dateutil>=2.8.2", "pyzmq>=23.0", "tornado>=6.2", diff --git a/tests/test_session.py b/tests/test_session.py index f30f44f4..0085b1ca 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -14,6 +14,7 @@ import zmq from dateutil.tz import tzlocal from tornado import ioloop +from traitlets import TraitError from zmq.eventloop.zmqstream import ZMQStream from jupyter_client import jsonutil @@ -40,6 +41,14 @@ def session(): return ss.Session() +serializers = [ + ("json", ss.json_packer, ss.json_unpacker), + ("pickle", ss.pickle_packer, ss.pickle_unpacker), +] +if ss.orjson: + serializers.append(("orjson", ss.orjson_packer, ss.orjson_unpacker)) + + @pytest.mark.usefixtures("no_copy_threshold") class TestSession: def assertEqual(self, a, b): @@ -63,7 +72,11 @@ def test_msg(self, session): self.assertEqual(msg["header"]["msg_type"], "execute") self.assertEqual(msg["msg_type"], "execute") - def test_serialize(self, session): + @pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) + def test_serialize(self, session, packer, pack, unpack): + session.packer = packer + assert session.pack is pack + assert session.unpack is unpack msg = session.msg("execute", content=dict(a=10, b=1.1)) msg_list = session.serialize(msg, ident=b"foo") ident, msg_list = session.feed_identities(msg_list) @@ -233,16 +246,16 @@ async def test_send(self, session): def test_args(self, session): """initialization arguments for Session""" s = session - self.assertTrue(s.pack is ss.default_packer) - self.assertTrue(s.unpack is ss.default_unpacker) + assert s.pack is ss._default_pack_unpack[0] + assert s.unpack is ss._default_pack_unpack[1] self.assertEqual(s.username, os.environ.get("USER", "username")) s = ss.Session() self.assertEqual(s.username, os.environ.get("USER", "username")) - with pytest.raises(TypeError): + with pytest.raises(TraitError): ss.Session(pack="hi") - with pytest.raises(TypeError): + with pytest.raises(TraitError): ss.Session(unpack="hi") u = str(uuid.uuid4()) s = ss.Session(username="carrot", session=u) @@ -490,11 +503,6 @@ async def test_send_raw(self, session): B.close() ctx.term() - def test_set_packer(self, session): - s = session - s.packer = "json" - s.unpacker = "json" - def test_clone(self, session): s = session s._add_digest("initial") @@ -514,14 +522,43 @@ def test_squash_unicode(): assert ss.squash_unicode("hi") == b"hi" -def test_json_packer(): - ss.json_packer(dict(a=1)) - with pytest.raises(ValueError): - ss.json_packer(dict(a=ss.Session())) - ss.json_packer(dict(a=datetime(2021, 4, 1, 12, tzinfo=tzlocal()))) +@pytest.mark.parametrize( + ["description", "data"], + [ + ("dict", [{"a": 1}, [{"a": 1}]]), + ("infinite", [math.inf, ["inf", None]]), + ("datetime", [datetime(2021, 4, 1, 12, tzinfo=tzlocal()), ["2021-04-01T12:00:00+11:00"]]), + ], +) +@pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) +def test_serialize_objects(packer, pack, unpack, description, data): + data_in, data_out_options = data with warnings.catch_warnings(): warnings.simplefilter("ignore") - ss.json_packer(dict(a=math.inf)) + value = pack(data_in) + unpacked = unpack(value) + if (description == "infinite") and (packer == "pickle"): + assert math.isinf(unpacked) + return + assert unpacked in data_out_options + + +@pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) +def test_cannot_serialize(session, packer, pack, unpack): + data = {"a": session} + with pytest.raises((TypeError, ValueError)): + pack(data) + + +@pytest.mark.parametrize("mode", ["packer", "unpacker"]) +@pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) +def test_packer_unpacker(session, packer, pack, unpack, mode): + s: ss.Session = session + s.set_trait(mode, packer) + assert s.pack is pack + assert s.unpack is unpack + mode_reverse = "unpacker" if mode == "packer" else "packer" + assert getattr(s, mode_reverse) == packer def test_message_cls(): From 6ec7306eb1d070b2ee34d8d3a66e42895cab1e67 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 9 Jun 2025 11:53:50 +1000 Subject: [PATCH 02/24] Refactor orjson packer to use functools.partial. --- jupyter_client/session.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 92f8829d..5883cbdb 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -132,16 +132,11 @@ def json_unpacker(s: str | bytes) -> t.Any: _default_packer_unpacker = "json", "json" _default_pack_unpack = (json_packer, json_unpacker) else: + import functools - def orjson_packer(obj: t.Any) -> bytes: - """Convert a json object to a bytes using orjson with a fallback to json_packer.""" - try: - return orjson.dumps( - obj, default=json_default, option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z - ) - except (TypeError, ValueError): - return json_packer(obj) - + orjson_packer = functools.partial( + orjson.dumps, default=json_default, option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z + ) orjson_unpacker = orjson.loads _default_packer_unpacker = "orjson", "orjson" _default_pack_unpack = (orjson_packer, orjson_unpacker) From c92b9c74eb5127a10322dacc18bbf2927bf42c17 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 9 Jun 2025 14:16:35 +1000 Subject: [PATCH 03/24] Add msgpack support for message serialization in Session --- CHANGELOG.md | 2 +- jupyter_client/session.py | 20 ++++++++++++++++---- tests/test_session.py | 6 ++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fed08eb4..1e199a88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ### Enhancements made - Support psutil for finding network addresses [#1033](https://github.com/jupyter/jupyter_client/pull/1033) ([@juliangilbey](https://github.com/juliangilbey)) -- Add new functions `orjson_packer` and `orjson_unpacker`. +- Add new functions `orjson_packer`, `orjson_unpacker`, `msgpack_packer` and `msgpack_unpacker` ### Bugs fixed diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 5883cbdb..2adfaa0f 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -12,6 +12,7 @@ # Distributed under the terms of the Modified BSD License. from __future__ import annotations +import functools import hashlib import hmac import json @@ -132,8 +133,6 @@ def json_unpacker(s: str | bytes) -> t.Any: _default_packer_unpacker = "json", "json" _default_pack_unpack = (json_packer, json_unpacker) else: - import functools - orjson_packer = functools.partial( orjson.dumps, default=json_default, option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z ) @@ -141,6 +140,15 @@ def json_unpacker(s: str | bytes) -> t.Any: _default_packer_unpacker = "orjson", "orjson" _default_pack_unpack = (orjson_packer, orjson_unpacker) +try: + import msgpack # type:ignore[import-not-found] + +except ModuleNotFoundError: + msgpack = None +else: + msgpack_packer = functools.partial(msgpack.packb, default=json_default) + msgpack_unpacker = msgpack.unpackb + def pickle_packer(o: t.Any) -> bytes: """Pack an object using the pickle module.""" @@ -331,7 +339,7 @@ class Session(Configurable): debug : bool whether to trigger extra debugging statements - packer/unpacker : str : 'json', 'pickle' or import_string + packer/unpacker : str : 'orjson', 'json', 'pickle', 'msgpack' or import_string importstrings for methods to serialize message parts. If just 'json' or 'pickle', predefined JSON and pickle packers will be used. Otherwise, the entire importstring must be used. @@ -399,6 +407,10 @@ def _packer_unpacker_changed(self, change: t.Any) -> None: self.pack = pickle_packer self.unpack = pickle_unpacker self.packer = self.unpacker = new + elif new_ == "msgpack": + self.pack = msgpack_packer + self.unpack = msgpack_unpacker + self.packer = self.unpacker = new else: obj = import_item(str(new)) name = "pack" if change["name"] == "packer" else "unpack" @@ -523,7 +535,7 @@ def __init__(self, **kwargs: t.Any) -> None: debug : bool whether to trigger extra debugging statements - packer/unpacker : str : 'json', 'pickle' or import_string + packer/unpacker : str : 'orjson', 'json', 'pickle', 'msgpack' or import_string importstrings for methods to serialize message parts. If just 'json' or 'pickle', predefined JSON and pickle packers will be used. Otherwise, the entire importstring must be used. diff --git a/tests/test_session.py b/tests/test_session.py index 0085b1ca..34c0203d 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -47,6 +47,8 @@ def session(): ] if ss.orjson: serializers.append(("orjson", ss.orjson_packer, ss.orjson_unpacker)) +if ss.msgpack: + serializers.append(("msgpack", ss.msgpack_packer, ss.msgpack_unpacker)) @pytest.mark.usefixtures("no_copy_threshold") @@ -537,7 +539,7 @@ def test_serialize_objects(packer, pack, unpack, description, data): warnings.simplefilter("ignore") value = pack(data_in) unpacked = unpack(value) - if (description == "infinite") and (packer == "pickle"): + if (description == "infinite") and (packer in ["pickle", "msgpack"]): assert math.isinf(unpacked) return assert unpacked in data_out_options @@ -552,7 +554,7 @@ def test_cannot_serialize(session, packer, pack, unpack): @pytest.mark.parametrize("mode", ["packer", "unpacker"]) @pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) -def test_packer_unpacker(session, packer, pack, unpack, mode): +def test_pack_unpack(session, packer, pack, unpack, mode): s: ss.Session = session s.set_trait(mode, packer) assert s.pack is pack From c03c7a0a84aeb2f511a8f63511496e6a73e9ea4b Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 9 Jun 2025 20:35:04 +1000 Subject: [PATCH 04/24] Refactor packer/unpacker change handling for improved readability --- CHANGELOG.md | 1 - jupyter_client/session.py | 31 ++++++++++++------------------- tests/test_session.py | 6 +++++- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e199a88..12caeba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ ### Enhancements made - Support psutil for finding network addresses [#1033](https://github.com/jupyter/jupyter_client/pull/1033) ([@juliangilbey](https://github.com/juliangilbey)) -- Add new functions `orjson_packer`, `orjson_unpacker`, `msgpack_packer` and `msgpack_unpacker` ### Bugs fixed diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 2adfaa0f..799b08d6 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -393,28 +393,21 @@ class Session(Configurable): @observe("packer", "unpacker") def _packer_unpacker_changed(self, change: t.Any) -> None: - new = change["new"] - new_ = new.lower() - if new_ == "orjson" and orjson: - self.pack = orjson_packer - self.unpack = orjson_unpacker - self.packer = self.unpacker = new - elif new_ in ["json", "orjson"]: - self.pack = json_packer - self.unpack = json_unpacker - self.packer = self.unpacker = new - elif new_ == "pickle": - self.pack = pickle_packer - self.unpack = pickle_unpacker - self.packer = self.unpacker = new - elif new_ == "msgpack": - self.pack = msgpack_packer - self.unpack = msgpack_unpacker - self.packer = self.unpacker = new + new = change["new"].lower() + if new == "orjson" and orjson: + self.pack, self.unpack = orjson_packer, orjson_unpacker + elif new == "json" or new == "orjson": + self.pack, self.unpack = json_packer, json_unpacker + elif new == "pickle": + self.pack, self.unpack = pickle_packer, pickle_unpacker + elif new == "msgpack" and msgpack: + self.pack, self.unpack = msgpack_packer, msgpack_unpacker else: - obj = import_item(str(new)) + obj = import_item(str(change["new"])) name = "pack" if change["name"] == "packer" else "unpack" self.set_trait(name, obj) + return + self.packer = self.unpacker = change["new"] session = CUnicode("", config=True, help="""The UUID identifying this session.""") diff --git a/tests/test_session.py b/tests/test_session.py index 34c0203d..694f9922 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -524,12 +524,16 @@ def test_squash_unicode(): assert ss.squash_unicode("hi") == b"hi" +_sample_time = datetime(2021, 4, 1, 12, tzinfo=tzlocal()) +_sample_time_string = ss.json_default(_sample_time) + + @pytest.mark.parametrize( ["description", "data"], [ ("dict", [{"a": 1}, [{"a": 1}]]), ("infinite", [math.inf, ["inf", None]]), - ("datetime", [datetime(2021, 4, 1, 12, tzinfo=tzlocal()), ["2021-04-01T12:00:00+11:00"]]), + ("datetime", [_sample_time, [_sample_time_string]]), ], ) @pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) From f4d15806f76f87076541e88ffa765d405035e132 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 9 Jun 2025 21:00:24 +1000 Subject: [PATCH 05/24] Fix test_serialize_objects datetime checks on ci to compare using datetime format. --- tests/test_session.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_session.py b/tests/test_session.py index 694f9922..c1c50f1c 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -524,16 +524,12 @@ def test_squash_unicode(): assert ss.squash_unicode("hi") == b"hi" -_sample_time = datetime(2021, 4, 1, 12, tzinfo=tzlocal()) -_sample_time_string = ss.json_default(_sample_time) - - @pytest.mark.parametrize( ["description", "data"], [ ("dict", [{"a": 1}, [{"a": 1}]]), ("infinite", [math.inf, ["inf", None]]), - ("datetime", [_sample_time, [_sample_time_string]]), + ("datetime", [datetime(2021, 4, 1, 12, tzinfo=tzlocal()), []]), ], ) @pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) @@ -545,8 +541,10 @@ def test_serialize_objects(packer, pack, unpack, description, data): unpacked = unpack(value) if (description == "infinite") and (packer in ["pickle", "msgpack"]): assert math.isinf(unpacked) - return - assert unpacked in data_out_options + elif description == "datetime": + assert data_in == datetime.fromisoformat(unpacked) + else: + assert unpacked in data_out_options @pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) From fd29e5d8689c4e85223c83f6c61fd89332ceba76 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 9 Jun 2025 21:59:03 +1000 Subject: [PATCH 06/24] Fix datetime deserialization in test_serialize_objects to use dateutil.parser.isoparse --- tests/test_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_session.py b/tests/test_session.py index c1c50f1c..340ad4ed 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -10,6 +10,7 @@ from datetime import datetime from unittest import mock +import dateutil.parser import pytest import zmq from dateutil.tz import tzlocal @@ -542,7 +543,7 @@ def test_serialize_objects(packer, pack, unpack, description, data): if (description == "infinite") and (packer in ["pickle", "msgpack"]): assert math.isinf(unpacked) elif description == "datetime": - assert data_in == datetime.fromisoformat(unpacked) + assert data_in == dateutil.parser.isoparse(unpacked) else: assert unpacked in data_out_options From cec22e6e97101f0b4393cbd0a8a00d3a7585c7e6 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 9 Jun 2025 22:12:49 +1000 Subject: [PATCH 07/24] Add PicklingError to exception handling in test_cannot_serialize --- tests/test_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_session.py b/tests/test_session.py index 340ad4ed..257df5c8 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -8,6 +8,7 @@ import uuid import warnings from datetime import datetime +from pickle import PicklingError from unittest import mock import dateutil.parser @@ -551,7 +552,7 @@ def test_serialize_objects(packer, pack, unpack, description, data): @pytest.mark.parametrize(["packer", "pack", "unpack"], serializers) def test_cannot_serialize(session, packer, pack, unpack): data = {"a": session} - with pytest.raises((TypeError, ValueError)): + with pytest.raises((TypeError, ValueError, PicklingError)): pack(data) From 452103bafa17573206b77d78062b2c0dea2bc04c Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Tue, 10 Jun 2025 07:23:48 +1000 Subject: [PATCH 08/24] Update api docs --- docs/api/jupyter_client.asynchronous.rst | 4 +-- docs/api/jupyter_client.blocking.rst | 4 +-- docs/api/jupyter_client.ioloop.rst | 6 ++-- docs/api/jupyter_client.provisioning.rst | 8 ++--- docs/api/jupyter_client.rst | 46 ++++++++++++------------ docs/api/jupyter_client.ssh.rst | 6 ++-- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/api/jupyter_client.asynchronous.rst b/docs/api/jupyter_client.asynchronous.rst index 5fee3b81..7377df34 100644 --- a/docs/api/jupyter_client.asynchronous.rst +++ b/docs/api/jupyter_client.asynchronous.rst @@ -7,13 +7,13 @@ Submodules .. automodule:: jupyter_client.asynchronous.client :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: jupyter_client.asynchronous :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/api/jupyter_client.blocking.rst b/docs/api/jupyter_client.blocking.rst index ba194cbf..e74eb840 100644 --- a/docs/api/jupyter_client.blocking.rst +++ b/docs/api/jupyter_client.blocking.rst @@ -7,13 +7,13 @@ Submodules .. automodule:: jupyter_client.blocking.client :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: jupyter_client.blocking :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/api/jupyter_client.ioloop.rst b/docs/api/jupyter_client.ioloop.rst index 2c06a832..f1d9d909 100644 --- a/docs/api/jupyter_client.ioloop.rst +++ b/docs/api/jupyter_client.ioloop.rst @@ -7,19 +7,19 @@ Submodules .. automodule:: jupyter_client.ioloop.manager :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.ioloop.restarter :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: jupyter_client.ioloop :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/api/jupyter_client.provisioning.rst b/docs/api/jupyter_client.provisioning.rst index 5a2de00f..80c431a7 100644 --- a/docs/api/jupyter_client.provisioning.rst +++ b/docs/api/jupyter_client.provisioning.rst @@ -7,25 +7,25 @@ Submodules .. automodule:: jupyter_client.provisioning.factory :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.provisioning.local_provisioner :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.provisioning.provisioner_base :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: jupyter_client.provisioning :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/api/jupyter_client.rst b/docs/api/jupyter_client.rst index 336dcc2d..797ceae1 100644 --- a/docs/api/jupyter_client.rst +++ b/docs/api/jupyter_client.rst @@ -19,139 +19,139 @@ Submodules .. automodule:: jupyter_client.adapter :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.channels :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.channelsabc :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.client :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.clientabc :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.connect :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.consoleapp :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.jsonutil :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.kernelapp :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.kernelspec :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.kernelspecapp :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.launcher :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.localinterfaces :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.manager :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.managerabc :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.multikernelmanager :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.restarter :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.runapp :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.session :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.threaded :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.utils :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.win_interrupt :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: jupyter_client :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/api/jupyter_client.ssh.rst b/docs/api/jupyter_client.ssh.rst index 06a1d9db..9ad3c79b 100644 --- a/docs/api/jupyter_client.ssh.rst +++ b/docs/api/jupyter_client.ssh.rst @@ -7,19 +7,19 @@ Submodules .. automodule:: jupyter_client.ssh.forward :members: - :undoc-members: :show-inheritance: + :undoc-members: .. automodule:: jupyter_client.ssh.tunnel :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: jupyter_client.ssh :members: - :undoc-members: :show-inheritance: + :undoc-members: From 7386b1db9f7a93c108905224029bc6334f00037c Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Tue, 10 Jun 2025 14:12:16 +1000 Subject: [PATCH 09/24] Replace dateutil.parser.isoparse with jsonutil.parse_date in test_serialize_objects for datetime validation --- tests/test_session.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_session.py b/tests/test_session.py index 257df5c8..88117d05 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -11,7 +11,6 @@ from pickle import PicklingError from unittest import mock -import dateutil.parser import pytest import zmq from dateutil.tz import tzlocal @@ -544,7 +543,7 @@ def test_serialize_objects(packer, pack, unpack, description, data): if (description == "infinite") and (packer in ["pickle", "msgpack"]): assert math.isinf(unpacked) elif description == "datetime": - assert data_in == dateutil.parser.isoparse(unpacked) + assert data_in == jsonutil.parse_date(unpacked) else: assert unpacked in data_out_options From e2f003ab92c2401a89236e51b4f19244bb6d3463 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Fri, 17 Oct 2025 08:50:40 +1100 Subject: [PATCH 10/24] Use rep in fstring Co-authored-by: M Bussonnier --- 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 799b08d6..40f60503 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -620,7 +620,7 @@ def _check_packers(self) -> None: unpacked = unpack(packed) assert unpacked == msg_list except Exception as e: - msg = f"unpacker '{self.unpacker}' could not handle output from packer '{self.packer}': {e}" + msg = f"unpacker {self.unpacker!r} could not handle output from packer {self.packer!r}: {e}" raise ValueError(msg) from e # check datetime support From 260c07c24c8ab92b711ac0ec09cc360e67bb35eb Mon Sep 17 00:00:00 2001 From: Alan <> Date: Sat, 18 Oct 2025 09:35:04 +1100 Subject: [PATCH 11/24] Add msgpack as a test dependency. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0cefd33e..cf4378f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ test = [ "pytest-jupyter[client]>=0.6.2", "pytest-cov", "pytest-timeout", + "msgpack" ] docs = [ "ipykernel", From aac1ecf496a75216939420ef6f8b910084d77b1f Mon Sep 17 00:00:00 2001 From: Alan <> Date: Fri, 14 Nov 2025 10:22:37 +1100 Subject: [PATCH 12/24] Fallback to json_packer and json_unpacker for orjson to handle for better resilience. --- jupyter_client/session.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 435c2009..8cfd892d 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -131,15 +131,25 @@ def json_unpacker(s: str | bytes) -> t.Any: import orjson # type:ignore[import-not-found] except ModuleNotFoundError: orjson = None - _default_packer_unpacker = "json", "json" - _default_pack_unpack = (json_packer, json_unpacker) + orjson_packer, orjson_unpacker = json_packer, json_unpacker else: - orjson_packer = functools.partial( - orjson.dumps, default=json_default, option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z - ) - orjson_unpacker = orjson.loads - _default_packer_unpacker = "orjson", "orjson" - _default_pack_unpack = (orjson_packer, orjson_unpacker) + + def orjson_packer(obj, *, options=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z) -> bytes: + """Convert a json object to a bytes using orjson with fallback to json_packer.""" + try: + return orjson.dumps(obj, default=json_default, options=options) + except Exception: + pass + return json_packer(obj) + + def orjson_unpacker(s: str | bytes) -> t.Any: + """Convert a json bytes or string to an object using orjson with fallback to json_unpacker.""" + try: + orjson.loads(s) + except Exception: + pass + return json_unpacker(s) + try: import msgpack # type:ignore[import-not-found] @@ -377,20 +387,20 @@ class Session(Configurable): # serialization traits: packer = DottedObjectName( - _default_packer_unpacker[0], + "orjson" if orjson else "json", config=True, help="""The name of the packer for serializing messages. Should be one of 'json', 'pickle', or an import name for a custom callable serializer.""", ) unpacker = DottedObjectName( - _default_packer_unpacker[1], + "orjson" if orjson else "json", config=True, help="""The name of the unpacker for unserializing messages. Only used with custom functions for `packer`.""", ) - pack = Callable(_default_pack_unpack[0]) # the actual packer function - unpack = Callable(_default_pack_unpack[1]) # the actual unpacker function + pack = Callable(orjson_packer if orjson else json_packer) # the actual packer function + unpack = Callable(orjson_unpacker if orjson else json_unpacker) # the actual unpacker function @observe("packer", "unpacker") def _packer_unpacker_changed(self, change: t.Any) -> None: From 58e1405733b068f785ff47bef1516b63e5c4513e Mon Sep 17 00:00:00 2001 From: Alan <> Date: Wed, 10 Dec 2025 09:21:09 +1100 Subject: [PATCH 13/24] Fix: test_args checking for removed _default_pack_unpack and _default_pack_unpack. --- tests/test_session.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_session.py b/tests/test_session.py index b7a136b9..97bc6eba 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -250,8 +250,6 @@ async def test_send(self, session): def test_args(self, session): """initialization arguments for Session""" s = session - assert s.pack is ss._default_pack_unpack[0] - assert s.unpack is ss._default_pack_unpack[1] self.assertEqual(s.username, os.environ.get("USER", "username")) s = ss.Session() From 1906968fb5cc26ab54b704f8e8ce14503f42be09 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Wed, 10 Dec 2025 09:23:32 +1100 Subject: [PATCH 14/24] Add type annotation to orjson_packer. --- 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 8cfd892d..80c5c06b 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -134,7 +134,7 @@ def json_unpacker(s: str | bytes) -> t.Any: orjson_packer, orjson_unpacker = json_packer, json_unpacker else: - def orjson_packer(obj, *, options=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z) -> bytes: + def orjson_packer(obj: t.Any, *, options=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z) -> bytes: """Convert a json object to a bytes using orjson with fallback to json_packer.""" try: return orjson.dumps(obj, default=json_default, options=options) From 021f3feb5d8ebc78eeffc9e4ea71b1c19f866e85 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Wed, 10 Dec 2025 10:32:16 +1100 Subject: [PATCH 15/24] Add the other missing type annoatation. --- jupyter_client/session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 80c5c06b..4484ad85 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -134,7 +134,9 @@ def json_unpacker(s: str | bytes) -> t.Any: orjson_packer, orjson_unpacker = json_packer, json_unpacker else: - def orjson_packer(obj: t.Any, *, options=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z) -> bytes: + def orjson_packer( + obj: t.Any, *, options: int | None = orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z + ) -> bytes: """Convert a json object to a bytes using orjson with fallback to json_packer.""" try: return orjson.dumps(obj, default=json_default, options=options) From b7a03887acf882f88574e57615ac65f41f5b49d9 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Thu, 11 Dec 2025 08:00:18 +1100 Subject: [PATCH 16/24] Change orjson from a dependency to an optional dependency. Test against the minimum version in CI. --- .github/workflows/main.yml | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d0ac44c8..5e750d52 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -143,6 +143,11 @@ jobs: run: | hatch -vv run test:nowarn + - name: Run the unit tests with orjson installed + run: | + hatch -e test run pip install orjson + hatch -vv run test:nowarn + test_prereleases: name: Test Prereleases timeout-minutes: 10 diff --git a/pyproject.toml b/pyproject.toml index 18d205e8..fcda69f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "jupyter_core>=5.1", - "orjson>=3.10.18; implementation_name == 'cpython'", "python-dateutil>=2.8.2", "pyzmq>=25.0", "tornado>=6.4.1", @@ -67,6 +66,7 @@ docs = [ "sphinxcontrib-spelling", "sphinx-autodoc-typehints", ] +orjson = ["orjson"] # When orjson is installed it will be used for faster pack and unpack [project.scripts] jupyter-kernelspec = "jupyter_client.kernelspecapp:KernelSpecApp.launch_instance" From 547a74eca443878f6800e72c25cd3f6407319354 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Thu, 11 Dec 2025 08:57:12 +1100 Subject: [PATCH 17/24] Double timeout for test_minimum_verisons --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5e750d52..14c39a5e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -126,7 +126,7 @@ jobs: test_minimum_verisons: name: Test Minimum Versions runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 From 5a24ca0e1facb48ab539611513c26178580c3e07 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Thu, 11 Dec 2025 09:28:03 +1100 Subject: [PATCH 18/24] Fix invalid argument name. --- jupyter_client/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 4484ad85..8191798d 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -135,11 +135,11 @@ def json_unpacker(s: str | bytes) -> t.Any: else: def orjson_packer( - obj: t.Any, *, options: int | None = orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z + obj: t.Any, *, option: int | None = orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z ) -> bytes: """Convert a json object to a bytes using orjson with fallback to json_packer.""" try: - return orjson.dumps(obj, default=json_default, options=options) + return orjson.dumps(obj, default=json_default, option=option) except Exception: pass return json_packer(obj) From 86257c8a224df234e4656b8bb9eec0ed8e745e95 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Fri, 12 Dec 2025 08:22:43 +1100 Subject: [PATCH 19/24] Add orjson and msgpack as additional_dependencies for mypy in .pre-commit-config.yaml. --- .pre-commit-config.yaml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c74d89a..f9986dde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,14 +37,20 @@ repos: types_or: [yaml, html, json] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.18.2" + rev: "v1.19.0" hooks: - id: mypy files: jupyter_client stages: [manual] args: ["--install-types", "--non-interactive"] additional_dependencies: - ["traitlets>=5.13", "ipykernel>=6.26", "jupyter_core>=5.3.2"] + [ + "traitlets>=5.13", + "ipykernel>=6.26", + "jupyter_core>=5.3.2", + "orjson>=3.11.4", + "msgpack>=1.1.2", + ] - repo: https://github.com/adamchainz/blacken-docs rev: "1.20.0" From 4518a011fdf9d6cc7494a23c21a18e376d5ab5c8 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Fri, 12 Dec 2025 08:47:54 +1100 Subject: [PATCH 20/24] Remove # type:ignore[import-not-found]. --- jupyter_client/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 8191798d..7488c70f 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -128,7 +128,7 @@ def json_unpacker(s: str | bytes) -> t.Any: try: - import orjson # type:ignore[import-not-found] + import orjson except ModuleNotFoundError: orjson = None orjson_packer, orjson_unpacker = json_packer, json_unpacker @@ -154,7 +154,7 @@ def orjson_unpacker(s: str | bytes) -> t.Any: try: - import msgpack # type:ignore[import-not-found] + import msgpack except ModuleNotFoundError: msgpack = None From 65b2e6104a5861ee0e0fe8a8baaba82532f9cb40 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Fri, 12 Dec 2025 09:19:28 +1100 Subject: [PATCH 21/24] Should return result of orjson.loads. --- jupyter_client/session.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 7488c70f..2241965d 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -16,6 +16,7 @@ import functools import hashlib import hmac +import importlib.util import json import logging import os @@ -127,12 +128,13 @@ def json_unpacker(s: str | bytes) -> t.Any: return json.loads(s) -try: +orjson = None +orjson_packer, orjson_unpacker = json_packer, json_unpacker + +if importlib.util.find_spec("orjson"): import orjson -except ModuleNotFoundError: - orjson = None - orjson_packer, orjson_unpacker = json_packer, json_unpacker -else: + + assert orjson def orjson_packer( obj: t.Any, *, option: int | None = orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z @@ -141,16 +143,14 @@ def orjson_packer( try: return orjson.dumps(obj, default=json_default, option=option) except Exception: - pass - return json_packer(obj) + return json_packer(obj) def orjson_unpacker(s: str | bytes) -> t.Any: """Convert a json bytes or string to an object using orjson with fallback to json_unpacker.""" try: - orjson.loads(s) + return orjson.loads(s) except Exception: - pass - return json_unpacker(s) + return json_unpacker(s) try: From d95030ccd227df0c127b8738698cd2a1be2d61a0 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 11 Dec 2025 19:51:37 -0500 Subject: [PATCH 22/24] fix: get mypy working with orjson/msgpack Signed-off-by: Henry Schreiner --- .pre-commit-config.yaml | 12 +++++------- jupyter_client/session.py | 30 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9986dde..c18bafea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,13 +44,11 @@ repos: stages: [manual] args: ["--install-types", "--non-interactive"] additional_dependencies: - [ - "traitlets>=5.13", - "ipykernel>=6.26", - "jupyter_core>=5.3.2", - "orjson>=3.11.4", - "msgpack>=1.1.2", - ] + - traitlets>=5.13 + - ipykernel>=6.26 + - jupyter_core>=5.3.2 + - orjson>=3.11.4 + - msgpack-types - repo: https://github.com/adamchainz/blacken-docs rev: "1.20.0" diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 2241965d..f812ff60 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -128,20 +128,20 @@ def json_unpacker(s: str | bytes) -> t.Any: return json.loads(s) -orjson = None -orjson_packer, orjson_unpacker = json_packer, json_unpacker - -if importlib.util.find_spec("orjson"): +try: import orjson - - assert orjson +except ModuleNotFoundError: + has_orjson = False + orjson_packer, orjson_unpacker = json_packer, json_unpacker +else: + has_orjson = True def orjson_packer( obj: t.Any, *, option: int | None = orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z ) -> bytes: """Convert a json object to a bytes using orjson with fallback to json_packer.""" try: - return orjson.dumps(obj, default=json_default, option=option) + return orjson.dumps(obj, default=json_default, options=option) except Exception: return json_packer(obj) @@ -155,10 +155,10 @@ def orjson_unpacker(s: str | bytes) -> t.Any: try: import msgpack - except ModuleNotFoundError: - msgpack = None + has_msgpack = False else: + has_msgpack = True msgpack_packer = functools.partial(msgpack.packb, default=json_default) msgpack_unpacker = msgpack.unpackb @@ -389,31 +389,31 @@ class Session(Configurable): # serialization traits: packer = DottedObjectName( - "orjson" if orjson else "json", + "orjson" if has_orjson else "json", config=True, help="""The name of the packer for serializing messages. Should be one of 'json', 'pickle', or an import name for a custom callable serializer.""", ) unpacker = DottedObjectName( - "orjson" if orjson else "json", + "orjson" if has_orjson else "json", config=True, help="""The name of the unpacker for unserializing messages. Only used with custom functions for `packer`.""", ) - pack = Callable(orjson_packer if orjson else json_packer) # the actual packer function - unpack = Callable(orjson_unpacker if orjson else json_unpacker) # the actual unpacker function + pack = Callable(orjson_packer if has_orjson else json_packer) # the actual packer function + unpack = Callable(orjson_unpacker if has_orjson else json_unpacker) # the actual unpacker function @observe("packer", "unpacker") def _packer_unpacker_changed(self, change: t.Any) -> None: new = change["new"].lower() - if new == "orjson" and orjson: + if new == "orjson" and has_orjson: self.pack, self.unpack = orjson_packer, orjson_unpacker elif new == "json" or new == "orjson": self.pack, self.unpack = json_packer, json_unpacker elif new == "pickle": self.pack, self.unpack = pickle_packer, pickle_unpacker - elif new == "msgpack" and msgpack: + elif new == "msgpack" and has_msgpack: self.pack, self.unpack = msgpack_packer, msgpack_unpacker else: obj = import_item(str(change["new"])) From d1c1ad17dd7eb01163927312858ccfcd4853234f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 00:52:20 +0000 Subject: [PATCH 23/24] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- jupyter_client/session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index f812ff60..24026567 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -402,7 +402,9 @@ class Session(Configurable): Only used with custom functions for `packer`.""", ) pack = Callable(orjson_packer if has_orjson else json_packer) # the actual packer function - unpack = Callable(orjson_unpacker if has_orjson else json_unpacker) # the actual unpacker function + unpack = Callable( + orjson_unpacker if has_orjson else json_unpacker + ) # the actual unpacker function @observe("packer", "unpacker") def _packer_unpacker_changed(self, change: t.Any) -> None: From 27d68be4f66c25e474bee11673ffb0a9641d5957 Mon Sep 17 00:00:00 2001 From: Alan <> Date: Fri, 12 Dec 2025 19:33:29 +1100 Subject: [PATCH 24/24] Fix for previous refactor. --- jupyter_client/session.py | 3 +-- tests/test_session.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 24026567..aa28be68 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -16,7 +16,6 @@ import functools import hashlib import hmac -import importlib.util import json import logging import os @@ -141,7 +140,7 @@ def orjson_packer( ) -> bytes: """Convert a json object to a bytes using orjson with fallback to json_packer.""" try: - return orjson.dumps(obj, default=json_default, options=option) + return orjson.dumps(obj, default=json_default, option=option) except Exception: return json_packer(obj) diff --git a/tests/test_session.py b/tests/test_session.py index 97bc6eba..61a298d5 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -47,9 +47,9 @@ def session(): ("json", ss.json_packer, ss.json_unpacker), ("pickle", ss.pickle_packer, ss.pickle_unpacker), ] -if ss.orjson: +if ss.has_orjson: serializers.append(("orjson", ss.orjson_packer, ss.orjson_unpacker)) -if ss.msgpack: +if ss.has_msgpack: serializers.append(("msgpack", ss.msgpack_packer, ss.msgpack_unpacker))