Skip to content

Commit f69fee7

Browse files
authored
Merge branch 'master' into PYTHON-4706
2 parents 644a877 + 29bbf77 commit f69fee7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+4373
-299
lines changed

.evergreen/config.yml

Lines changed: 53 additions & 90 deletions
Large diffs are not rendered by default.

.evergreen/scripts/configure-env.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/bin/bash -ex
2+
3+
# Get the current unique version of this checkout
4+
# shellcheck disable=SC2154
5+
if [ "$is_patch" = "true" ]; then
6+
# shellcheck disable=SC2154
7+
CURRENT_VERSION="$(git describe)-patch-$version_id"
8+
else
9+
CURRENT_VERSION=latest
10+
fi
11+
12+
PROJECT_DIRECTORY="$(pwd)"
13+
DRIVERS_TOOLS="$(dirname $PROJECT_DIRECTORY)/drivers-tools"
14+
15+
# Python has cygwin path problems on Windows. Detect prospective mongo-orchestration home directory
16+
if [ "Windows_NT" = "$OS" ]; then # Magic variable in cygwin
17+
DRIVERS_TOOLS=$(cygpath -m $DRIVERS_TOOLS)
18+
PROJECT_DIRECTORY=$(cygpath -m $PROJECT_DIRECTORY)
19+
fi
20+
21+
SCRIPT_DIR="$PROJECT_DIRECTORY/.evergreen/scripts"
22+
23+
if [ -f "$SCRIPT_DIR/env.sh" ]; then
24+
echo "Reading $SCRIPT_DIR/env.sh file"
25+
. "$SCRIPT_DIR/env.sh"
26+
exit 0
27+
fi
28+
29+
export MONGO_ORCHESTRATION_HOME="$DRIVERS_TOOLS/.evergreen/orchestration"
30+
export MONGODB_BINARIES="$DRIVERS_TOOLS/mongodb/bin"
31+
32+
cat <<EOT > $SCRIPT_DIR/env.sh
33+
set -o errexit
34+
export PROJECT_DIRECTORY="$PROJECT_DIRECTORY"
35+
export CURRENT_VERSION="$CURRENT_VERSION"
36+
export SKIP_LEGACY_SHELL=1
37+
export DRIVERS_TOOLS="$DRIVERS_TOOLS"
38+
export MONGO_ORCHESTRATION_HOME="$MONGO_ORCHESTRATION_HOME"
39+
export MONGODB_BINARIES="$MONGODB_BINARIES"
40+
export PROJECT_DIRECTORY="$PROJECT_DIRECTORY"
41+
42+
export TMPDIR="$MONGO_ORCHESTRATION_HOME/db"
43+
export PATH="$MONGODB_BINARIES:$PATH"
44+
# shellcheck disable=SC2154
45+
export PROJECT="$project"
46+
export PIP_QUIET=1
47+
EOT
48+
49+
# Add these expansions to make it easier to call out tests scripts from the EVG yaml
50+
cat <<EOT > expansion.yml
51+
DRIVERS_TOOLS: "$DRIVERS_TOOLS"
52+
PROJECT_DIRECTORY: "$PROJECT_DIRECTORY"
53+
EOT

.github/workflows/test-python.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,4 @@ jobs:
209209
ls
210210
which python
211211
pip install -e ".[test]"
212-
PYMONGO_MUST_CONNECT=1 pytest -v test/test_client_context.py
212+
PYMONGO_MUST_CONNECT=1 pytest -v -k client_context

CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,10 @@ you are attempting to validate new spec tests in PyMongo.
248248
## Making a Release
249249

250250
Follow the [Python Driver Release Process Uncyclo](https://wiki.corp.mongodb.com/display/DRIVERS/Python+Driver+Release+Process).
251+
252+
## Converting a test to async
253+
The `tools/convert_test_to_async.py` script takes in an existing synchronous test file and outputs a
254+
partially-converted asynchronous version of the same name to the `test/asynchronous` directory.
255+
Use this generated file as a starting point for the completed conversion.
256+
257+
The script is used like so: `python tools/convert_test_to_async.py [test_file.py]`

bson/_cbsonmodule.c

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,11 @@ static int millis_from_datetime_ms(PyObject* dt, long long* out){
383383
static PyObject* decode_datetime(PyObject* self, long long millis, const codec_options_t* options){
384384
PyObject* naive = NULL;
385385
PyObject* replace = NULL;
386-
PyObject* args = NULL;
387-
PyObject* kwargs = NULL;
388386
PyObject* value = NULL;
389387
struct module_state *state = GETSTATE(self);
388+
if (!state) {
389+
goto invalid;
390+
}
390391
if (options->datetime_conversion == DATETIME_MS){
391392
return datetime_ms_from_millis(self, millis);
392393
}
@@ -414,8 +415,8 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
414415
Py_DECREF(utcoffset);
415416
return 0;
416417
}
417-
min_millis_offset = (PyDateTime_DELTA_GET_DAYS(utcoffset) * 86400 +
418-
PyDateTime_DELTA_GET_SECONDS(utcoffset)) * 1000 +
418+
min_millis_offset = (PyDateTime_DELTA_GET_DAYS(utcoffset) * (int64_t)86400 +
419+
PyDateTime_DELTA_GET_SECONDS(utcoffset)) * (int64_t)1000 +
419420
(PyDateTime_DELTA_GET_MICROSECONDS(utcoffset) / 1000);
420421
}
421422
Py_DECREF(utcoffset);
@@ -433,8 +434,8 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
433434
Py_DECREF(utcoffset);
434435
return 0;
435436
}
436-
max_millis_offset = (PyDateTime_DELTA_GET_DAYS(utcoffset) * 86400 +
437-
PyDateTime_DELTA_GET_SECONDS(utcoffset)) * 1000 +
437+
max_millis_offset = (PyDateTime_DELTA_GET_DAYS(utcoffset) * (int64_t)86400 +
438+
PyDateTime_DELTA_GET_SECONDS(utcoffset)) * (int64_t)1000 +
438439
(PyDateTime_DELTA_GET_MICROSECONDS(utcoffset) / 1000);
439440
}
440441
Py_DECREF(utcoffset);
@@ -487,8 +488,6 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
487488
invalid:
488489
Py_XDECREF(naive);
489490
Py_XDECREF(replace);
490-
Py_XDECREF(args);
491-
Py_XDECREF(kwargs);
492491
return value;
493492
}
494493

