Skip to content

PYTHON-2046 Change default JSONMode from LEGACY to RELAXED #711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 8, 2021
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
107 changes: 67 additions & 40 deletions bson/json_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
This module provides two helper methods `dumps` and `loads` that wrap the
native :mod:`json` methods and provide explicit BSON conversion to and from
JSON. :class:`~bson.json_util.JSONOptions` provides a way to control how JSON
is emitted and parsed, with the default being the legacy PyMongo format.
:mod:`~bson.json_util` can also generate Canonical or Relaxed `Extended JSON`_
when :const:`CANONICAL_JSON_OPTIONS` or :const:`RELAXED_JSON_OPTIONS` is
is emitted and parsed, with the default being the Relaxed Extended JSON format.
:mod:`~bson.json_util` can also generate Canonical or legacy `Extended JSON`_
when :const:`CANONICAL_JSON_OPTIONS` or :const:`LEGACY_JSON_OPTIONS` is
provided, respectively.

.. _Extended JSON: https://github.com/mongodb/specifications/blob/master/source/extended-json.rst
Expand All @@ -32,17 +32,17 @@
>>> loads('[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": "80", "$binary": "AQIDBA=="}}]')
[{'foo': [1, 2]}, {'bar': {'hello': 'world'}}, {'code': Code('function x() { return 1; }', {})}, {'bin': Binary(b'...', 128)}]

Example usage (serialization):
Example usage with :const:`RELAXED_JSON_OPTIONS` (the default):

.. doctest::

>>> from bson import Binary, Code
>>> from bson.json_util import dumps
>>> dumps([{'foo': [1, 2]},
... {'bar': {'hello': 'world'}},
... {'code': Code("function x() { return 1; }", {})},
... {'code': Code("function x() { return 1; }")},
... {'bin': Binary(b"\x01\x02\x03\x04")}])
'[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]'
'[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]'

Example usage (with :const:`CANONICAL_JSON_OPTIONS`):

Expand All @@ -57,18 +57,18 @@
... json_options=CANONICAL_JSON_OPTIONS)
'[{"foo": [{"$numberInt": "1"}, {"$numberInt": "2"}]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]'

Example usage (with :const:`RELAXED_JSON_OPTIONS`):
Example usage (with :const:`LEGACY_JSON_OPTIONS`):

.. doctest::

>>> from bson import Binary, Code
>>> from bson.json_util import dumps, RELAXED_JSON_OPTIONS
>>> from bson.json_util import dumps, LEGACY_JSON_OPTIONS
>>> dumps([{'foo': [1, 2]},
... {'bar': {'hello': 'world'}},
... {'code': Code("function x() { return 1; }")},
... {'code': Code("function x() { return 1; }", {})},
... {'bin': Binary(b"\x01\x02\x03\x04")}],
... json_options=RELAXED_JSON_OPTIONS)
'[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]'
... json_options=LEGACY_JSON_OPTIONS)
'[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]'

Alternatively, you can manually pass the `default` to :func:`json.dumps`.
It won't handle :class:`~bson.binary.Binary` and :class:`~bson.code.Code`
Expand Down Expand Up @@ -238,23 +238,27 @@ class JSONOptions(CodecOptions):

.. seealso:: The specification for Relaxed and Canonical `Extended JSON`_.

.. versionadded:: 3.4
.. versionchanged:: 4.0
The default for `json_mode` was changed from :const:`JSONMode.LEGACY`
to :const:`JSONMode.RELAXED`.

.. versionchanged:: 3.5
Accepts the optional parameter `json_mode`.

