Skip to content

Commit 2e2fee4

Browse files
committed
Merge branch 'master' of github.com:mongodb/mongo-python-driver into PYTHON-3152
2 parents 6365e70 + 6fe8543 commit 2e2fee4

File tree

12 files changed

+164
-90
lines changed

12 files changed

+164
-90
lines changed

.github/workflows/create-release-branch.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ jobs:
3333
outputs:
3434
version: ${{ steps.pre-publish.outputs.version }}
3535
steps:
36-
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v2
36+
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
3737
with:
3838
app_id: ${{ vars.APP_ID }}
3939
private_key: ${{ secrets.APP_PRIVATE_KEY }}
40-
- uses: mongodb-labs/drivers-github-tools/setup@v2
40+
- uses: mongodb-labs/drivers-github-tools/setup@v3
4141
with:
4242
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
4343
aws_region_name: ${{ vars.AWS_REGION_NAME }}
4444
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
4545
artifactory_username: ${{ vars.ARTIFACTORY_USERNAME }}
4646
- name: Get hatch
4747
run: pip install hatch
48-
- uses: mongodb-labs/drivers-github-tools/create-branch@v2
48+
- uses: mongodb-labs/drivers-github-tools/create-branch@v3
4949
id: create-branch
5050
with:
5151
branch_name: ${{ inputs.branch_name }}

.github/workflows/release-python.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,16 @@ jobs:
3838
outputs:
3939
version: ${{ steps.pre-publish.outputs.version }}
4040
steps:
41-
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v2
41+
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
4242
with:
4343
app_id: ${{ vars.APP_ID }}
4444
private_key: ${{ secrets.APP_PRIVATE_KEY }}
45-
- uses: mongodb-labs/drivers-github-tools/setup@v2
45+
- uses: mongodb-labs/drivers-github-tools/setup@v3
4646
with:
4747
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
4848
aws_region_name: ${{ vars.AWS_REGION_NAME }}
4949
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
50-
artifactory_username: ${{ vars.ARTIFACTORY_USERNAME }}
51-
- uses: mongodb-labs/drivers-github-tools/python/pre-publish@v2
50+
- uses: mongodb-labs/drivers-github-tools/python/pre-publish@v3
5251
id: pre-publish
5352
with:
5453
dry_run: ${{ env.DRY_RUN }}
@@ -100,17 +99,16 @@ jobs:
10099
attestations: write
101100
security-events: write
102101
steps:
103-
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v2
102+
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
104103
with:
105104
app_id: ${{ vars.APP_ID }}
106105
private_key: ${{ secrets.APP_PRIVATE_KEY }}
107-
- uses: mongodb-labs/drivers-github-tools/setup@v2
106+
- uses: mongodb-labs/drivers-github-tools/setup@v3
108107
with:
109108
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
110109
aws_region_name: ${{ vars.AWS_REGION_NAME }}
111110
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
112-
artifactory_username: ${{ vars.ARTIFACTORY_USERNAME }}
113-
- uses: mongodb-labs/drivers-github-tools/python/post-publish@v2
111+
- uses: mongodb-labs/drivers-github-tools/python/post-publish@v3
114112
with:
115113
following_version: ${{ env.FOLLOWING_VERSION }}
116114
product_name: ${{ env.PRODUCT_NAME }}

.github/workflows/zizmor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ jobs:
1818
with:
1919
persist-credentials: false
2020
- name: Run zizmor 🌈
21-
uses: zizmorcore/zizmor-action@873539476a7f9b0da7504d0d9e9a6a5275094d98
21+
uses: zizmorcore/zizmor-action@0696496a48b64e0568faa46ddaf5f6fe48b83b04