doc/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ Changelog
44
Changes in Version 4.9.0
55
-------------------------
66

7+
.. warning:: Driver support for MongoDB 3.6 reached end of life in April 2024.
8+
PyMongo 4.9 will be the last release to support MongoDB 3.6.
9+
710
PyMongo 4.9 brings a number of improvements including:
811

912
- Added support for MongoDB 8.0.
1013
- A new asynchronous API with full asyncio support.
1114
- Added support for In-Use Encryption range queries with MongoDB 8.0.
1215
Added :attr:`~pymongo.encryption.Algorithm.RANGE`.
1316
``sparsity`` and ``trim_factor`` are now optional in :class:`~pymongo.encryption_options.RangeOpts`.
17+
- Added support for the "delegated" option for the KMIP ``master_key`` in
18+
:meth:`~pymongo.encryption.ClientEncryption.create_data_key`.
1419
- pymongocrypt>=1.10 is now required for :ref:`In-Use Encryption` support.
1520
- Added :meth:`~pymongo.cursor.Cursor.to_list` to :class:`~pymongo.cursor.Cursor`,
1621
:class:`~pymongo.command_cursor.CommandCursor`,

gridfs/asynchronous/grid_file.py

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,24 +1176,6 @@ def __getattr__(self, name: str) -> Any:
11761176
raise AttributeError("GridIn object has no attribute '%s'" % name)
11771177

11781178
def __setattr__(self, name: str, value: Any) -> None:
1179-
# For properties of this instance like _buffer, or descriptors set on
1180-
# the class like filename, use regular __setattr__
1181-
if name in self.__dict__ or name in self.__class__.__dict__:
1182-
object.__setattr__(self, name, value)
1183-
else:
1184-
if _IS_SYNC:
1185-
# All other attributes are part of the document in db.fs.files.
1186-
# Store them to be sent to server on close() or if closed, send
1187-
# them now.
1188-
self._file[name] = value
1189-
if self._closed:
1190-
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1191-
else:
1192-
raise AttributeError(
1193-
"AsyncGridIn does not support __setattr__. Use AsyncGridIn.set() instead"
1194-
)
1195-
1196-
async def set(self, name: str, value: Any) -> None:
11971179
# For properties of this instance like _buffer, or descriptors set on
11981180
# the class like filename, use regular __setattr__
11991181
if name in self.__dict__ or name in self.__class__.__dict__:
@@ -1204,9 +1186,17 @@ async def set(self, name: str, value: Any) -> None:
12041186
# them now.
12051187
self._file[name] = value
12061188
if self._closed:
1207-
await self._coll.files.update_one(
1208-
{"_id": self._file["_id"]}, {"$set": {name: value}}
1209-
)
1189+
if _IS_SYNC:
1190+
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1191+
else:
1192+
raise AttributeError(
1193+
"AsyncGridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use AsyncGridIn.set() instead"
1194+
)
1195+
1196+
async def set(self, name: str, value: Any) -> None:
1197+
self._file[name] = value
1198+
if self._closed:
1199+
await self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
12101200

12111201
async def _flush_data(self, data: Any, force: bool = False) -> None:
12121202
"""Flush `data` to a chunk."""
@@ -1400,7 +1390,11 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any:
14001390
return False
14011391

14021392

1403-
class AsyncGridOut(io.IOBase):
1393+
GRIDOUT_BASE_CLASS = io.IOBase if _IS_SYNC else object # type: Any
1394+
1395+
1396+
class AsyncGridOut(GRIDOUT_BASE_CLASS): # type: ignore
1397+
14041398
"""Class to read data out of GridFS."""
14051399