.. versionadded:: 3.4
"""

def __new__(cls, strict_number_long=False,
datetime_representation=DatetimeRepresentation.LEGACY,
strict_uuid=False, json_mode=JSONMode.LEGACY,
def __new__(cls, strict_number_long=None,
datetime_representation=None,
strict_uuid=None, json_mode=JSONMode.RELAXED,
*args, **kwargs):
kwargs["tz_aware"] = kwargs.get("tz_aware", True)
if kwargs["tz_aware"]:
kwargs["tzinfo"] = kwargs.get("tzinfo", utc)
if datetime_representation not in (DatetimeRepresentation.LEGACY,
DatetimeRepresentation.NUMBERLONG,
DatetimeRepresentation.ISO8601):
DatetimeRepresentation.ISO8601,
None):
raise ConfigurationError(
"JSONOptions.datetime_representation must be one of LEGACY, "
"NUMBERLONG, or ISO8601 from DatetimeRepresentation.")
Expand All @@ -267,17 +271,47 @@ def __new__(cls, strict_number_long=False,
"or CANONICAL from JSONMode.")
self.json_mode = json_mode
if self.json_mode == JSONMode.RELAXED:
if strict_number_long:
raise ConfigurationError(
"Cannot specify strict_number_long=True with"
" JSONMode.RELAXED")
if datetime_representation not in (None,
DatetimeRepresentation.ISO8601):
raise ConfigurationError(
"datetime_representation must be DatetimeRepresentation."
"ISO8601 or omitted with JSONMode.RELAXED")
if strict_uuid not in (None, True):
raise ConfigurationError(
"Cannot specify strict_uuid=False with JSONMode.RELAXED")
self.strict_number_long = False
self.datetime_representation = DatetimeRepresentation.ISO8601
self.strict_uuid = True
elif self.json_mode == JSONMode.CANONICAL:
if strict_number_long not in (None, True):
raise ConfigurationError(
"Cannot specify strict_number_long=False with"
" JSONMode.RELAXED")
if datetime_representation not in (
None, DatetimeRepresentation.NUMBERLONG):
raise ConfigurationError(
"datetime_representation must be DatetimeRepresentation."
"NUMBERLONG or omitted with JSONMode.RELAXED")
if strict_uuid not in (None, True):
raise ConfigurationError(
"Cannot specify strict_uuid=False with JSONMode.RELAXED")
self.strict_number_long = True
self.datetime_representation = DatetimeRepresentation.NUMBERLONG
self.strict_uuid = True
else:
self.strict_number_long = strict_number_long
self.datetime_representation = datetime_representation
self.strict_uuid = strict_uuid
else: # JSONMode.LEGACY
self.strict_number_long = False
self.datetime_representation = DatetimeRepresentation.LEGACY
self.strict_uuid = False
if strict_number_long is not None:
self.strict_number_long = strict_number_long
if datetime_representation is not None:
self.datetime_representation = datetime_representation
if strict_uuid is not None:
self.strict_uuid = strict_uuid
return self

def _arguments_repr(self):
Expand Down Expand Up @@ -307,7 +341,7 @@ def with_options(self, **kwargs):
>>> from bson.json_util import CANONICAL_JSON_OPTIONS
>>> CANONICAL_JSON_OPTIONS.tz_aware
True
>>> json_options = CANONICAL_JSON_OPTIONS.with_options(tz_aware=False)
>>> json_options = CANONICAL_JSON_OPTIONS.with_options(tz_aware=False, tzinfo=None)
>>> json_options.tz_aware
False

Expand All @@ -329,15 +363,6 @@ def with_options(self, **kwargs):
.. versionadded:: 3.5
"""

DEFAULT_JSON_OPTIONS = LEGACY_JSON_OPTIONS
"""The default :class:`JSONOptions` for JSON encoding/decoding.

The same as :const:`LEGACY_JSON_OPTIONS`. This will change to
:const:`RELAXED_JSON_OPTIONS` in a future release.

.. versionadded:: 3.4
"""

CANONICAL_JSON_OPTIONS = JSONOptions(json_mode=JSONMode.CANONICAL)
""":class:`JSONOptions` for Canonical Extended JSON.

Expand All @@ -354,18 +379,16 @@ def with_options(self, **kwargs):
.. versionadded:: 3.5
"""

STRICT_JSON_OPTIONS = JSONOptions(
strict_number_long=True,
datetime_representation=DatetimeRepresentation.ISO8601,
strict_uuid=True)
"""**DEPRECATED** - :class:`JSONOptions` for MongoDB Extended JSON's *Strict
mode* encoding.
DEFAULT_JSON_OPTIONS = RELAXED_JSON_OPTIONS
"""The default :class:`JSONOptions` for JSON encoding/decoding.

