Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions bson/json_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,25 +617,28 @@ def _parse_canonical_datetime(
raise TypeError(f"Bad $date, extra field(s): {doc}")
# mongoexport 2.6 and newer
if isinstance(dtm, str):
# Parse offset
if dtm[-1] == "Z":
dt = dtm[:-1]
offset = "Z"
elif dtm[-6] in ("+", "-") and dtm[-3] == ":":
# (+|-)HH:MM
dt = dtm[:-6]
offset = dtm[-6:]
elif dtm[-5] in ("+", "-"):
# (+|-)HHMM
dt = dtm[:-5]
offset = dtm[-5:]
elif dtm[-3] in ("+", "-"):
# (+|-)HH
dt = dtm[:-3]
offset = dtm[-3:]
else:
dt = dtm
offset = ""
try:
# Parse offset
if dtm[-1] == "Z":
dt = dtm[:-1]
offset = "Z"
elif dtm[-6] in ("+", "-") and dtm[-3] == ":":
# (+|-)HH:MM
dt = dtm[:-6]
offset = dtm[-6:]
elif dtm[-5] in ("+", "-"):
# (+|-)HHMM
dt = dtm[:-5]
offset = dtm[-5:]
elif dtm[-3] in ("+", "-"):
# (+|-)HH
dt = dtm[:-3]
offset = dtm[-3:]
else:
dt = dtm
offset = ""
except IndexError as exc:
raise ValueError(f"time data {dtm!r} does not match ISO-8601 datetime format") from exc

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


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

Issues Resolved
...............
Expand Down
25 changes: 22 additions & 3 deletions test/test_json_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def test_datetime(self):
'{"dt": { "$date" : "1970-01-01T00:00:00.000Z"}}',
'{"dt": { "$date" : "1970-01-01T00:00:00.000000Z"}}',
'{"dt": { "$date" : "1970-01-01T00:00:00Z"}}',
'{"dt": {"$date": "1970-01-01T00:00:00.000"}}',
'{"dt": { "$date" : "1970-01-01T00:00:00.000"}}',
'{"dt": { "$date" : "1970-01-01T00:00:00"}}',
'{"dt": { "$date" : "1970-01-01T00:00:00.000000"}}',
'{"dt": { "$date" : "1969-12-31T16:00:00.000-0800"}}',
Expand Down Expand Up @@ -282,9 +282,9 @@ def test_datetime_ms(self):
opts = JSONOptions(
datetime_representation=DatetimeRepresentation.LEGACY, json_mode=JSONMode.LEGACY
)
self.assertEqual('{"x": {"$date": "-1"}}', json_util.dumps(dat_min, json_options=opts))
self.assertEqual('{"x": {"$date": -1}}', json_util.dumps(dat_min, json_options=opts))
self.assertEqual(
'{"x": {"$date": "' + str(int(dat_max["x"])) + '"}}',
'{"x": {"$date": ' + str(int(dat_max["x"])) + "}}",
json_util.dumps(dat_max, json_options=opts),
)

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

def test_parse_invalid_date(self):
# These cases should raise ValueError, not IndexError.
for invalid in [
'{"dt": { "$date" : "1970-01-01T00:00:"}}',
'{"dt": { "$date" : "1970-01-01T01:00"}}',
'{"dt": { "$date" : "1970-01-01T01:"}}',
'{"dt": { "$date" : "1970-01-01T01"}}',
'{"dt": { "$date" : "1970-01-01T"}}',
'{"dt": { "$date" : "1970-01-01"}}',
'{"dt": { "$date" : "1970-01-"}}',
'{"dt": { "$date" : "1970-01"}}',
'{"dt": { "$date" : "1970-"}}',
'{"dt": { "$date" : "1970"}}',
'{"dt": { "$date" : "1"}}',
'{"dt": { "$date" : ""}}',
]:
with self.assertRaisesRegex(ValueError, "does not match"):
json_util.loads(invalid)

def test_regex_object_hook(self):
# Extended JSON format regular expression.
pat = "a*b"
Expand Down
Loading