14061400
def __init__(
@@ -1460,6 +1454,8 @@ def __init__(
14601454
self._position = 0
14611455
self._file = file_document
14621456
self._session = session
1457+
if not _IS_SYNC:
1458+
self.closed = False
14631459

14641460
_id: Any = _a_grid_out_property("_id", "The ``'_id'`` value for this file.")
14651461
filename: str = _a_grid_out_property("filename", "Name of this file.")
@@ -1486,16 +1482,43 @@ def __init__(
14861482
_file: Any
14871483
_chunk_iter: Any
14881484

1489-
async def __anext__(self) -> bytes:
1490-
return super().__next__()
1485+
if not _IS_SYNC:
1486+
closed: bool
14911487

1492-
def __next__(self) -> bytes: # noqa: F811, RUF100
1493-
if _IS_SYNC:
1494-
return super().__next__()
1495-
else:
1496-
raise TypeError(
1497-
"AsyncGridOut does not support synchronous iteration. Use `async for` instead"
1498-
)
1488+
async def __anext__(self) -> bytes:
1489+
line = await self.readline()
1490+
if line:
1491+
return line
1492+
raise StopAsyncIteration()
1493+
1494+
async def to_list(self) -> list[bytes]:
1495+
return [x async for x in self] # noqa: C416, RUF100
1496+
1497+
async def readline(self, size: int = -1) -> bytes:
1498+
"""Read one line or up to `size` bytes from the file.
1499+
1500+
:param size: the maximum number of bytes to read
1501+
"""
1502+
return await self._read_size_or_line(size=size, line=True)
1503+
1504+
async def readlines(self, size: int = -1) -> list[bytes]:
1505+
"""Read one line or up to `size` bytes from the file.
1506+
1507+
:param size: the maximum number of bytes to read
1508+
"""
1509+
await self.open()
1510+
lines = []
1511+
remainder = int(self.length) - self._position
1512+
bytes_read = 0
1513+
while remainder > 0:
1514+
line = await self._read_size_or_line(line=True)
1515+
bytes_read += len(line)
1516+
lines.append(line)
1517+
remainder = int(self.length) - self._position
1518+
if 0 < size < bytes_read:
1519+
break
1520+
1521+
return lines
14991522

15001523
async def open(self) -> None:
15011524
if not self._file:
@@ -1616,18 +1639,11 @@ async def read(self, size: int = -1) -> bytes:
16161639
"""
16171640
return await self._read_size_or_line(size=size)
16181641

1619-
async def readline(self, size: int = -1) -> bytes: # type: ignore[override]
1620-
"""Read one line or up to `size` bytes from the file.
1621-
1622-
:param size: the maximum number of bytes to read
1623-
"""
1624-
return await self._read_size_or_line(size=size, line=True)
1625-
16261642
def tell(self) -> int:
16271643
"""Return the current position of this file."""
16281644
return self._position
16291645

1630-
async def seek(self, pos: int, whence: int = _SEEK_SET) -> int: # type: ignore[override]
1646+
async def seek(self, pos: int, whence: int = _SEEK_SET) -> int:
16311647
"""Set the current position of this file.
16321648
16331649
:param pos: the position (or offset if using relative
@@ -1690,12 +1706,15 @@ def __aiter__(self) -> AsyncGridOut:
16901706
"""
16911707
return self
16921708

1693-
async def close(self) -> None: # type: ignore[override]
1709+
async def close(self) -> None:
16941710
"""Make GridOut more generically file-like."""
16951711
if self._chunk_iter:
16961712
await self._chunk_iter.close()
16971713
self._chunk_iter = None
1698-
super().close()
1714+
if _IS_SYNC:
1715+
super().close()
1716+
else:
1717+
self.closed = True
16991718

17001719
def write(self, value: Any) -> NoReturn:
17011720
raise io.UnsupportedOperation("write")

gridfs/grid_file_shared.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,31 @@ def _a_grid_in_property(
3838
) -> Any:
3939
"""Create a GridIn property."""
4040

41+
warn_str = ""
42+
if docstring.startswith("DEPRECATED,"):
43+
warn_str = (
44+
f"GridIn property '{field_name}' is deprecated and will be removed in PyMongo 5.0"
45+
)
46+
4147
def getter(self: Any) -> Any:
48+
if warn_str:
49+
warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning)
4250
if closed_only and not self._closed:
4351
raise AttributeError("can only get %r on a closed file" % field_name)
4452
# Protect against PHP-237
4553
if field_name == "length":
4654
return self._file.get(field_name, 0)
4755
return self._file.get(field_name, None)
4856

57+
def setter(self: Any, value: Any) -> Any:
58+
if warn_str:
59+
warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning)
60+
if self._closed:
61+
raise InvalidOperation(
62+
"AsyncGridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use AsyncGridIn.set() instead"
63+
)
64+
self._file[field_name] = value
65+
4966
if read_only:
5067
docstring += "\n\nThis attribute is read-only."
5168
elif closed_only:
@@ -56,6 +73,8 @@ def getter(self: Any) -> Any:
5673
"has been called.",
5774
)
5875

76+
if not read_only and not closed_only:
77+
return property(getter, setter, doc=docstring)
5978
return property(getter, doc=docstring)
6079

6180

0 commit comments

Comments
 (0)