Merge branch 'master' of github.com:mongodb/mongo-python-driver

This commit is contained in:
Steven Silvester 2024-08-30 19:29:45 -05:00
commit a0d99ac66e
No known key found for this signature in database
GPG Key ID: B1BF5EC3A8B32F91
17 changed files with 3573 additions and 332 deletions

View File

@ -22,30 +22,46 @@ Python Type BSON Type Supported Direction
None null both
bool boolean both
int [#int]_ int32 / int64 py -> bson
`bson.int64.Int64` int64 both
:class:`bson.int64.Int64` int64 both
float number (real) both
str string both
list array both
dict / `SON` object both
datetime.datetime [#dt]_ [#dt2]_ date both
`bson.regex.Regex` regex both
dict object both
:class:`~bson.son.SON` object both
:py:class:`~collections.abc.Mapping` object py -> bson
:class:`~bson.raw_bson.RawBSONDocument` object both [#raw]_
datetime.datetime [#dt]_ [#dt2]_ UTC datetime both
:class:`~bson.datetime_ms.DatetimeMS` UTC datetime both [#dt3]_
:class:`~bson.regex.Regex` regex both
compiled re [#re]_ regex py -> bson
`bson.binary.Binary` binary both
`bson.objectid.ObjectId` oid both
`bson.dbref.DBRef` dbref both
:class:`~bson.binary.Binary` binary both
:py:class:`uuid.UUID` [#uuid]_ binary both
:class:`~bson.objectid.ObjectId` oid both
:class:`~bson.dbref.DBRef` dbref both
:class:`~bson.dbref.DBRef` dbpointer bson -> py
None undefined bson -> py
`bson.code.Code` code both
:class:`~bson.code.Code` code both
str symbol bson -> py
bytes [#bytes]_ binary both
:class:`~bson.timestamp.Timestamp` timestamp both
:class:`~bson.decimal128.Decimal128` decimal128 both
:class:`~bson.min_key.MinKey` min key both
:class:`~bson.max_key.MaxKey` max key both
======================================= ============= ===================
.. [#int] A Python int will be saved as a BSON int32 or BSON int64 depending
on its size. A BSON int32 will always decode to a Python int. A BSON
int64 will always decode to a :class:`~bson.int64.Int64`.
.. [#dt] datetime.datetime instances will be rounded to the nearest
millisecond when saved
.. [#dt2] all datetime.datetime instances are treated as *naive*. clients
should always use UTC.
.. [#raw] Decoding a bson object to :class:`~bson.raw_bson.RawBSONDocument` can be
optionally configured via :attr:`~bson.codec_options.CodecOptions.document_class`.
.. [#dt] datetime.datetime instances are encoded with millisecond precision so
the microsecond field is truncated.
.. [#dt2] all datetime.datetime instances are encoded as UTC. By default, they
are decoded as *naive* but timezone aware datetimes are also supported.
See :doc:`/examples/datetimes` for examples.
.. [#dt3] To enable decoding a bson UTC datetime to a :class:`~bson.datetime_ms.DatetimeMS`
instance see :ref:`handling-out-of-range-datetimes`.
.. [#uuid] For :py:class:`uuid.UUID` encoding and decoding behavior see :doc:`/examples/uuid`.
.. [#re] :class:`~bson.regex.Regex` instances and regular expression
objects from ``re.compile()`` are both saved as BSON regular expressions.
BSON regular expressions are decoded as :class:`~bson.regex.Regex`

View File

@ -53,8 +53,10 @@ struct module_state {
PyObject* Decimal128;
PyObject* Mapping;
PyObject* DatetimeMS;
PyObject* _min_datetime_ms;
PyObject* _max_datetime_ms;
PyObject* min_datetime;
PyObject* max_datetime;
PyObject* replace_args;
PyObject* replace_kwargs;
PyObject* _type_marker_str;
PyObject* _flags_str;
PyObject* _pattern_str;
@ -80,6 +82,8 @@ struct module_state {
PyObject* _from_uuid_str;
PyObject* _as_uuid_str;
PyObject* _from_bid_str;
int64_t min_millis;
int64_t max_millis;
};
#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m))
@ -253,7 +257,7 @@ static PyObject* datetime_from_millis(long long millis) {
* 2. Multiply that by 1000: 253402300799000
* 3. Add in microseconds divided by 1000 253402300799999
*
* (Note: BSON doesn't support microsecond accuracy, hence the rounding.)
* (Note: BSON doesn't support microsecond accuracy, hence the truncation.)
*
* To decode we could do:
* 1. Get seconds: timestamp / 1000: 253402300799
@ -376,6 +380,118 @@ static int millis_from_datetime_ms(PyObject* dt, long long* out){
return 1;
}
static PyObject* decode_datetime(PyObject* self, long long millis, const codec_options_t* options){
PyObject* naive = NULL;
PyObject* replace = NULL;
PyObject* args = NULL;
PyObject* kwargs = NULL;
PyObject* value = NULL;
struct module_state *state = GETSTATE(self);
if (options->datetime_conversion == DATETIME_MS){
return datetime_ms_from_millis(self, millis);
}
int dt_clamp = options->datetime_conversion == DATETIME_CLAMP;
int dt_auto = options->datetime_conversion == DATETIME_AUTO;
if (dt_clamp || dt_auto){
int64_t min_millis = state->min_millis;
int64_t max_millis = state->max_millis;
int64_t min_millis_offset = 0;
int64_t max_millis_offset = 0;
if (options->tz_aware && options->tzinfo && options->tzinfo != Py_None) {
PyObject* utcoffset = PyObject_CallMethodObjArgs(options->tzinfo, state->_utcoffset_str, state->min_datetime, NULL);
if (utcoffset == NULL) {
return 0;
}
if (utcoffset != Py_None) {
if (!PyDelta_Check(utcoffset)) {
PyObject* BSONError = _error("BSONError");
if (BSONError) {
PyErr_SetString(BSONError, "tzinfo.utcoffset() did not return a datetime.timedelta");
Py_DECREF(BSONError);
}
Py_DECREF(utcoffset);
return 0;
}
min_millis_offset = (PyDateTime_DELTA_GET_DAYS(utcoffset) * 86400 +
PyDateTime_DELTA_GET_SECONDS(utcoffset)) * 1000 +
(PyDateTime_DELTA_GET_MICROSECONDS(utcoffset) / 1000);
}
Py_DECREF(utcoffset);
utcoffset = PyObject_CallMethodObjArgs(options->tzinfo, state->_utcoffset_str, state->max_datetime, NULL);
if (utcoffset == NULL) {
return 0;
}
if (utcoffset != Py_None) {
if (!PyDelta_Check(utcoffset)) {
PyObject* BSONError = _error("BSONError");
if (BSONError) {
PyErr_SetString(BSONError, "tzinfo.utcoffset() did not return a datetime.timedelta");
Py_DECREF(BSONError);
}
Py_DECREF(utcoffset);
return 0;
}
max_millis_offset = (PyDateTime_DELTA_GET_DAYS(utcoffset) * 86400 +
PyDateTime_DELTA_GET_SECONDS(utcoffset)) * 1000 +
(PyDateTime_DELTA_GET_MICROSECONDS(utcoffset) / 1000);
}
Py_DECREF(utcoffset);
}
if (min_millis_offset < 0) {
min_millis -= min_millis_offset;
}
if (max_millis_offset > 0) {
max_millis -= max_millis_offset;
}
if (dt_clamp) {
if (millis < min_millis) {
millis = min_millis;
} else if (millis > max_millis) {
millis = max_millis;
}
// Continues from here to return a datetime.
} else { // dt_auto
if (millis < min_millis || millis > max_millis){
return datetime_ms_from_millis(self, millis);
}
}
}
naive = datetime_from_millis(millis);
if (!naive) {
goto invalid;
}
if (!options->tz_aware) { /* In the naive case, we're done here. */
return naive;
}
replace = PyObject_GetAttr(naive, state->_replace_str);
if (!replace) {
goto invalid;
}
value = PyObject_Call(replace, state->replace_args, state->replace_kwargs);
if (!value) {
goto invalid;
}
/* convert to local time */
if (options->tzinfo != Py_None) {
PyObject* temp = PyObject_CallMethodObjArgs(value, state->_astimezone_str, options->tzinfo, NULL);
Py_DECREF(value);
value = temp;
}
invalid:
Py_XDECREF(naive);
Py_XDECREF(replace);
Py_XDECREF(args);
Py_XDECREF(kwargs);
return value;
}
/* Just make this compatible w/ the old API. */
int buffer_write_bytes(buffer_t buffer, const char* data, int size) {
if (pymongo_buffer_write(buffer, data, size)) {
@ -482,6 +598,8 @@ static int _load_python_objects(PyObject* module) {
PyObject* empty_string = NULL;
PyObject* re_compile = NULL;
PyObject* compiled = NULL;
PyObject* min_datetime_ms = NULL;
PyObject* max_datetime_ms = NULL;
struct module_state *state = GETSTATE(module);
if (!state) {
return 1;
@ -530,10 +648,34 @@ static int _load_python_objects(PyObject* module) {
_load_object(&state->UUID, "uuid", "UUID") ||
_load_object(&state->Mapping, "collections.abc", "Mapping") ||
_load_object(&state->DatetimeMS, "bson.datetime_ms", "DatetimeMS") ||
_load_object(&state->_min_datetime_ms, "bson.datetime_ms", "_min_datetime_ms") ||
_load_object(&state->_max_datetime_ms, "bson.datetime_ms", "_max_datetime_ms")) {
_load_object(&min_datetime_ms, "bson.datetime_ms", "_MIN_UTC_MS") ||
_load_object(&max_datetime_ms, "bson.datetime_ms", "_MAX_UTC_MS") ||
_load_object(&state->min_datetime, "bson.datetime_ms", "_MIN_UTC") ||
_load_object(&state->max_datetime, "bson.datetime_ms", "_MAX_UTC")) {
return 1;
}
state->min_millis = PyLong_AsLongLong(min_datetime_ms);
state->max_millis = PyLong_AsLongLong(max_datetime_ms);
Py_DECREF(min_datetime_ms);
Py_DECREF(max_datetime_ms);
if ((state->min_millis == -1 || state->max_millis == -1) && PyErr_Occurred()) {
return 1;
}
/* Speed up datetime.replace(tzinfo=utc) call */
state->replace_args = PyTuple_New(0);
if (!state->replace_args) {
return 1;
}
state->replace_kwargs = PyDict_New();
if (!state->replace_kwargs) {
return 1;
}
if (PyDict_SetItem(state->replace_kwargs, state->_tzinfo_str, state->UTC) == -1) {
return 1;
}
/* Reload our REType hack too. */
empty_string = PyBytes_FromString("");
if (empty_string == NULL) {
@ -1247,8 +1389,8 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
return 0;
if (utcoffset != Py_None) {
PyObject* result = PyNumber_Subtract(value, utcoffset);
Py_DECREF(utcoffset);
if (!result) {
Py_DECREF(utcoffset);
return 0;
}
millis = millis_from_datetime(result);
@ -1256,6 +1398,7 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
} else {
millis = millis_from_datetime(value);
}
Py_DECREF(utcoffset);
*(pymongo_buffer_get_buffer(buffer) + type_byte) = 0x09;
return buffer_write_int64(buffer, (int64_t)millis);
} else if (PyObject_TypeCheck(value, state->REType)) {
@ -2043,11 +2186,6 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
}
case 9:
{
PyObject* naive;
PyObject* replace;
PyObject* args;
PyObject* kwargs;
PyObject* astimezone;
int64_t millis;
if (max < 8) {
goto invalid;
@ -2056,120 +2194,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
millis = (int64_t)BSON_UINT64_FROM_LE(millis);
*position += 8;
if (options->datetime_conversion == DATETIME_MS){
value = datetime_ms_from_millis(self, millis);
break;
}
int dt_clamp = options->datetime_conversion == DATETIME_CLAMP;
int dt_auto = options->datetime_conversion == DATETIME_AUTO;
if (dt_clamp || dt_auto){
PyObject *min_millis_fn_res;
PyObject *max_millis_fn_res;
int64_t min_millis;
int64_t max_millis;
if (options->tz_aware){
PyObject* tzinfo = options->tzinfo;
if (tzinfo == Py_None) {
// Default to UTC.
tzinfo = state->UTC;
}
min_millis_fn_res = PyObject_CallFunctionObjArgs(state->_min_datetime_ms, tzinfo, NULL);
max_millis_fn_res = PyObject_CallFunctionObjArgs(state->_max_datetime_ms, tzinfo, NULL);
} else {
min_millis_fn_res = PyObject_CallObject(state->_min_datetime_ms, NULL);
max_millis_fn_res = PyObject_CallObject(state->_max_datetime_ms, NULL);
}
if (!min_millis_fn_res || !max_millis_fn_res){
Py_XDECREF(min_millis_fn_res);
Py_XDECREF(max_millis_fn_res);
goto invalid;
}
min_millis = PyLong_AsLongLong(min_millis_fn_res);
max_millis = PyLong_AsLongLong(max_millis_fn_res);
if ((min_millis == -1 || max_millis == -1) && PyErr_Occurred())
{
// min/max_millis check
goto invalid;
}
if (dt_clamp) {
if (millis < min_millis) {
millis = min_millis;
} else if (millis > max_millis) {
millis = max_millis;
}
// Continues from here to return a datetime.
} else { // dt_auto
if (millis < min_millis || millis > max_millis){
value = datetime_ms_from_millis(self, millis);
break; // Out-of-range so done.
}
}
}
naive = datetime_from_millis(millis);
if (!options->tz_aware) { /* In the naive case, we're done here. */
value = naive;
break;
}
if (!naive) {
goto invalid;
}
replace = PyObject_GetAttr(naive, state->_replace_str);
Py_DECREF(naive);
if (!replace) {
goto invalid;
}
args = PyTuple_New(0);
if (!args) {
Py_DECREF(replace);
goto invalid;
}
kwargs = PyDict_New();
if (!kwargs) {
Py_DECREF(replace);
Py_DECREF(args);
goto invalid;
}
if (PyDict_SetItem(kwargs, state->_tzinfo_str, state->UTC) == -1) {
Py_DECREF(replace);
Py_DECREF(args);
Py_DECREF(kwargs);
goto invalid;
}
value = PyObject_Call(replace, args, kwargs);
if (!value) {
Py_DECREF(replace);
Py_DECREF(args);
Py_DECREF(kwargs);
goto invalid;
}
/* convert to local time */
if (options->tzinfo != Py_None) {
astimezone = PyObject_GetAttr(value, state->_astimezone_str);
Py_DECREF(value);
if (!astimezone) {
Py_DECREF(replace);
Py_DECREF(args);
Py_DECREF(kwargs);
goto invalid;
}
value = PyObject_CallFunctionObjArgs(astimezone, options->tzinfo, NULL);
Py_DECREF(astimezone);
}
Py_DECREF(replace);
Py_DECREF(args);
Py_DECREF(kwargs);
value = decode_datetime(self, millis, options);
break;
}
case 11:
@ -3053,6 +3078,10 @@ static int _cbson_traverse(PyObject *m, visitproc visit, void *arg) {
Py_VISIT(state->_from_uuid_str);
Py_VISIT(state->_as_uuid_str);
Py_VISIT(state->_from_bid_str);
Py_VISIT(state->min_datetime);
Py_VISIT(state->max_datetime);
Py_VISIT(state->replace_args);
Py_VISIT(state->replace_kwargs);
return 0;
}
@ -3097,6 +3126,10 @@ static int _cbson_clear(PyObject *m) {
Py_CLEAR(state->_from_uuid_str);
Py_CLEAR(state->_as_uuid_str);
Py_CLEAR(state->_from_bid_str);
Py_CLEAR(state->min_datetime);
Py_CLEAR(state->max_datetime);
Py_CLEAR(state->replace_args);
Py_CLEAR(state->replace_kwargs);
return 0;
}

View File

@ -20,7 +20,6 @@ from __future__ import annotations
import calendar
import datetime
import functools
from typing import Any, Union, cast
from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, DatetimeConversion
@ -127,11 +126,8 @@ _MIN_UTC_MS = _datetime_to_millis(_MIN_UTC)
_MAX_UTC_MS = _datetime_to_millis(_MAX_UTC)
# Inclusive and exclusive min and max for timezones.
# Timezones are hashed by their offset, which is a timedelta
# and therefore there are more than 24 possible timezones.
@functools.lru_cache(maxsize=None)
def _min_datetime_ms(tz: datetime.timezone = datetime.timezone.utc) -> int:
# Inclusive min and max for timezones.
def _min_datetime_ms(tz: datetime.tzinfo = utc) -> int:
delta = tz.utcoffset(_MIN_UTC)
if delta is not None:
offset_millis = (delta.days * 86400 + delta.seconds) * 1000 + delta.microseconds // 1000
@ -140,8 +136,7 @@ def _min_datetime_ms(tz: datetime.timezone = datetime.timezone.utc) -> int:
return max(_MIN_UTC_MS, _MIN_UTC_MS - offset_millis)
@functools.lru_cache(maxsize=None)
def _max_datetime_ms(tz: datetime.timezone = datetime.timezone.utc) -> int:
def _max_datetime_ms(tz: datetime.tzinfo = utc) -> int:
delta = tz.utcoffset(_MAX_UTC)
if delta is not None:
offset_millis = (delta.days * 86400 + delta.seconds) * 1000 + delta.microseconds // 1000
@ -159,7 +154,7 @@ def _millis_to_datetime(
or opts.datetime_conversion == DatetimeConversion.DATETIME_CLAMP
or opts.datetime_conversion == DatetimeConversion.DATETIME_AUTO
):
tz = opts.tzinfo or datetime.timezone.utc
tz = opts.tzinfo or utc
if opts.datetime_conversion == DatetimeConversion.DATETIME_CLAMP:
millis = max(_min_datetime_ms(tz), min(millis, _max_datetime_ms(tz)))
elif opts.datetime_conversion == DatetimeConversion.DATETIME_AUTO:

View File

@ -125,10 +125,10 @@ from bson.binary import ALL_UUID_SUBTYPES, UUID_SUBTYPE, Binary, UuidRepresentat
from bson.code import Code
from bson.codec_options import CodecOptions, DatetimeConversion
from bson.datetime_ms import (
_MAX_UTC_MS,
EPOCH_AWARE,
DatetimeMS,
_datetime_to_millis,
_max_datetime_ms,
_millis_to_datetime,
)
from bson.dbref import DBRef
@ -844,7 +844,7 @@ def _encode_binary(data: bytes, subtype: int, json_options: JSONOptions) -> Any:
def _encode_datetimems(obj: Any, json_options: JSONOptions) -> dict:
if (
json_options.datetime_representation == DatetimeRepresentation.ISO8601
and 0 <= int(obj) <= _max_datetime_ms()
and 0 <= int(obj) <= _MAX_UTC_MS
):
return _encode_datetime(obj.as_datetime(), json_options)
elif json_options.datetime_representation == DatetimeRepresentation.LEGACY:

View File

@ -16,7 +16,6 @@
from __future__ import annotations
import binascii
import calendar
import datetime
import os
import struct
@ -25,6 +24,7 @@ import time
from random import SystemRandom
from typing import Any, NoReturn, Optional, Type, Union
from bson.datetime_ms import _datetime_to_millis
from bson.errors import InvalidId
from bson.tz_util import utc
@ -131,11 +131,10 @@ class ObjectId:
:param generation_time: :class:`~datetime.datetime` to be used
as the generation time for the resulting ObjectId.
"""
offset = generation_time.utcoffset()
if offset is not None:
generation_time = generation_time - offset
timestamp = calendar.timegm(generation_time.timetuple())
oid = _PACK_INT(int(timestamp)) + b"\x00\x00\x00\x00\x00\x00\x00\x00"
oid = (
_PACK_INT(_datetime_to_millis(generation_time) // 1000)
+ b"\x00\x00\x00\x00\x00\x00\x00\x00"
)
return cls(oid)
@classmethod

View File

@ -31,6 +31,12 @@ PyMongo 4.9 brings a number of improvements including:
:class:`~pymongo.operations.DeleteMany` operations, so
they can be used in the new :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
- Added :func:`repr` support to :class:`bson.tz_util.FixedOffset`.
- Fixed a bug where PyMongo would raise ``InvalidBSON: unhashable type: 'tzfile'``
when using :attr:`~bson.codec_options.DatetimeConversion.DATETIME_CLAMP` or
:attr:`~bson.codec_options.DatetimeConversion.DATETIME_AUTO` with a timezone from dateutil.
- Fixed a bug where PyMongo would raise ``InvalidBSON: date value out of range``
when using :attr:`~bson.codec_options.DatetimeConversion.DATETIME_CLAMP` or
:attr:`~bson.codec_options.DatetimeConversion.DATETIME_AUTO` with a non-UTC timezone.
Issues Resolved
...............

View File

@ -98,7 +98,7 @@ out of MongoDB in US/Pacific time:
>>> aware_times = db.times.with_options(codec_options=CodecOptions(
... tz_aware=True,
... tzinfo=pytz.timezone('US/Pacific')))
>>> result = aware_times.find_one()
>>> result = aware_times.find_one()['date']
datetime.datetime(2002, 10, 27, 6, 0, # doctest: +NORMALIZE_WHITESPACE
tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)

View File

@ -50,7 +50,7 @@ try:
_HAVE_PYMONGOCRYPT = True
except ImportError:
_HAVE_PYMONGOCRYPT = False
MongoCryptCallback = object
AsyncMongoCryptCallback = object
from bson import _dict_to_bson, decode, encode
from bson.binary import STANDARD, UUID_SUBTYPE, Binary
@ -207,10 +207,10 @@ class _EncryptionIO(AsyncMongoCryptCallback): # type: ignore[misc]
:return: The first document from the listCollections command response as BSON.
"""
async with self.client_ref()[database].list_collections(
async with await self.client_ref()[database].list_collections(
filter=RawBSONDocument(filter)
) as cursor:
for doc in cursor:
async for doc in cursor:
return _dict_to_bson(doc, False, _DATA_KEY_OPTS)
return None

View File

@ -297,7 +297,7 @@ async def command(
)
if client and client._encrypter and reply:
decrypted = client._encrypter.decrypt(reply.raw_command_response())
decrypted = await client._encrypter.decrypt(reply.raw_command_response())
response_doc = cast(
"_DocumentOut", _decode_all_selective(decrypted, codec_options, user_fields)[0]
)

View File

@ -309,7 +309,7 @@ class Server:
client = operation.client # type: ignore[assignment]
if client and client._encrypter:
if use_cmd:
decrypted = client._encrypter.decrypt(reply.raw_command_response())
decrypted = await client._encrypter.decrypt(reply.raw_command_response())
docs = _decode_all_selective(decrypted, operation.codec_options, user_fields)
response: Response

View File

@ -1538,7 +1538,7 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
if not _IS_SYNC:
# Add support for contextlib.closing.
aclose = close
close = close
def _get_topology(self) -> Topology:
"""Get the internal :class:`~pymongo.topology.Topology` object.

File diff suppressed because it is too large Load Diff

View File

@ -1362,6 +1362,31 @@ class TestDatetimeConversion(unittest.TestCase):
opts = CodecOptions(datetime_conversion=conversion, tz_aware=True, tzinfo=tz)
self.assertEqual(decode(encoded, opts)["d"], dtm.replace(tzinfo=utc).astimezone(tz))
def test_tz_clamping_non_hashable(self):
class NonHashableTZ(FixedOffset):
__hash__ = None
tz = NonHashableTZ(0, "UTC-non-hashable")
self.assertRaises(TypeError, hash, tz)
# Aware clamping.
opts = CodecOptions(
datetime_conversion=DatetimeConversion.DATETIME_CLAMP, tz_aware=True, tzinfo=tz
)
below = encode({"x": DatetimeMS(_datetime_to_millis(datetime.datetime.min) - 24 * 60 * 60)})
dec_below = decode(below, opts)
self.assertEqual(dec_below["x"], datetime.datetime.min.replace(tzinfo=tz))
within = encode({"x": EPOCH_AWARE.astimezone(tz)})
dec_within = decode(within, opts)
self.assertEqual(dec_within["x"], EPOCH_AWARE.astimezone(tz))
above = encode({"x": DatetimeMS(_datetime_to_millis(datetime.datetime.max) + 24 * 60 * 60)})
dec_above = decode(above, opts)
self.assertEqual(
dec_above["x"],
datetime.datetime.max.replace(tzinfo=tz, microsecond=999000),
)
def test_datetime_auto(self):
# Naive auto, in range.
opts1 = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_AUTO)

View File

@ -18,6 +18,7 @@ from __future__ import annotations
import base64
import copy
import os
import pathlib
import re
import socket
import socketserver
@ -27,6 +28,7 @@ import textwrap
import traceback
import uuid
import warnings
from test import IntegrationTest, PyMongoTestCase, client_context
from threading import Thread
from typing import Any, Dict, Mapping
@ -34,13 +36,11 @@ import pytest
from pymongo.daemon import _spawn_daemon
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.helpers import next
sys.path[0:0] = [""]
from test import (
IntegrationTest,
PyMongoTestCase,
client_context,
unittest,
)
from test.helpers import (
@ -93,6 +93,8 @@ from pymongo.synchronous.encryption import Algorithm, ClientEncryption, QueryTyp
from pymongo.synchronous.mongo_client import MongoClient
from pymongo.write_concern import WriteConcern
_IS_SYNC = True
pytestmark = pytest.mark.encryption
KMS_PROVIDERS = {"local": {"key": b"\x00" * 96}}
@ -216,8 +218,8 @@ class EncryptionIntegrationTest(IntegrationTest):
@classmethod
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
@client_context.require_version_min(4, 2, -1)
def setUpClass(cls):
super().setUpClass()
def _setup_class(cls):
super()._setup_class()
def assertEncrypted(self, val):
self.assertIsInstance(val, Binary)
@ -229,7 +231,11 @@ class EncryptionIntegrationTest(IntegrationTest):
# Location of JSON test files.
BASE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "client-side-encryption")
if _IS_SYNC:
BASE = os.path.join(pathlib.Path(__file__).resolve().parent, "client-side-encryption")
else:
BASE = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "client-side-encryption")
SPEC_PATH = os.path.join(BASE, "spec")
OPTS = CodecOptions()
@ -278,9 +284,11 @@ class TestClientSimple(EncryptionIntegrationTest):
unack = encrypted_coll.with_options(write_concern=WriteConcern(w=0))
unack.insert_one(docs[3])
unack.insert_many(docs[4:], ordered=False)
wait_until(
lambda: self.db.test.count_documents({}) == len(docs), "insert documents with w=0"
)
def count_documents():
return self.db.test.count_documents({}) == len(docs)
wait_until(count_documents, "insert documents with w=0")
# Database.command auto decrypts.
res = client.pymongo_test.command("find", "test", filter={"ssn": "000"})
@ -288,19 +296,19 @@ class TestClientSimple(EncryptionIntegrationTest):
self.assertEqual(decrypted_docs, [{"_id": 0, "ssn": "000"}])
# Collection.find auto decrypts.
decrypted_docs = list(encrypted_coll.find())
decrypted_docs = encrypted_coll.find().to_list()
self.assertEqual(decrypted_docs, docs)
# Collection.find auto decrypts getMores.
decrypted_docs = list(encrypted_coll.find(batch_size=1))
decrypted_docs = encrypted_coll.find(batch_size=1).to_list()
self.assertEqual(decrypted_docs, docs)
# Collection.aggregate auto decrypts.
decrypted_docs = list(encrypted_coll.aggregate([]))
decrypted_docs = (encrypted_coll.aggregate([])).to_list()
self.assertEqual(decrypted_docs, docs)
# Collection.aggregate auto decrypts getMores.
decrypted_docs = list(encrypted_coll.aggregate([], batchSize=1))
decrypted_docs = (encrypted_coll.aggregate([], batchSize=1)).to_list()
self.assertEqual(decrypted_docs, docs)
# Collection.distinct auto decrypts.
@ -402,8 +410,8 @@ class TestEncryptedBulkWrite(BulkTestBase, EncryptionIntegrationTest):
class TestClientMaxWireVersion(IntegrationTest):
@classmethod
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
def setUpClass(cls):
super().setUpClass()
def _setup_class(cls):
super()._setup_class()
@client_context.require_version_max(4, 0, 99)
def test_raise_max_wire_version_error(self):
@ -601,131 +609,130 @@ AWS_TEMP_NO_SESSION_CREDS = {
KMS_TLS_OPTS = {"kmip": {"tlsCAFile": CA_PEM, "tlsCertificateKeyFile": CLIENT_PEM}}
class TestSpec(SpecRunner):
@classmethod
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
def setUpClass(cls):
super().setUpClass()
if _IS_SYNC:
# TODO: Add synchronous SpecRunner (https://jira.mongodb.org/browse/PYTHON-4700)
class TestSpec(SpecRunner):
@classmethod
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
def setUpClass(cls):
super().setUpClass()
def parse_auto_encrypt_opts(self, opts):
"""Parse clientOptions.autoEncryptOpts."""
opts = camel_to_snake_args(opts)
kms_providers = opts["kms_providers"]
if "aws" in kms_providers:
kms_providers["aws"] = AWS_CREDS
if not any(AWS_CREDS.values()):
self.skipTest("AWS environment credentials are not set")
if "awsTemporary" in kms_providers:
kms_providers["aws"] = AWS_TEMP_CREDS
del kms_providers["awsTemporary"]
if not any(AWS_TEMP_CREDS.values()):
self.skipTest("AWS Temp environment credentials are not set")
if "awsTemporaryNoSessionToken" in kms_providers:
kms_providers["aws"] = AWS_TEMP_NO_SESSION_CREDS
del kms_providers["awsTemporaryNoSessionToken"]
if not any(AWS_TEMP_NO_SESSION_CREDS.values()):
self.skipTest("AWS Temp environment credentials are not set")
if "azure" in kms_providers:
kms_providers["azure"] = AZURE_CREDS
if not any(AZURE_CREDS.values()):
self.skipTest("Azure environment credentials are not set")
if "gcp" in kms_providers:
kms_providers["gcp"] = GCP_CREDS
if not any(AZURE_CREDS.values()):
self.skipTest("GCP environment credentials are not set")
if "kmip" in kms_providers:
kms_providers["kmip"] = KMIP_CREDS
opts["kms_tls_options"] = KMS_TLS_OPTS
if "key_vault_namespace" not in opts:
opts["key_vault_namespace"] = "keyvault.datakeys"
if "extra_options" in opts:
opts.update(camel_to_snake_args(opts.pop("extra_options")))
def parse_auto_encrypt_opts(self, opts):
"""Parse clientOptions.autoEncryptOpts."""
opts = camel_to_snake_args(opts)
kms_providers = opts["kms_providers"]
if "aws" in kms_providers:
kms_providers["aws"] = AWS_CREDS
if not any(AWS_CREDS.values()):
self.skipTest("AWS environment credentials are not set")
if "awsTemporary" in kms_providers:
kms_providers["aws"] = AWS_TEMP_CREDS
del kms_providers["awsTemporary"]
if not any(AWS_TEMP_CREDS.values()):
self.skipTest("AWS Temp environment credentials are not set")
if "awsTemporaryNoSessionToken" in kms_providers:
kms_providers["aws"] = AWS_TEMP_NO_SESSION_CREDS
del kms_providers["awsTemporaryNoSessionToken"]
if not any(AWS_TEMP_NO_SESSION_CREDS.values()):
self.skipTest("AWS Temp environment credentials are not set")
if "azure" in kms_providers:
kms_providers["azure"] = AZURE_CREDS
if not any(AZURE_CREDS.values()):
self.skipTest("Azure environment credentials are not set")
if "gcp" in kms_providers:
kms_providers["gcp"] = GCP_CREDS
if not any(AZURE_CREDS.values()):
self.skipTest("GCP environment credentials are not set")
if "kmip" in kms_providers:
kms_providers["kmip"] = KMIP_CREDS
opts["kms_tls_options"] = KMS_TLS_OPTS
if "key_vault_namespace" not in opts:
opts["key_vault_namespace"] = "keyvault.datakeys"
if "extra_options" in opts:
opts.update(camel_to_snake_args(opts.pop("extra_options")))
opts = dict(opts)
return AutoEncryptionOpts(**opts)
opts = dict(opts)
return AutoEncryptionOpts(**opts)
def parse_client_options(self, opts):
"""Override clientOptions parsing to support autoEncryptOpts."""
encrypt_opts = opts.pop("autoEncryptOpts", None)
if encrypt_opts:
opts["auto_encryption_opts"] = self.parse_auto_encrypt_opts(encrypt_opts)
def parse_client_options(self, opts):
"""Override clientOptions parsing to support autoEncryptOpts."""
encrypt_opts = opts.pop("autoEncryptOpts", None)
if encrypt_opts:
opts["auto_encryption_opts"] = self.parse_auto_encrypt_opts(encrypt_opts)
return super().parse_client_options(opts)
return super().parse_client_options(opts)
def get_object_name(self, op):
"""Default object is collection."""
return op.get("object", "collection")
def get_object_name(self, op):
"""Default object is collection."""
return op.get("object", "collection")
def maybe_skip_scenario(self, test):
super().maybe_skip_scenario(test)
desc = test["description"].lower()
if (
"timeoutms applied to listcollections to get collection schema" in desc
and sys.platform in ("win32", "darwin")
):
self.skipTest("PYTHON-3706 flaky test on Windows/macOS")
if "type=symbol" in desc:
self.skipTest("PyMongo does not support the symbol type")
def maybe_skip_scenario(self, test):
super().maybe_skip_scenario(test)
desc = test["description"].lower()
if (
"timeoutms applied to listcollections to get collection schema" in desc
and sys.platform in ("win32", "darwin")
):
self.skipTest("PYTHON-3706 flaky test on Windows/macOS")
if "type=symbol" in desc:
self.skipTest("PyMongo does not support the symbol type")
def setup_scenario(self, scenario_def):
"""Override a test's setup."""
key_vault_data = scenario_def["key_vault_data"]
encrypted_fields = scenario_def["encrypted_fields"]
json_schema = scenario_def["json_schema"]
data = scenario_def["data"]
coll = client_context.client.get_database("keyvault", codec_options=OPTS)["datakeys"]
coll.delete_many({})
if key_vault_data:
coll.insert_many(key_vault_data)
def setup_scenario(self, scenario_def):
"""Override a test's setup."""
key_vault_data = scenario_def["key_vault_data"]
encrypted_fields = scenario_def["encrypted_fields"]
json_schema = scenario_def["json_schema"]
data = scenario_def["data"]
coll = client_context.client.get_database("keyvault", codec_options=OPTS)["datakeys"]
coll.delete_many({})
if key_vault_data:
coll.insert_many(key_vault_data)
db_name = self.get_scenario_db_name(scenario_def)
coll_name = self.get_scenario_coll_name(scenario_def)
db = client_context.client.get_database(db_name, codec_options=OPTS)
coll = db.drop_collection(coll_name, encrypted_fields=encrypted_fields)
wc = WriteConcern(w="majority")
kwargs: Dict[str, Any] = {}
if json_schema:
kwargs["validator"] = {"$jsonSchema": json_schema}
kwargs["codec_options"] = OPTS
if not data:
kwargs["write_concern"] = wc
if encrypted_fields:
kwargs["encryptedFields"] = encrypted_fields
db.create_collection(coll_name, **kwargs)
coll = db[coll_name]
if data:
# Load data.
coll.with_options(write_concern=wc).insert_many(scenario_def["data"])
db_name = self.get_scenario_db_name(scenario_def)
coll_name = self.get_scenario_coll_name(scenario_def)
db = client_context.client.get_database(db_name, codec_options=OPTS)
coll = db.drop_collection(coll_name, encrypted_fields=encrypted_fields)
wc = WriteConcern(w="majority")
kwargs: Dict[str, Any] = {}
if json_schema:
kwargs["validator"] = {"$jsonSchema": json_schema}
kwargs["codec_options"] = OPTS
if not data:
kwargs["write_concern"] = wc
if encrypted_fields:
kwargs["encryptedFields"] = encrypted_fields
db.create_collection(coll_name, **kwargs)
coll = db[coll_name]
if data:
# Load data.
coll.with_options(write_concern=wc).insert_many(scenario_def["data"])
def allowable_errors(self, op):
"""Override expected error classes."""
errors = super().allowable_errors(op)
# An updateOne test expects encryption to error when no $ operator
# appears but pymongo raises a client side ValueError in this case.
if op["name"] == "updateOne":
errors += (ValueError,)
return errors
def allowable_errors(self, op):
"""Override expected error classes."""
errors = super().allowable_errors(op)
# An updateOne test expects encryption to error when no $ operator
# appears but pymongo raises a client side ValueError in this case.
if op["name"] == "updateOne":
errors += (ValueError,)
return errors
def create_test(scenario_def, test, name):
@client_context.require_test_commands
def run_scenario(self):
self.run_scenario(scenario_def, test)
def create_test(scenario_def, test, name):
@client_context.require_test_commands
def run_scenario(self):
self.run_scenario(scenario_def, test)
return run_scenario
return run_scenario
test_creator = SpecTestCreator(create_test, TestSpec, os.path.join(SPEC_PATH, "legacy"))
test_creator.create_tests()
test_creator = SpecTestCreator(create_test, TestSpec, os.path.join(SPEC_PATH, "legacy"))
test_creator.create_tests()
if _HAVE_PYMONGOCRYPT:
globals().update(
generate_test_classes(
os.path.join(SPEC_PATH, "unified"),
module=__name__,
if _HAVE_PYMONGOCRYPT:
globals().update(
generate_test_classes(
os.path.join(SPEC_PATH, "unified"),
module=__name__,
)
)
)
# Prose Tests
ALL_KMS_PROVIDERS = {
@ -797,8 +804,8 @@ class TestDataKeyDoubleEncryption(EncryptionIntegrationTest):
any([all(AWS_CREDS.values()), all(AZURE_CREDS.values()), all(GCP_CREDS.values())]),
"No environment credentials are set",
)
def setUpClass(cls):
super().setUpClass()
def _setup_class(cls):
super()._setup_class()
cls.listener = OvertCommandListener()
cls.client = rs_or_single_client(event_listeners=[cls.listener])
cls.client.db.coll.drop()
@ -830,7 +837,7 @@ class TestDataKeyDoubleEncryption(EncryptionIntegrationTest):
)
@classmethod
def tearDownClass(cls):
def _tearDown_class(cls):
cls.vault.drop()
cls.client.close()
cls.client_encrypted.close()
@ -849,7 +856,7 @@ class TestDataKeyDoubleEncryption(EncryptionIntegrationTest):
cmd = self.listener.started_events[-1]
self.assertEqual("insert", cmd.command_name)
self.assertEqual({"w": "majority"}, cmd.command.get("writeConcern"))
docs = list(self.vault.find({"_id": datakey_id}))
docs = self.vault.find({"_id": datakey_id}).to_list()
self.assertEqual(len(docs), 1)
self.assertEqual(docs[0]["masterKey"]["provider"], provider_name)
@ -989,8 +996,8 @@ class TestViews(EncryptionIntegrationTest):
class TestCorpus(EncryptionIntegrationTest):
@classmethod
@unittest.skipUnless(any(AWS_CREDS.values()), "AWS environment credentials are not set")
def setUpClass(cls):
super().setUpClass()
def _setup_class(cls):
super()._setup_class()
@staticmethod
def kms_providers():
@ -1167,8 +1174,8 @@ class TestBsonSizeBatches(EncryptionIntegrationTest):
listener: OvertCommandListener
@classmethod
def setUpClass(cls):
super().setUpClass()
def _setup_class(cls):
super()._setup_class()
db = client_context.client.db
cls.coll = db.coll
cls.coll.drop()
@ -1196,10 +1203,10 @@ class TestBsonSizeBatches(EncryptionIntegrationTest):
cls.coll_encrypted = cls.client_encrypted.db.coll
@classmethod
def tearDownClass(cls):
def _tearDown_class(cls):
cls.coll_encrypted.drop()
cls.client_encrypted.close()
super().tearDownClass()
super()._tearDown_class()
def test_01_insert_succeeds_under_2MiB(self):
doc = {"_id": "over_2mib_under_16mib", "unencrypted": "a" * _2_MiB}
@ -1268,8 +1275,8 @@ class TestCustomEndpoint(EncryptionIntegrationTest):
any([all(AWS_CREDS.values()), all(AZURE_CREDS.values()), all(GCP_CREDS.values())]),
"No environment credentials are set",
)
def setUpClass(cls):
super().setUpClass()
def _setup_class(cls):
super()._setup_class()
def setUp(self):
kms_providers = {
@ -1537,11 +1544,11 @@ class AzureGCPEncryptionTestMixin:
class TestAzureEncryption(AzureGCPEncryptionTestMixin, EncryptionIntegrationTest):
@classmethod
@unittest.skipUnless(any(AZURE_CREDS.values()), "Azure environment credentials are not set")
def setUpClass(cls):
def _setup_class(cls):
cls.KMS_PROVIDER_MAP = {"azure": AZURE_CREDS}
cls.DEK = json_data(BASE, "custom", "azure-dek.json")
cls.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json")
super().setUpClass()
super()._setup_class()
def test_explicit(self):
return self._test_explicit(
@ -1563,11 +1570,11 @@ class TestAzureEncryption(AzureGCPEncryptionTestMixin, EncryptionIntegrationTest
class TestGCPEncryption(AzureGCPEncryptionTestMixin, EncryptionIntegrationTest):
@classmethod
@unittest.skipUnless(any(GCP_CREDS.values()), "GCP environment credentials are not set")
def setUpClass(cls):
def _setup_class(cls):
cls.KMS_PROVIDER_MAP = {"gcp": GCP_CREDS}
cls.DEK = json_data(BASE, "custom", "gcp-dek.json")
cls.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json")
super().setUpClass()
super()._setup_class()
def test_explicit(self):
return self._test_explicit(
@ -1944,7 +1951,8 @@ class TestBypassSpawningMongocryptdProse(EncryptionIntegrationTest):
@unittest.skipUnless(os.environ.get("TEST_CRYPT_SHARED"), "crypt_shared lib is not installed")
def test_via_loading_shared_library(self):
create_key_vault(
client_context.client.keyvault.datakeys, json_data("external", "external-key.json")
client_context.client.keyvault.datakeys,
json_data("external", "external-key.json"),
)
schemas = {"db.coll": json_data("external", "external-schema.json")}
opts = AutoEncryptionOpts(
@ -1962,7 +1970,7 @@ class TestBypassSpawningMongocryptdProse(EncryptionIntegrationTest):
self.addCleanup(client_encrypted.close)
client_encrypted.db.coll.drop()
client_encrypted.db.coll.insert_one({"encrypted": "test"})
self.assertEncrypted(client_context.client.db.coll.find_one({})["encrypted"])
self.assertEncrypted((client_context.client.db.coll.find_one({}))["encrypted"])
no_mongocryptd_client = MongoClient(
host="mongodb://localhost:47021/db?serverSelectionTimeoutMS=1000"
)
@ -1989,7 +1997,8 @@ class TestBypassSpawningMongocryptdProse(EncryptionIntegrationTest):
listener_t = Thread(target=listener)
listener_t.start()
create_key_vault(
client_context.client.keyvault.datakeys, json_data("external", "external-key.json")
client_context.client.keyvault.datakeys,
json_data("external", "external-key.json"),
)
schemas = {"db.coll": json_data("external", "external-schema.json")}
opts = AutoEncryptionOpts(
@ -2326,11 +2335,12 @@ class TestExplicitQueryableEncryption(EncryptionIntegrationTest):
find_payload = self.client_encryption.encrypt(
val, Algorithm.INDEXED, self.key1_id, query_type=QueryType.EQUALITY, contention_factor=0
)
docs = list(
self.encrypted_client[self.db.name].explicit_encryption.find(
{"encryptedIndexed": find_payload}
)
docs = (
self.encrypted_client[self.db.name]
.explicit_encryption.find({"encryptedIndexed": find_payload})
.to_list()
)
self.assertEqual(len(docs), 1)
self.assertEqual(docs[0]["encryptedIndexed"], val)
@ -2348,11 +2358,12 @@ class TestExplicitQueryableEncryption(EncryptionIntegrationTest):
find_payload = self.client_encryption.encrypt(
val, Algorithm.INDEXED, self.key1_id, query_type=QueryType.EQUALITY, contention_factor=0
)
docs = list(
self.encrypted_client[self.db.name].explicit_encryption.find(
{"encryptedIndexed": find_payload}
)
docs = (
self.encrypted_client[self.db.name]
.explicit_encryption.find({"encryptedIndexed": find_payload})
.to_list()
)
self.assertLessEqual(len(docs), 10)
for doc in docs:
self.assertEqual(doc["encryptedIndexed"], val)
@ -2365,11 +2376,12 @@ class TestExplicitQueryableEncryption(EncryptionIntegrationTest):
query_type=QueryType.EQUALITY,
contention_factor=contention,
)
docs = list(
self.encrypted_client[self.db.name].explicit_encryption.find(
{"encryptedIndexed": find_payload}
)
docs = (
self.encrypted_client[self.db.name]
.explicit_encryption.find({"encryptedIndexed": find_payload})
.to_list()
)
self.assertEqual(len(docs), 10)
for doc in docs:
self.assertEqual(doc["encryptedIndexed"], val)
@ -2381,7 +2393,7 @@ class TestExplicitQueryableEncryption(EncryptionIntegrationTest):
{"_id": 1, "encryptedUnindexed": insert_payload}
)
docs = list(self.encrypted_client[self.db.name].explicit_encryption.find({"_id": 1}))
docs = self.encrypted_client[self.db.name].explicit_encryption.find({"_id": 1}).to_list()
self.assertEqual(len(docs), 1)
self.assertEqual(docs[0]["encryptedUnindexed"], val)
@ -2461,7 +2473,7 @@ class TestRewrapWithSeparateClientEncryption(EncryptionIntegrationTest):
kms_tls_options=KMS_TLS_OPTS,
codec_options=OPTS,
)
self.addCleanup(client_encryption1.close)
self.addCleanup(client_encryption2.close)
# Step 6. Call ``client_encryption2.rewrap_many_data_key`` with an empty ``filter``.
rewrap_many_data_key_result = client_encryption2.rewrap_many_data_key(
@ -2647,7 +2659,8 @@ class TestRangeQueryProse(EncryptionIntegrationTest):
if use_expr:
find_payload = {"$expr": find_payload}
sorted_find = sorted(
self.encrypted_client.db.explicit_encryption.find(find_payload), key=lambda x: x["_id"]
self.encrypted_client.db.explicit_encryption.find(find_payload).to_list(),
key=lambda x: x["_id"],
)
for elem, expected in zip(sorted_find, expected_elems):
self.assertEqual(elem[f"encrypted{name}"], expected)
@ -3073,13 +3086,13 @@ class TestNoSessionsSupport(EncryptionIntegrationTest):
@classmethod
@unittest.skipIf(os.environ.get("TEST_CRYPT_SHARED"), "crypt_shared lib is installed")
def setUpClass(cls):
super().setUpClass()
def _setup_class(cls):
super()._setup_class()
start_mongocryptd(cls.MONGOCRYPTD_PORT)
@classmethod
def tearDownClass(cls):
super().tearDownClass()
def _tearDown_class(cls):
super()._tearDown_class()
def setUp(self) -> None:
self.listener = OvertCommandListener()

View File

@ -39,7 +39,7 @@ from bson.binary import (
UuidRepresentation,
)
from bson.code import Code
from bson.datetime_ms import _max_datetime_ms
from bson.datetime_ms import _MAX_UTC_MS
from bson.dbref import DBRef
from bson.decimal128 import Decimal128
from bson.int64 import Int64
@ -257,7 +257,7 @@ class TestJsonUtil(unittest.TestCase):
def test_datetime_ms(self):
# Test ISO8601 in-range
dat_min: dict[str, Any] = {"x": DatetimeMS(0)}
dat_max: dict[str, Any] = {"x": DatetimeMS(_max_datetime_ms())}
dat_max: dict[str, Any] = {"x": DatetimeMS(_MAX_UTC_MS)}
opts = JSONOptions(datetime_representation=DatetimeRepresentation.ISO8601)
self.assertEqual(
@ -271,7 +271,7 @@ class TestJsonUtil(unittest.TestCase):
# Test ISO8601 out-of-range
dat_min = {"x": DatetimeMS(-1)}
dat_max = {"x": DatetimeMS(_max_datetime_ms() + 1)}
dat_max = {"x": DatetimeMS(_MAX_UTC_MS + 1)}
self.assertEqual('{"x": {"$date": {"$numberLong": "-1"}}}', json_util.dumps(dat_min))
self.assertEqual(
@ -302,7 +302,7 @@ class TestJsonUtil(unittest.TestCase):
# Test decode from datetime.datetime to DatetimeMS
dat_min = {"x": datetime.datetime.min}
dat_max = {"x": DatetimeMS(_max_datetime_ms()).as_datetime(CodecOptions(tz_aware=False))}
dat_max = {"x": DatetimeMS(_MAX_UTC_MS).as_datetime(CodecOptions(tz_aware=False))}
opts = JSONOptions(
datetime_representation=DatetimeRepresentation.ISO8601,
datetime_conversion=DatetimeConversion.DATETIME_MS,

View File

@ -95,9 +95,6 @@ class TestObjectId(unittest.TestCase):
self.assertTrue(d2 - d1 < datetime.timedelta(seconds=2))
def test_from_datetime(self):
if "PyPy 1.8.0" in sys.version:
# See https://bugs.pypy.org/issue1092
raise SkipTest("datetime.timedelta is broken in pypy 1.8.0")
d = datetime.datetime.now(tz=datetime.timezone.utc).replace(tzinfo=None)
d = d - datetime.timedelta(microseconds=d.microsecond)
oid = ObjectId.from_datetime(d)

View File

@ -96,6 +96,7 @@ replacements = {
"async-transactions-ref": "transactions-ref",
"async-snapshot-reads-ref": "snapshot-reads-ref",
"default_async": "default",
"aclose": "close",
"PyMongo|async": "PyMongo",
}
@ -158,6 +159,7 @@ converted_tests = [
"test_collection.py",
"test_cursor.py",
"test_database.py",
"test_encryption.py",
"test_logger.py",
"test_session.py",
"test_transactions.py",