Skip to content

Commit 32269aa

Browse files
authored
PYTHON-4885 Fix legacy extended JSON encoding of DatetimeMS (mongodb#1986)
1 parent 351196b commit 32269aa

File tree

3 files changed

+50
-23
lines changed

3 files changed

+50
-23
lines changed

bson/json_util.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -617,25 +617,28 @@ def _parse_canonical_datetime(
617617
raise TypeError(f"Bad $date, extra field(s): {doc}")
618618
# mongoexport 2.6 and newer
619619
if isinstance(dtm, str):
620-
# Parse offset
621-
if dtm[-1] == "Z":
622-
dt = dtm[:-1]
623-
offset = "Z"
624-
elif dtm[-6] in ("+", "-") and dtm[-3] == ":":
625-
# (+|-)HH:MM
626-
dt = dtm[:-6]
627-
offset = dtm[-6:]
628-
elif dtm[-5] in ("+", "-"):
629-
# (+|-)HHMM
630-
dt = dtm[:-5]
631-
offset = dtm[-5:]
632-
elif dtm[-3] in ("+", "-"):
633-
# (+|-)HH
634-
dt = dtm[:-3]
635-
offset = dtm[-3:]
636-
else:
637-
dt = dtm
638-
offset = ""
620+
try:
621+
# Parse offset
622+
if dtm[-1] == "Z":
623+
dt = dtm[:-1]
624+
offset = "Z"
625+
elif dtm[-6] in ("+", "-") and dtm[-3] == ":":
626+
# (+|-)HH:MM
627+
dt = dtm[:-6]
628+
offset = dtm[-6:]
629+
elif dtm[-5] in ("+", "-"):
630+
# (+|-)HHMM
631+
dt = dtm[:-5]
632+
offset = dtm[-5:]
633+
elif dtm[-3] in ("+", "-"):
634+
# (+|-)HH
635+
dt = dtm[:-3]
636+
offset = dtm[-3:]
637+
else:
638+
dt = dtm
639+
offset = ""
640+
except IndexError as exc:
641+
raise ValueError(f"time data {dtm!r} does not match ISO-8601 datetime format") from exc
639642

640643
# Parse the optional factional seconds portion.
641644
dot_index = dt.rfind(".")
@@ -848,7 +851,7 @@ def _encode_datetimems(obj: Any, json_options: JSONOptions) -> dict:
848851
):
849852
return _encode_datetime(obj.as_datetime(), json_options)
850853
elif json_options.datetime_representation == DatetimeRepresentation.LEGACY:
851-
return {"$date": str(int(obj))}
854+
return {"$date": int(obj)}
852855
return {"$date": {"$numberLong": str(int(obj))}}
853856

854857

doc/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ PyMongo 4.11 brings a number of changes including:
2828
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.bulk_write` now throw an error
2929
when ``ordered=True`` or ``verboseResults=True`` are used with unacknowledged writes.
3030
These are unavoidable breaking changes.
31+
- Fixed a bug in :const:`bson.json_util.dumps` where a :class:`bson.datetime_ms.DatetimeMS` would
32+
be incorrectly encoded as ``'{"$date": "X"}'`` instead of ``'{"$date": X}'`` when using the
33+
legacy MongoDB Extended JSON datetime representation.
34+
- Fixed a bug where :const:`bson.json_util.loads` would raise an IndexError when parsing an invalid
35+
``"$date"`` instead of a ValueError.
3136

3237
Issues Resolved
3338
...............

test/test_json_util.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def test_datetime(self):
137137
'{"dt": { "$date" : "1970-01-01T00:00:00.000Z"}}',
138138
'{"dt": { "$date" : "1970-01-01T00:00:00.000000Z"}}',
139139
'{"dt": { "$date" : "1970-01-01T00:00:00Z"}}',
140-
'{"dt": {"$date": "1970-01-01T00:00:00.000"}}',
140+
'{"dt": { "$date" : "1970-01-01T00:00:00.000"}}',
141141
'{"dt": { "$date" : "1970-01-01T00:00:00"}}',
142142
'{"dt": { "$date" : "1970-01-01T00:00:00.000000"}}',
143143
'{"dt": { "$date" : "1969-12-31T16:00:00.000-0800"}}',
@@ -282,9 +282,9 @@ def test_datetime_ms(self):
282282
opts = JSONOptions(
283283
datetime_representation=DatetimeRepresentation.LEGACY, json_mode=JSONMode.LEGACY
284284
)
285-
self.assertEqual('{"x": {"$date": "-1"}}', json_util.dumps(dat_min, json_options=opts))
285+
self.assertEqual('{"x": {"$date": -1}}', json_util.dumps(dat_min, json_options=opts))
286286
self.assertEqual(
287-
'{"x": {"$date": "' + str(int(dat_max["x"])) + '"}}',
287+
'{"x": {"$date": ' + str(int(dat_max["x"])) + "}}",
288288
json_util.dumps(dat_max, json_options=opts),
289289
)
290290

@@ -317,6 +317,25 @@ def test_datetime_ms(self):
317317
json_util.loads(json_util.dumps(dat_max), json_options=opts)["x"],
318318
)
319319

320+
def test_parse_invalid_date(self):
321+
# These cases should raise ValueError, not IndexError.
322+
for invalid in [
323+
'{"dt": { "$date" : "1970-01-01T00:00:"}}',
324+
'{"dt": { "$date" : "1970-01-01T01:00"}}',
325+
'{"dt": { "$date" : "1970-01-01T01:"}}',
326+
'{"dt": { "$date" : "1970-01-01T01"}}',
327+
'{"dt": { "$date" : "1970-01-01T"}}',
328+
'{"dt": { "$date" : "1970-01-01"}}',
329+
'{"dt": { "$date" : "1970-01-"}}',
330+
'{"dt": { "$date" : "1970-01"}}',
331+
'{"dt": { "$date" : "1970-"}}',
332+
'{"dt": { "$date" : "1970"}}',
333+
'{"dt": { "$date" : "1"}}',
334+
'{"dt": { "$date" : ""}}',
335+
]:
336+
with self.assertRaisesRegex(ValueError, "does not match"):
337+
json_util.loads(invalid)
338+
320339
def test_regex_object_hook(self):
321340
# Extended JSON format regular expression.
322341
pat = "a*b"

0 commit comments

Comments
 (0)