From 46673c370521330f2705ae83c2b74db2a34fe7e5 Mon Sep 17 00:00:00 2001 From: Ben Warner Date: Thu, 4 Aug 2022 12:53:57 -0700 Subject: [PATCH] PYTHON-3379 Refactored DatetimeConversionOpts to DatetimeConversion (#1031) --- bson/__init__.py | 4 ++-- bson/codec_options.py | 6 +++--- bson/codec_options.pyi | 2 +- bson/datetime_ms.py | 20 ++++++++------------ bson/json_util.py | 6 +++--- doc/examples/datetimes.rst | 26 +++++++++++++------------- doc/faq.rst | 12 ++++++------ pymongo/common.py | 16 ++++++++-------- test/test_bson.py | 22 +++++++++------------- test/test_client.py | 10 ++++------ test/test_json_util.py | 4 ++-- 11 files changed, 59 insertions(+), 69 deletions(-) diff --git a/bson/__init__.py b/bson/__init__.py index 4283faf7d..b43c686de 100644 --- a/bson/__init__.py +++ b/bson/__init__.py @@ -99,7 +99,7 @@ from bson.code import Code from bson.codec_options import ( DEFAULT_CODEC_OPTIONS, CodecOptions, - DatetimeConversionOpts, + DatetimeConversion, _DocumentType, _raw_document_class, ) @@ -194,7 +194,7 @@ __all__ = [ "is_valid", "BSON", "has_c", - "DatetimeConversionOpts", + "DatetimeConversion", "DatetimeMS", ] diff --git a/bson/codec_options.py b/bson/codec_options.py index bceab5e00..efba8af78 100644 --- a/bson/codec_options.py +++ b/bson/codec_options.py @@ -199,7 +199,7 @@ class TypeRegistry(object): ) -class DatetimeConversionOpts(enum.IntEnum): +class DatetimeConversion(enum.IntEnum): """Options for decoding BSON datetimes.""" DATETIME = 1 @@ -241,7 +241,7 @@ class _BaseCodecOptions(NamedTuple): unicode_decode_error_handler: str tzinfo: Optional[datetime.tzinfo] type_registry: TypeRegistry - datetime_conversion: Optional[DatetimeConversionOpts] + datetime_conversion: Optional[DatetimeConversion] class CodecOptions(_BaseCodecOptions): @@ -335,7 +335,7 @@ class CodecOptions(_BaseCodecOptions): unicode_decode_error_handler: str = "strict", tzinfo: Optional[datetime.tzinfo] = None, type_registry: Optional[TypeRegistry] = None, - datetime_conversion: Optional[DatetimeConversionOpts] = DatetimeConversionOpts.DATETIME, + datetime_conversion: Optional[DatetimeConversion] = DatetimeConversion.DATETIME, ) -> "CodecOptions": doc_class = document_class or dict # issubclass can raise TypeError for generic aliases like SON[str, Any]. diff --git a/bson/codec_options.pyi b/bson/codec_options.pyi index 260407524..2424516f0 100644 --- a/bson/codec_options.pyi +++ b/bson/codec_options.pyi @@ -55,7 +55,7 @@ class TypeRegistry: _DocumentType = TypeVar("_DocumentType", bound=Mapping[str, Any]) -class DatetimeConversionOpts(int, enum.Enum): +class DatetimeConversion(int, enum.Enum): DATETIME = ... DATETIME_CLAMP = ... DATETIME_MS = ... diff --git a/bson/datetime_ms.py b/bson/datetime_ms.py index 925087a5a..c64a0cce8 100644 --- a/bson/datetime_ms.py +++ b/bson/datetime_ms.py @@ -22,11 +22,7 @@ import datetime import functools from typing import Any, Union, cast -from bson.codec_options import ( - DEFAULT_CODEC_OPTIONS, - CodecOptions, - DatetimeConversionOpts, -) +from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, DatetimeConversion from bson.tz_util import utc EPOCH_AWARE = datetime.datetime.fromtimestamp(0, utc) @@ -127,14 +123,14 @@ def _max_datetime_ms(tz=datetime.timezone.utc): def _millis_to_datetime(millis: int, opts: CodecOptions) -> Union[datetime.datetime, DatetimeMS]: """Convert milliseconds since epoch UTC to datetime.""" if ( - opts.datetime_conversion == DatetimeConversionOpts.DATETIME - or opts.datetime_conversion == DatetimeConversionOpts.DATETIME_CLAMP - or opts.datetime_conversion == DatetimeConversionOpts.DATETIME_AUTO + opts.datetime_conversion == DatetimeConversion.DATETIME + or opts.datetime_conversion == DatetimeConversion.DATETIME_CLAMP + or opts.datetime_conversion == DatetimeConversion.DATETIME_AUTO ): tz = opts.tzinfo or datetime.timezone.utc - if opts.datetime_conversion == DatetimeConversionOpts.DATETIME_CLAMP: + if opts.datetime_conversion == DatetimeConversion.DATETIME_CLAMP: millis = max(_min_datetime_ms(tz), min(millis, _max_datetime_ms(tz))) - elif opts.datetime_conversion == DatetimeConversionOpts.DATETIME_AUTO: + elif opts.datetime_conversion == DatetimeConversion.DATETIME_AUTO: if not (_min_datetime_ms(tz) <= millis <= _max_datetime_ms(tz)): return DatetimeMS(millis) @@ -149,10 +145,10 @@ def _millis_to_datetime(millis: int, opts: CodecOptions) -> Union[datetime.datet return dt else: return EPOCH_NAIVE + datetime.timedelta(seconds=seconds, microseconds=micros) - elif opts.datetime_conversion == DatetimeConversionOpts.DATETIME_MS: + elif opts.datetime_conversion == DatetimeConversion.DATETIME_MS: return DatetimeMS(millis) else: - raise ValueError("datetime_conversion must be an element of DatetimeConversionOpts") + raise ValueError("datetime_conversion must be an element of DatetimeConversion") def _datetime_to_millis(dtm: datetime.datetime) -> int: diff --git a/bson/json_util.py b/bson/json_util.py index 0b5494e85..517adff4e 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -96,7 +96,7 @@ from typing import Any, Dict, Mapping, Optional, Sequence, Tuple, Type, Union, c from bson.binary import ALL_UUID_SUBTYPES, UUID_SUBTYPE, Binary, UuidRepresentation from bson.code import Code -from bson.codec_options import CodecOptions, DatetimeConversionOpts +from bson.codec_options import CodecOptions, DatetimeConversion from bson.datetime_ms import ( EPOCH_AWARE, DatetimeMS, @@ -662,12 +662,12 @@ def _parse_canonical_datetime( if json_options.tz_aware: if json_options.tzinfo: aware = aware.astimezone(json_options.tzinfo) - if json_options.datetime_conversion == DatetimeConversionOpts.DATETIME_MS: + if json_options.datetime_conversion == DatetimeConversion.DATETIME_MS: return DatetimeMS(aware) return aware else: aware_tzinfo_none = aware.replace(tzinfo=None) - if json_options.datetime_conversion == DatetimeConversionOpts.DATETIME_MS: + if json_options.datetime_conversion == DatetimeConversion.DATETIME_MS: return DatetimeMS(aware_tzinfo_none) return aware_tzinfo_none return _millis_to_datetime(int(dtm), json_options) diff --git a/doc/examples/datetimes.rst b/doc/examples/datetimes.rst index f965b9f58..3b30000ff 100644 --- a/doc/examples/datetimes.rst +++ b/doc/examples/datetimes.rst @@ -119,15 +119,15 @@ of milliseconds from the Unix epoch. To deal with this, we can use the To decode UTC datetime values as :class:`~bson.datetime_ms.DatetimeMS`, :class:`~bson.codec_options.CodecOptions` should have its ``datetime_conversion`` parameter set to one of the options available in -:class:`bson.datetime_ms.DatetimeConversionOpts`. These include -:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME`, -:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_MS`, -:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_AUTO`, -:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_CLAMP`. -:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME` is the default +:class:`bson.datetime_ms.DatetimeConversion`. These include +:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME`, +:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_MS`, +:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_AUTO`, +:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_CLAMP`. +:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME` is the default 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 +:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_MS` will only return :class:`~bson.datetime_ms.DatetimeMS` objects, regardless of whether the represented datetime is in- or out-of-range: @@ -136,13 +136,13 @@ represented datetime is in- or out-of-range: >>> 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, DatetimeConversion >>> x = encode({"x": datetime(1970, 1, 1)}) - >>> codec_ms = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_MS) + >>> codec_ms = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_MS) >>> decode(x, codec_options=codec_ms) {'x': DatetimeMS(0)} -:attr:`~bson.datetime_ms.DatetimeConversionOpts.DATETIME_AUTO` will return +:attr:`~bson.datetime_ms.DatetimeConversion.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`: @@ -151,13 +151,13 @@ cannot be represented using the builtin Python :class:`~datetime.datetime`: >>> x = encode({"x": datetime(1970, 1, 1)}) >>> y = encode({"x": DatetimeMS(-2**62)}) - >>> codec_auto = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_AUTO) + >>> codec_auto = CodecOptions(datetime_conversion=DatetimeConversion.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 +:attr:`~bson.datetime_ms.DatetimeConversion.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): @@ -166,7 +166,7 @@ resulting :class:`~datetime.datetime` objects to be within >>> x = encode({"x": DatetimeMS(2**62)}) >>> y = encode({"x": DatetimeMS(-2**62)}) - >>> codec_clamp = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_CLAMP) + >>> codec_clamp = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_CLAMP) >>> decode(x, codec_options=codec_clamp) {'x': datetime.datetime(9999, 12, 31, 23, 59, 59, 999000)} >>> decode(y, codec_options=codec_clamp) diff --git a/doc/faq.rst b/doc/faq.rst index 5eb39c427..c48dd316e 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), datetime_conversion=DatetimeConversionOpts.DATETIME) + 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=DatetimeConversion.DATETIME) >>> collection_son = collection.with_options(codec_options=opts) Now, documents and subdocuments in query results are represented with @@ -495,10 +495,10 @@ 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 +:attr:`~bson.codec_options.DatetimeConversion.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 +:attr:`~bson.codec_options.DatetimeConversion.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: @@ -507,9 +507,9 @@ objects as before: >>> from datetime import datetime >>> from bson.datetime_ms import DatetimeMS - >>> from bson.codec_options import DatetimeConversionOpts + >>> from bson.codec_options import DatetimeConversion >>> from pymongo import MongoClient - >>> client = MongoClient(datetime_conversion=DatetimeConversionOpts.DATETIME_AUTO) + >>> client = MongoClient(datetime_conversion=DatetimeConversion.DATETIME_AUTO) >>> client.db.collection.insert_one({"x": datetime(1970, 1, 1)}) >>> client.db.collection.insert_one({"x": DatetimeMS(2**62)}) @@ -520,7 +520,7 @@ objects as before: {'_id': ObjectId('...'), 'x': DatetimeMS(4611686018427387904)} For other options, please refer to -:class:`~bson.codec_options.DatetimeConversionOpts`. +:class:`~bson.codec_options.DatetimeConversion`. Another option that does not involve setting `datetime_conversion` is to to filter out documents values outside of the range supported by diff --git a/pymongo/common.py b/pymongo/common.py index 319b07193..add70cfb5 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -36,7 +36,7 @@ from urllib.parse import unquote_plus from bson import SON from bson.binary import UuidRepresentation -from bson.codec_options import CodecOptions, DatetimeConversionOpts, TypeRegistry +from bson.codec_options import CodecOptions, DatetimeConversion, TypeRegistry from bson.raw_bson import RawBSONDocument from pymongo.auth import MECHANISMS from pymongo.compression_support import ( @@ -620,19 +620,19 @@ def validate_auto_encryption_opts_or_none(option: Any, value: Any) -> Optional[A return value -def validate_datetime_conversion(option: Any, value: Any) -> Optional[DatetimeConversionOpts]: - """Validate a DatetimeConversionOpts string.""" +def validate_datetime_conversion(option: Any, value: Any) -> Optional[DatetimeConversion]: + """Validate a DatetimeConversion string.""" if value is None: - return DatetimeConversionOpts.DATETIME + return DatetimeConversion.DATETIME if isinstance(value, str): if value.isdigit(): - return DatetimeConversionOpts(int(value)) - return DatetimeConversionOpts[value] + return DatetimeConversion(int(value)) + return DatetimeConversion[value] elif isinstance(value, int): - return DatetimeConversionOpts(value) + return DatetimeConversion(value) - raise TypeError("%s must be a str or int representing DatetimeConversionOpts" % (option,)) + raise TypeError("%s must be a str or int representing DatetimeConversion" % (option,)) # Dictionary where keys are the names of public URI options, and values diff --git a/test/test_bson.py b/test/test_bson.py index 7fe0c168c..e3c4a3a02 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -50,7 +50,7 @@ from bson import ( ) from bson.binary import Binary, UuidRepresentation from bson.code import Code -from bson.codec_options import CodecOptions, DatetimeConversionOpts +from bson.codec_options import CodecOptions, DatetimeConversion from bson.dbref import DBRef from bson.errors import InvalidBSON, InvalidDocument from bson.int64 import Int64 @@ -981,7 +981,7 @@ class TestCodecOptions(unittest.TestCase): "unicode_decode_error_handler='strict', " "tzinfo=None, type_registry=TypeRegistry(type_codecs=[], " "fallback_encoder=None), " - "datetime_conversion=DatetimeConversionOpts.DATETIME)" + "datetime_conversion=DatetimeConversion.DATETIME)" ) self.assertEqual(r, repr(CodecOptions())) @@ -1189,14 +1189,14 @@ class TestDatetimeConversion(unittest.TestCase): self.assertNotEqual(type(dtr1), type(dec1["x"])) # Test encode and decode with codec options. Expect: UTCDateimteRaw => DatetimeMS - opts1 = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_MS) + opts1 = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_MS) enc1 = encode({"x": dtr1}) dec1 = decode(enc1, opts1) self.assertEqual(type(dtr1), type(dec1["x"])) self.assertEqual(dtr1, dec1["x"]) # Expect: datetime => DatetimeMS - opts1 = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_MS) + opts1 = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_MS) dt1 = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) enc1 = encode({"x": dt1}) dec1 = decode(enc1, opts1) @@ -1206,7 +1206,7 @@ class TestDatetimeConversion(unittest.TestCase): def test_clamping(self): # Test clamping from below and above. opts1 = CodecOptions( - datetime_conversion=DatetimeConversionOpts.DATETIME_CLAMP, + datetime_conversion=DatetimeConversion.DATETIME_CLAMP, tz_aware=True, tzinfo=datetime.timezone.utc, ) @@ -1225,9 +1225,7 @@ class TestDatetimeConversion(unittest.TestCase): def test_tz_clamping(self): # Naive clamping to local tz. - opts1 = CodecOptions( - datetime_conversion=DatetimeConversionOpts.DATETIME_CLAMP, tz_aware=False - ) + opts1 = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_CLAMP, tz_aware=False) below = encode({"x": DatetimeMS(_datetime_to_millis(datetime.datetime.min) - 24 * 60 * 60)}) dec_below = decode(below, opts1) @@ -1241,9 +1239,7 @@ class TestDatetimeConversion(unittest.TestCase): ) # Aware clamping. - opts2 = CodecOptions( - datetime_conversion=DatetimeConversionOpts.DATETIME_CLAMP, tz_aware=True - ) + opts2 = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_CLAMP, tz_aware=True) below = encode({"x": DatetimeMS(_datetime_to_millis(datetime.datetime.min) - 24 * 60 * 60)}) dec_below = decode(below, opts2) self.assertEqual( @@ -1259,7 +1255,7 @@ class TestDatetimeConversion(unittest.TestCase): def test_datetime_auto(self): # Naive auto, in range. - opts1 = CodecOptions(datetime_conversion=DatetimeConversionOpts.DATETIME_AUTO) + opts1 = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_AUTO) inr = encode({"x": datetime.datetime(1970, 1, 1)}, codec_options=opts1) dec_inr = decode(inr) self.assertEqual(dec_inr["x"], datetime.datetime(1970, 1, 1)) @@ -1281,7 +1277,7 @@ class TestDatetimeConversion(unittest.TestCase): # Aware auto, in range. opts2 = CodecOptions( - datetime_conversion=DatetimeConversionOpts.DATETIME_AUTO, + datetime_conversion=DatetimeConversion.DATETIME_AUTO, tz_aware=True, tzinfo=datetime.timezone.utc, ) diff --git a/test/test_client.py b/test/test_client.py index f520043ec..7e7e14c0e 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -67,7 +67,7 @@ import pymongo from bson import encode from bson.codec_options import ( CodecOptions, - DatetimeConversionOpts, + DatetimeConversion, TypeEncoder, TypeRegistry, ) @@ -412,17 +412,15 @@ class ClientUnitTest(unittest.TestCase): ) self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) self.assertEqual( - c.codec_options.datetime_conversion, DatetimeConversionOpts[datetime_conversion] + c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] ) # Change the passed datetime_conversion to a number and re-assert. - uri = uri.replace( - datetime_conversion, f"{int(DatetimeConversionOpts[datetime_conversion])}" - ) + uri = uri.replace(datetime_conversion, f"{int(DatetimeConversion[datetime_conversion])}") c = MongoClient(uri, connect=False) self.assertEqual( - c.codec_options.datetime_conversion, DatetimeConversionOpts[datetime_conversion] + c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] ) def test_uri_option_precedence(self): diff --git a/test/test_json_util.py b/test/test_json_util.py index 576746e86..08ee63618 100644 --- a/test/test_json_util.py +++ b/test/test_json_util.py @@ -21,7 +21,7 @@ import sys import uuid from typing import Any, List, MutableMapping -from bson.codec_options import CodecOptions, DatetimeConversionOpts +from bson.codec_options import CodecOptions, DatetimeConversion sys.path[0:0] = [""] @@ -295,7 +295,7 @@ class TestJsonUtil(unittest.TestCase): dat_max = {"x": DatetimeMS(_max_datetime_ms()).as_datetime(CodecOptions(tz_aware=False))} opts = JSONOptions( datetime_representation=DatetimeRepresentation.ISO8601, - datetime_conversion=DatetimeConversionOpts.DATETIME_MS, + datetime_conversion=DatetimeConversion.DATETIME_MS, ) self.assertEqual(