diff --git a/jupyter_client/session.py b/jupyter_client/session.py index c387cd06..9f3b2fad 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -41,6 +41,7 @@ Set, TraitError, Unicode, + default, observe, ) from traitlets.config.configurable import Configurable, LoggingConfigurable @@ -420,6 +421,36 @@ def _session_changed(self, change: t.Any) -> None: "message.", ) + extract_header_dates = Bool( + True, + config=True, + help=""" + Parse timestamps in message headers to datetime objects. + + If True, `date` and other timestamp fields + will be `datetime.datetime` objects. + If False, they will be ISO8601 strings. + + Parsing has a performance cost and is deprecated, + but kept as default for backward compatibility. + """, + ) + + @default("extract_header_dates") + def _extract_header_dates_default(self): + msg = """Session.extract_header_dates = True is deprecated in jupyter-client 8.6 + + set cfg.Session.extract_header_dates = False + or JUPYTER_SESSION_EXTRACT_HEADER_DATES=0 + to avoid this message. + """ + env_value = os.environ.get("JUPYTER_SESSION_EXTRACT_HEADER_DATES", "") != "0" + if env_value: + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return True + else: + return False + # if 0, no adapting to do. adapt_version = Integer(0) @@ -1076,10 +1107,14 @@ def deserialize( msg = "malformed message, must have at least %i elements" % minlen raise TypeError(msg) header = self.unpack(msg_list[1]) - message["header"] = extract_dates(header) + parent_header = self.unpack(msg_list[2]) + if self.extract_header_dates: + header = extract_dates(header) + parent_header = extract_dates(parent_header) + message["header"] = header message["msg_id"] = header["msg_id"] message["msg_type"] = header["msg_type"] - message["parent_header"] = extract_dates(self.unpack(msg_list[2])) + message["parent_header"] = parent_header message["metadata"] = self.unpack(msg_list[3]) if content: message["content"] = self.unpack(msg_list[4]) diff --git a/pyproject.toml b/pyproject.toml index c2be8205..9012ea3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,6 +133,8 @@ filterwarnings= [ # from python-dateutil "ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning", "ignore:datetime.datetime.utcnow:DeprecationWarning", + # ignore header dates warning + "ignore:.*extract_header_dates.*:DeprecationWarning", ] [tool.coverage.report] diff --git a/tests/test_session.py b/tests/test_session.py index f30f44f4..2bcb6ffd 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -411,10 +411,14 @@ def _datetime_test(self, session): msg = session.msg("msg", content=content, metadata=metadata, parent=p["header"]) smsg = session.serialize(msg) msg2 = session.deserialize(session.feed_identities(smsg)[1]) - assert isinstance(msg2["header"]["date"], datetime) - self.assertEqual(msg["header"], msg2["header"]) - self.assertEqual(msg["parent_header"], msg2["parent_header"]) - self.assertEqual(msg["parent_header"], msg2["parent_header"]) + if session.extract_header_dates: + date_type = datetime + else: + date_type = str + assert isinstance(msg2["header"]["date"], date_type) + assert isinstance(msg2["parent_header"]["date"], date_type) + self.assertEqual(msg["header"], jsonutil.extract_dates(msg2["header"])) + self.assertEqual(msg["parent_header"], jsonutil.extract_dates(msg2["parent_header"])) assert isinstance(msg["content"]["t"], datetime) assert isinstance(msg["metadata"]["t"], datetime) assert isinstance(msg2["content"]["t"], str) @@ -422,7 +426,9 @@ def _datetime_test(self, session): self.assertEqual(msg["content"], jsonutil.extract_dates(msg2["content"])) self.assertEqual(msg["content"], jsonutil.extract_dates(msg2["content"])) - def test_datetimes(self, session): + @pytest.mark.parametrize("extract_header_dates", [True, False]) + def test_datetimes(self, session, extract_header_dates): + session.extract_header_dates = extract_header_dates self._datetime_test(session) def test_datetimes_pickle(self):