pymongo/asynchronous/encryption.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@
6666
from pymongo.asynchronous.mongo_client import AsyncMongoClient
6767
from pymongo.common import CONNECT_TIMEOUT
6868
from pymongo.daemon import _spawn_daemon
69-
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
69+
from pymongo.encryption_options import (
70+
AutoEncryptionOpts,
71+
RangeOpts,
72+
TextOpts,
73+
check_min_pymongocrypt,
74+
)
7075
from pymongo.errors import (
7176
ConfigurationError,
7277
EncryptedCollectionError,
@@ -675,6 +680,8 @@ def __init__(
675680
"python -m pip install --upgrade 'pymongo[encryption]'"
676681
)
677682

683+
check_min_pymongocrypt()
684+
678685
if not isinstance(codec_options, CodecOptions):
679686
raise TypeError(
680687
f"codec_options must be an instance of bson.codec_options.CodecOptions, not {type(codec_options)}"

pymongo/asynchronous/srv_resolver.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import random
2020
from typing import TYPE_CHECKING, Any, Optional, Union
2121

22-
from pymongo.common import CONNECT_TIMEOUT
22+
from pymongo.common import CONNECT_TIMEOUT, check_for_min_version
2323
from pymongo.errors import ConfigurationError
2424

2525
if TYPE_CHECKING:
@@ -32,6 +32,14 @@ def _have_dnspython() -> bool:
3232
try:
3333
import dns # noqa: F401
3434

35+
dns_version, required_version, is_valid = check_for_min_version("dnspython")
36+
if not is_valid:
37+
raise RuntimeError(
38+
f"pymongo requires dnspython>={required_version}, "
39+
f"found version {dns_version}. "
40+
"Install a compatible version with pip"
41+
)
42+
3543
return True
3644
except ImportError:
3745
return False
@@ -80,6 +88,8 @@ def __init__(
8088
srv_service_name: str,
8189
srv_max_hosts: int = 0,
8290
):
91+
# Ensure the version of dnspython is compatible.
92+
_have_dnspython()
8393
self.__fqdn = fqdn
8494
self.__srv = srv_service_name
8595
self.__connect_timeout = connect_timeout or CONNECT_TIMEOUT

pymongo/common.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import warnings
2121
from collections import OrderedDict, abc
2222
from difflib import get_close_matches
23+
from importlib.metadata import requires, version
2324
from typing import (
2425
TYPE_CHECKING,
2526
Any,
@@ -1092,3 +1093,91 @@ def has_c() -> bool:
10921093
return True
10931094
except ImportError:
10941095
return False
1096+
1097+
1098+
class Version(tuple[int, ...]):
1099+
"""A class that can be used to compare version strings."""
1100+
1101+
def __new__(cls, *version: int) -> Version:
1102+
padded_version = cls._padded(version, 4)
1103+
return super().__new__(cls, tuple(padded_version))
1104+
1105+
@classmethod
1106+
def _padded(cls, iter: Any, length: int, padding: int = 0) -> list[int]:
1107+
as_list = list(iter)
1108+
if len(as_list) < length:
1109+
for _ in range(length - len(as_list)):
1110+
as_list.append(padding)
1111+
return as_list
1112+
1113+
@classmethod
1114+
def from_string(cls, version_string: str) -> Version:
1115+
mod = 0
1116+
bump_patch_level = False
1117+
if version_string.endswith("+"):
1118+
version_string = version_string[0:-1]
1119+
mod = 1
1120+
elif version_string.endswith("-pre-"):
1121+
version_string = version_string[0:-5]
1122+
mod = -1
1123+
elif version_string.endswith("-"):
1124+
version_string = version_string[0:-1]
1125+
mod = -1
1126+
# Deal with .devX substrings
1127+
if ".dev" in version_string:
1128+
version_string = version_string[0 : version_string.find(".dev")]
1129+
mod = -1
1130+
# Deal with '-rcX' substrings
1131+
if "-rc" in version_string:
1132+
version_string = version_string[0 : version_string.find("-rc")]
1133+
mod = -1
1134+
# Deal with git describe generated substrings
1135+
elif "-" in version_string:
1136+
version_string = version_string[0 : version_string.find("-")]
1137+
mod = -1
1138+
bump_patch_level = True
1139+
1140+
version = [int(part) for part in version_string.split(".")]
1141+
version = cls._padded(version, 3)
1142+
# Make from_string and from_version_array agree. For example:
1143+
# MongoDB Enterprise > db.runCommand('buildInfo').versionArray
1144+
# [ 3, 2, 1, -100 ]
1145+
# MongoDB Enterprise > db.runCommand('buildInfo').version
1146+
# 3.2.0-97-g1ef94fe
1147+
if bump_patch_level:
1148+
version[-1] += 1
1149+
version.append(mod)
1150+
1151+
return Version(*version)
1152+
1153+
@classmethod
1154+
def from_version_array(cls, version_array: Any) -> Version:
1155+
version = list(version_array)
1156+
if version[-1] < 0:
1157+
version[-1] = -1
1158+
version = cls._padded(version, 3)
1159+
return Version(*version)
1160+
1161+
def at_least(self, *other_version: Any) -> bool:
1162+
return self >= Version(*other_version)
1163+
1164+
def __str__(self) -> str:
1165+
return ".".join(map(str, self))
1166+
1167+
1168+
def check_for_min_version(package_name: str) -> tuple[str, str, bool]:
1169+
"""Test whether an installed package is of the desired version."""
1170+
package_version_str = version(package_name)
1171+
package_version = Version.from_string(package_version_str)
1172+
# Dependency is expected to be in one of the forms:
1173+
# "pymongocrypt<2.0.0,>=1.13.0; extra == 'encryption'"
1174+
# 'dnspython<3.0.0,>=1.16.0'
1175+
#
1176+
requirements = requires("pymongo")
1177+
assert requirements is not None
1178+
requirement = [i for i in requirements if i.startswith(package_name)][0] # noqa: RUF015
1179+
if ";" in requirement:
1180+
requirement = requirement.split(";")[0]
1181+
required_version = requirement[requirement.find(">=") + 2 :]
1182+
is_valid = package_version >= Version.from_string(required_version)
1183+
return package_version_str, required_version, is_valid

pymongo/encryption_options.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pymongo.uri_parser_shared import _parse_kms_tls_options
2424

2525
try:
26-
import pymongocrypt # type:ignore[import-untyped] # noqa: F401
26+
import pymongocrypt # type:ignore[import-untyped] # noqa: F401
2727

2828
# Check for pymongocrypt>=1.10.
2929
from pymongocrypt import synchronous as _ # noqa: F401
@@ -32,14 +32,26 @@
3232
except ImportError:
3333
_HAVE_PYMONGOCRYPT = False
3434
from bson import int64
35-
from pymongo.common import validate_is_mapping
35+
from pymongo.common import check_for_min_version, validate_is_mapping
3636
from pymongo.errors import ConfigurationError
3737

3838
if TYPE_CHECKING:
3939
from pymongo.pyopenssl_context import SSLContext
4040
from pymongo.typings import _AgnosticMongoClient
4141

4242

43+
def check_min_pymongocrypt() -> None:
44+
"""Raise an appropriate error if the min pymongocrypt is not installed."""
45+
pymongocrypt_version, required_version, is_valid = check_for_min_version("pymongocrypt")
46+
if not is_valid:
47+
raise ConfigurationError(
48+
f"client side encryption requires pymongocrypt>={required_version}, "
49+
f"found version {pymongocrypt_version}. "
50+
"Install a compatible version with: "
51+
"python -m pip install 'pymongo[encryption]'"
52+
)
53+
54+
4355
class AutoEncryptionOpts:
4456
"""Options to configure automatic client-side field level encryption."""
4557

@@ -215,6 +227,7 @@ def __init__(
215227
"install a compatible version with: "
216228
"python -m pip install 'pymongo[encryption]'"
217229
)
230+
check_min_pymongocrypt()
218231
if encrypted_fields_map:
219232
validate_is_mapping("encrypted_fields_map", encrypted_fields_map)
220233
self._encrypted_fields_map = encrypted_fields_map

pymongo/synchronous/encryption.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@
6161
from pymongo import _csot
6262
from pymongo.common import CONNECT_TIMEOUT
6363
from pymongo.daemon import _spawn_daemon
64-
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
64+
from pymongo.encryption_options import (
65+
AutoEncryptionOpts,
66+
RangeOpts,
67+
TextOpts,
68+
check_min_pymongocrypt,
69+
)
6570
from pymongo.errors import (
6671
ConfigurationError,
6772
EncryptedCollectionError,
@@ -672,6 +677,8 @@ def __init__(
672677
"python -m pip install --upgrade 'pymongo[encryption]'"
673678
)
674679

680+
check_min_pymongocrypt()
681+
675682
if not isinstance(codec_options, CodecOptions):
676683
raise TypeError(
677684
f"codec_options must be an instance of bson.codec_options.CodecOptions, not {type(codec_options)}"

pymongo/synchronous/srv_resolver.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import random
2020
from typing import TYPE_CHECKING, Any, Optional, Union
2121

22-
from pymongo.common import CONNECT_TIMEOUT
22+
from pymongo.common import CONNECT_TIMEOUT, check_for_min_version
2323
from pymongo.errors import ConfigurationError
2424

2525
if TYPE_CHECKING:
@@ -32,6 +32,14 @@ def _have_dnspython() -> bool:
3232
try:
3333
import dns # noqa: F401
3434

35+
dns_version, required_version, is_valid = check_for_min_version("dnspython")
36+
if not is_valid:
37+
raise RuntimeError(
38+
f"pymongo requires dnspython>={required_version}, "
39+
f"found version {dns_version}. "
40+
"Install a compatible version with pip"
41+
)
42+
3543
return True
3644
except ImportError:
3745
return False
@@ -80,6 +88,8 @@ def __init__(
8088
srv_service_name: str,
8189
srv_max_hosts: int = 0,
8290
):
91+
# Ensure the version of dnspython is compatible.
92+
_have_dnspython()
8393
self.__fqdn = fqdn
8494
self.__srv = srv_service_name
8595
self.__connect_timeout = connect_timeout or CONNECT_TIMEOUT

test/asynchronous/test_collection.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
OvertCommandListener,
4141
async_wait_until,
4242
)
43+
from test.version import Version
4344

4445
from bson import encode
4546
from bson.codec_options import CodecOptions
@@ -335,8 +336,6 @@ async def test_create_index(self):
335336
await db.test.create_index(["hello", ("world", DESCENDING)])
336337
await db.test.create_index({"hello": 1}.items()) # type:ignore[arg-type]
337338

338-
# TODO: PYTHON-5491 - remove version max
339-
@async_client_context.require_version_max(8, 0, -1)
340339
async def test_drop_index(self):
341340
db = self.db
342341
await db.test.drop_indexes()
@@ -348,7 +347,10 @@ async def test_drop_index(self):
348347
await db.test.drop_index(name)
349348

350349
# Drop it again.
351-
with self.assertRaises(OperationFailure):
350+
if async_client_context.version < Version(8, 3, -1):
351+
with self.assertRaises(OperationFailure):
352+
await db.test.drop_index(name)
353+
else:
352354
await db.test.drop_index(name)
353355
self.assertEqual(len(await db.test.index_information()), 2)
354356
self.assertIn("hello_1", await db.test.index_information())

0 commit comments

Comments
 (0)