.. versionadded:: 3.4
The same as :const:`RELAXED_JSON_OPTIONS`.

.. versionchanged:: 4.0
Changed from :const:`LEGACY_JSON_OPTIONS` to
:const:`RELAXED_JSON_OPTIONS`.

.. versionchanged:: 3.5
Deprecated. Use :const:`RELAXED_JSON_OPTIONS` or
:const:`CANONICAL_JSON_OPTIONS` instead.
.. versionadded:: 3.4
"""


Expand All @@ -380,6 +403,10 @@ def dumps(obj, *args, **kwargs):
encoding of MongoDB Extended JSON types. Defaults to
:const:`DEFAULT_JSON_OPTIONS`.

.. versionchanged:: 4.0
Now outputs MongoDB Relaxed Extended JSON by default (using
:const:`DEFAULT_JSON_OPTIONS`).

.. versionchanged:: 3.4
Accepts optional parameter `json_options`. See :class:`JSONOptions`.
"""
Expand Down
6 changes: 6 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ Breaking Changes in 4.0
- Removed :exc:`pymongo.errors.CertificateError`.
- Removed :attr:`pymongo.GEOHAYSTACK`.
- Removed :class:`bson.binary.UUIDLegacy`.
- Removed :const:`bson.json_util.STRICT_JSON_OPTIONS`. Use
:const:`~bson.json_util.RELAXED_JSON_OPTIONS` or
:const:`~bson.json_util.CANONICAL_JSON_OPTIONS` instead.
- Changed the default JSON encoding representation from legacy to relaxed.
The json_mode parameter for :const:`bson.json_util.dumps` now defaults to
:const:`~bson.json_util.RELAXED_JSON_OPTIONS`.
- The "tls" install extra is no longer necessary or supported and will be
ignored by pip.

Expand Down
4 changes: 1 addition & 3 deletions doc/migrate-to-pymongo3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,7 @@ modifier. Code like this::
# Set a 5 second select() timeout.
>>> cursor = collection.find({"a": 1}, network_timeout=5)

can be changed to this with PyMongo 2.9 or later:

.. doctest::
can be changed to this with PyMongo 2.9 or later::

# Set a 5 second (5000 millisecond) server side query timeout.
>>> cursor = collection.find({"a": 1}, modifiers={"$maxTimeMS": 5000})
Expand Down
7 changes: 7 additions & 0 deletions doc/migrate-to-pymongo4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,13 @@ can be changed to this::
uu = uuid.uuid4()
uuid_legacy = Binary.from_uuid(uu, PYTHON_LEGACY)

Default JSONMode changed from LEGACY to RELAXED
-----------------------------------------------

Changed the default JSON encoding representation from legacy to relaxed.
The json_mode parameter for :const:`bson.json_util.dumps` now defaults to
:const:`~bson.json_util.RELAXED_JSON_OPTIONS`.

Removed features with no migration path
---------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion gridfs/grid_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ def seekable(self):
def __iter__(self):
"""Return an iterator over all of this file's data.

The iterator will return lines (delimited by b'\n') of
The iterator will return lines (delimited by ``b'\\n'``) of
:class:`bytes`. This can be useful when serving files
using a webserver that handles such an iterator efficiently.

Expand Down
3 changes: 2 additions & 1 deletion test/test_bson_corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
json_options_uuid_04 = json_util.JSONOptions(json_mode=JSONMode.CANONICAL,
uuid_representation=STANDARD)
json_options_iso8601 = json_util.JSONOptions(
datetime_representation=json_util.DatetimeRepresentation.ISO8601)
datetime_representation=json_util.DatetimeRepresentation.ISO8601,
json_mode=JSONMode.LEGACY)
to_extjson = functools.partial(json_util.dumps,
json_options=json_util.CANONICAL_JSON_OPTIONS)
to_extjson_uuid_04 = functools.partial(json_util.dumps,
Expand Down
Loading