PYTHON-3379 Refactored DatetimeConversionOpts to DatetimeConversion (#1031)

This commit is contained in:
Ben Warner 2022-08-04 12:53:57 -07:00 committed by GitHub
parent 92a6fa79b6
commit 46673c3705
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 59 additions and 69 deletions

View File

@ -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",
]

View File

@ -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].

View File

@ -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 = ...

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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)})
<pymongo.results.InsertOneResult object at 0x...>
>>> 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

View File

@ -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

View File

@ -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,
)

View File

@ -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):

View File

@ -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(