Skip to content

Commit 484374e

Browse files
committed
PYTHON-3414 Improve error message when using incompatible pymongocrypt version
1 parent ef59602 commit 484374e

File tree

5 files changed

+107
-66
lines changed

5 files changed

+107
-66
lines changed

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/common.py

Lines changed: 74 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
2324
from typing import (
2425
TYPE_CHECKING,
2526
Any,
@@ -1092,3 +1093,76 @@ def has_c() -> bool:
10921093
return True
10931094
except ImportError:
10941095
return False
1096+
1097+
1098+
class Version(tuple):
1099+
def __new__(cls, *version):
1100+
padded_version = cls._padded(version, 4)
1101+
return super().__new__(cls, tuple(padded_version))
1102+
1103+
@classmethod
1104+
def _padded(cls, iter, length, padding=0):
1105+
as_list = list(iter)
1106+
if len(as_list) < length:
1107+
for _ in range(length - len(as_list)):
1108+
as_list.append(padding)
1109+
return as_list
1110+
1111+
@classmethod
1112+
def from_string(cls, version_string):
1113+
mod = 0
1114+
bump_patch_level = False
1115+
if version_string.endswith("+"):
1116+
version_string = version_string[0:-1]
1117+
mod = 1
1118+
elif version_string.endswith("-pre-"):
1119+
version_string = version_string[0:-5]
1120+
mod = -1
1121+
elif version_string.endswith("-"):
1122+
version_string = version_string[0:-1]
1123+
mod = -1
1124+
# Deal with '-rcX' substrings
1125+
if "-rc" in version_string:
1126+
version_string = version_string[0 : version_string.find("-rc")]
1127+
mod = -1
1128+
# Deal with git describe generated substrings
1129+
elif "-" in version_string:
1130+
version_string = version_string[0 : version_string.find("-")]
1131+
mod = -1
1132+
bump_patch_level = True
1133+
1134+
version = [int(part) for part in version_string.split(".")]
1135+
version = cls._padded(version, 3)
1136+
# Make from_string and from_version_array agree. For example:
1137+
# MongoDB Enterprise > db.runCommand('buildInfo').versionArray
1138+
# [ 3, 2, 1, -100 ]
1139+
# MongoDB Enterprise > db.runCommand('buildInfo').version
1140+
# 3.2.0-97-g1ef94fe
1141+
if bump_patch_level:
1142+
version[-1] += 1
1143+
version.append(mod)
1144+
1145+
return Version(*version)
1146+
1147+
@classmethod
1148+
def from_version_array(cls, version_array):
1149+
version = list(version_array)
1150+
if version[-1] < 0:
1151+
version[-1] = -1
1152+
version = cls._padded(version, 3)
1153+
return Version(*version)
1154+
1155+
def at_least(self, *other_version):
1156+
return self >= Version(*other_version)
1157+
1158+
def __str__(self):
1159+
return ".".join(map(str, self))
1160+
1161+
1162+
def check_for_min_version(package_version: str, package_name: str) -> tuple[str, bool]:
1163+
package_version = Version.from_string(package_version)
1164+
requirement = (
1165+
[i for i in requires("pymongo") if i.startswith(package_name)].next().split(";").next()
1166+
)
1167+
required_version = requirement[requirement.find(">=") + 2 :]
1168+
return required_version, package_version > Version.from_string(required_version)

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+
from pymongocrypt import __version__ as pymongocrypt_version # type:ignore[import-untyped]
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+
required_version, is_valid = check_for_min_version(pymongocrypt_version, "pymongocrypt")
46+
if not is_valid:
47+
raise ConfigurationError(
48+
f"client side encryption requires the 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)}"

test/version.py

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,64 +15,10 @@
1515
"""Some tools for running tests based on MongoDB server version."""
1616
from __future__ import annotations
1717

18+
from pymongo.common import Version as BaseVersion
1819

19-
class Version(tuple):
20-
def __new__(cls, *version):
21-
padded_version = cls._padded(version, 4)
22-
return super().__new__(cls, tuple(padded_version))
23-
24-
@classmethod
25-
def _padded(cls, iter, length, padding=0):
26-
l = list(iter)
27-
if len(l) < length:
28-
for _ in range(length - len(l)):
29-
l.append(padding)
30-
return l
31-
32-
@classmethod
33-
def from_string(cls, version_string):
34-
mod = 0
35-
bump_patch_level = False
36-
if version_string.endswith("+"):
37-
version_string = version_string[0:-1]
38-
mod = 1
39-
elif version_string.endswith("-pre-"):
40-
version_string = version_string[0:-5]
41-
mod = -1
42-
elif version_string.endswith("-"):
43-
version_string = version_string[0:-1]
44-
mod = -1
45-
# Deal with '-rcX' substrings
46-
if "-rc" in version_string:
47-
version_string = version_string[0 : version_string.find("-rc")]
48-
mod = -1
49-
# Deal with git describe generated substrings
50-
elif "-" in version_string:
51-
version_string = version_string[0 : version_string.find("-")]
52-
mod = -1
53-
bump_patch_level = True
54-
55-
version = [int(part) for part in version_string.split(".")]
56-
version = cls._padded(version, 3)
57-
# Make from_string and from_version_array agree. For example:
58-
# MongoDB Enterprise > db.runCommand('buildInfo').versionArray
59-
# [ 3, 2, 1, -100 ]
60-
# MongoDB Enterprise > db.runCommand('buildInfo').version
61-
# 3.2.0-97-g1ef94fe
62-
if bump_patch_level:
63-
version[-1] += 1
64-
version.append(mod)
65-
66-
return Version(*version)
67-
68-
@classmethod
69-
def from_version_array(cls, version_array):
70-
version = list(version_array)
71-
if version[-1] < 0:
72-
version[-1] = -1
73-
version = cls._padded(version, 3)
74-
return Version(*version)
7520

21+
class Version(BaseVersion):
7622
@classmethod
7723
def from_client(cls, client):
7824
info = client.server_info()
@@ -86,9 +32,3 @@ async def async_from_client(cls, client):
8632
if "versionArray" in info:
8733
return cls.from_version_array(info["versionArray"])
8834
return cls.from_string(info["version"])
89-
90-
def at_least(self, *other_version):
91-
return self >= Version(*other_version)
92-
93-
def __str__(self):
94-
return ".".join(map(str, self))

0 commit comments

Comments
 (0)