From 92a6fa79b66ae1f91691c7540d3f40331195278f Mon Sep 17 00:00:00 2001 From: Ben Warner Date: Wed, 3 Aug 2022 16:53:50 -0700 Subject: [PATCH] PYTHON-3376/PYTHON-3378 Update FAQ about OverflowError when decoding out of range datetimes (#1025) --- doc/examples/datetimes.rst | 54 ++++++++++++++++++++++++++------------ doc/faq.rst | 41 ++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/doc/examples/datetimes.rst b/doc/examples/datetimes.rst index b9c509e07..f965b9f58 100644 --- a/doc/examples/datetimes.rst +++ b/doc/examples/datetimes.rst @@ -125,33 +125,53 @@ To decode UTC datetime values as :class:`~bson.datetime_ms.DatetimeMS`, :attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_AUTO`, :attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_CLAMP`. :attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME` is the default -option and has the behavior of raising an exception upon attempting to -decode an out-of-range date. +option and has the behavior of raising an :class:`~builtin.OverflowError` upon +attempting to decode an out-of-range date. :attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_MS` will only return :class:`~bson.datetime_ms.DatetimeMS` objects, regardless of whether the -represented datetime is in- or out-of-range. -:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_AUTO` will return -:class:`~datetime.datetime` if the underlying UTC datetime is within range, -or :class:`~bson.datetime_ms.DatetimeMS` if the underlying datetime -cannot be represented using the builtin Python :class:`~datetime.datetime`. -:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_CLAMP` will clamp -resulting :class:`~datetime.datetime` objects to be within -:attr:`~datetime.datetime.min` and :attr:`~datetime.datetime.max` -(trimmed to `999000` microseconds). - -An example of encoding and decoding using `DATETIME_MS` is as follows: +represented datetime is in- or out-of-range: .. doctest:: + >>> from datetime import datetime >>> from bson import encode, decode >>> from bson.datetime_ms import DatetimeMS - >>> from bson.codec_options import CodecOptions,DatetimeConversionOpts + >>> from bson.codec_options import CodecOptions, DatetimeConversionOpts >>> x = encode({"x": datetime(1970, 1, 1)}) - >>> x - b'\x10\x00\x00\x00\tx\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - >>> decode(x, codec_options=CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_MS)) + >>> codec_ms = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_MS) + >>> decode(x, codec_options=codec_ms) {'x': DatetimeMS(0)} +:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_AUTO` will return +:class:`~datetime.datetime` if the underlying UTC datetime is within range, +or :class:`~bson.datetime_ms.DatetimeMS` if the underlying datetime +cannot be represented using the builtin Python :class:`~datetime.datetime`: + +.. doctest:: + + >>> x = encode({"x": datetime(1970, 1, 1)}) + >>> y = encode({"x": DatetimeMS(-2**62)}) + >>> codec_auto = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_AUTO) + >>> decode(x, codec_options=codec_auto) + {'x': datetime.datetime(1970, 1, 1, 0, 0)} + >>> decode(y, codec_options=codec_auto) + {'x': DatetimeMS(-4611686018427387904)} + +:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_CLAMP` will clamp +resulting :class:`~datetime.datetime` objects to be within +:attr:`~datetime.datetime.min` and :attr:`~datetime.datetime.max` +(trimmed to `999000` microseconds): + +.. doctest:: + + >>> x = encode({"x": DatetimeMS(2**62)}) + >>> y = encode({"x": DatetimeMS(-2**62)}) + >>> codec_clamp = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_CLAMP) + >>> decode(x, codec_options=codec_clamp) + {'x': datetime.datetime(9999, 12, 31, 23, 59, 59, 999000)} + >>> decode(y, codec_options=codec_clamp) + {'x': datetime.datetime(1, 1, 1, 0, 0)} + :class:`~bson.datetime_ms.DatetimeMS` objects have support for rich comparison methods against other instances of :class:`~bson.datetime_ms.DatetimeMS`. They can also be converted to :class:`~datetime.datetime` objects with diff --git a/doc/faq.rst b/doc/faq.rst index ca83f5de4..5eb39c427 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -264,7 +264,7 @@ collection, configured to use :class:`~bson.son.SON` instead of dict: >>> from bson import CodecOptions, SON >>> opts = CodecOptions(document_class=SON) >>> opts - CodecOptions(document_class=...SON..., tz_aware=False, uuid_representation=UuidRepresentation.UNSPECIFIED, unicode_decode_error_handler='strict', tzinfo=None, type_registry=TypeRegistry(type_codecs=[], fallback_encoder=None)) + CodecOptions(document_class=...SON..., tz_aware=False, uuid_representation=UuidRepresentation.UNSPECIFIED, unicode_decode_error_handler='strict', tzinfo=None, type_registry=TypeRegistry(type_codecs=[], fallback_encoder=None), datetime_conversion=DatetimeConversionOpts.DATETIME) >>> collection_son = collection.with_options(codec_options=opts) Now, documents and subdocuments in query results are represented with @@ -489,9 +489,42 @@ limited to years between :data:`datetime.MINYEAR` (usually 1) and driver) can store BSON datetimes with year values far outside those supported by :class:`datetime.datetime`. -There are a few ways to work around this issue. One option is to filter -out documents with values outside of the range supported by -:class:`datetime.datetime`:: +There are a few ways to work around this issue. Starting with PyMongo 4.3, +:func:`bson.decode` can decode BSON datetimes in one of four ways, and can +be specified using the ``datetime_conversion`` parameter of +:class:`~bson.codec_options.CodecOptions`. + +The default option is +:attr:`~bson.codec_options.DatetimeConversionOpts.DATETIME`, which will +attempt to decode as a :class:`datetime.datetime`, allowing +:class:`~builtin.OverflowError` to occur upon out-of-range dates. +:attr:`~bson.codec_options.DatetimeConversionOpts.DATETIME_AUTO` alters +this behavior to instead return :class:`~bson.datetime_ms.DatetimeMS` when +representations are out-of-range, while returning :class:`~datetime.datetime` +objects as before: + +.. doctest:: + + >>> from datetime import datetime + >>> from bson.datetime_ms import DatetimeMS + >>> from bson.codec_options import DatetimeConversionOpts + >>> from pymongo import MongoClient + >>> client = MongoClient(datetime_conversion=DatetimeConversionOpts.DATETIME_AUTO) + >>> client.db.collection.insert_one({"x": datetime(1970, 1, 1)}) + + >>> client.db.collection.insert_one({"x": DatetimeMS(2**62)}) + + >>> for x in client.db.collection.find(): + ... print(x) + {'_id': ObjectId('...'), 'x': datetime.datetime(1970, 1, 1, 0, 0)} + {'_id': ObjectId('...'), 'x': DatetimeMS(4611686018427387904)} + +For other options, please refer to +:class:`~bson.codec_options.DatetimeConversionOpts`. + +Another option that does not involve setting `datetime_conversion` is to to +filter out documents values outside of the range supported by +:class:`~datetime.datetime`: >>> from datetime import datetime >>> coll = client.test.dates