Skip to content

Commit 6f812b1

Browse files
committed
Merge branch 'master' of github.com:mongodb/mongo-python-driver
2 parents 017e1ef + c883012 commit 6f812b1

26 files changed

+240
-261
lines changed

doc/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,20 @@ PyMongo 4.9 brings a number of improvements including:
4242
- Fixed a bug where PyMongo would raise ``InvalidBSON: date value out of range``
4343
when using :attr:`~bson.codec_options.DatetimeConversion.DATETIME_CLAMP` or
4444
:attr:`~bson.codec_options.DatetimeConversion.DATETIME_AUTO` with a non-UTC timezone.
45+
- The default value for ``connect`` in ``MongoClient`` is changed to ``False`` when running on
46+
unction-as-a-service (FaaS) like AWS Lambda, Google Cloud Functions, and Microsoft Azure Functions.
47+
On some FaaS systems, there is a ``fork()`` operation at function
48+
startup. By delaying the connection to the first operation, we avoid a deadlock. See
49+
`Is PyMongo Fork-Safe`_ for more information.
50+
4551

4652
Issues Resolved
4753
...............
4854

4955
See the `PyMongo 4.9 release notes in JIRA`_ for the list of resolved issues
5056
in this release.
5157

58+
.. _Is PyMongo Fork-Safe : https://www.mongodb.com/docs/languages/python/pymongo-driver/current/faq/#is-pymongo-fork-safe-
5259
.. _PyMongo 4.9 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=39940
5360

5461

pymongo/_client_bulk_shared.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,6 @@ def _throw_client_bulk_write_exception(
7474
"to your connection string."
7575
)
7676
raise OperationFailure(errmsg, code, full_result)
77+
if isinstance(full_result["error"], BaseException):
78+
raise ClientBulkWriteException(full_result, verbose_results) from full_result["error"]
7779
raise ClientBulkWriteException(full_result, verbose_results)

pymongo/asynchronous/mongo_client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,10 @@ def __init__(
720720
721721
.. versionchanged:: 4.7
722722
Deprecated parameter ``wTimeoutMS``, use :meth:`~pymongo.timeout`.
723+
724+
.. versionchanged:: 4.9
725+
The default value of ``connect`` is changed to ``False`` when running in a
726+
Function-as-a-service environment.
723727
"""
724728
doc_class = document_class or dict
725729
self._init_kwargs: dict[str, Any] = {
@@ -803,7 +807,10 @@ def __init__(
803807
if tz_aware is None:
804808
tz_aware = opts.get("tz_aware", False)
805809
if connect is None:
806-
connect = opts.get("connect", True)
810+
# Default to connect=True unless on a FaaS system, which might use fork.
811+
from pymongo.pool_options import _is_faas
812+
813+
connect = opts.get("connect", not _is_faas())
807814
keyword_opts["tz_aware"] = tz_aware
808815
keyword_opts["connect"] = connect
809816

pymongo/synchronous/mongo_client.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ def __init__(
263263
aware (otherwise they will be naive)
264264
:param connect: If ``True`` (the default), immediately
265265
begin connecting to MongoDB in the background. Otherwise connect
266-
on the first operation.
266+
on the first operation. The default value is ``False`` when
267+
running in a Function-as-a-service environment.
267268
:param type_registry: instance of
268269
:class:`~bson.codec_options.TypeRegistry` to enable encoding
269270
and decoding of custom types.
@@ -719,6 +720,10 @@ def __init__(
719720
720721
.. versionchanged:: 4.7
721722
Deprecated parameter ``wTimeoutMS``, use :meth:`~pymongo.timeout`.
723+
724+
.. versionchanged:: 4.9
725+
The default value of ``connect`` is changed to ``False`` when running in a
726+
Function-as-a-service environment.
722727
"""
723728
doc_class = document_class or dict
724729
self._init_kwargs: dict[str, Any] = {
@@ -802,7 +807,10 @@ def __init__(
802807
if tz_aware is None:
803808
tz_aware = opts.get("tz_aware", False)
804809
if connect is None:
805-
connect = opts.get("connect", True)
810+
# Default to connect=True unless on a FaaS system, which might use fork.
811+
from pymongo.pool_options import _is_faas
812+
813+
connect = opts.get("connect", not _is_faas())
806814
keyword_opts["tz_aware"] = tz_aware
807815
keyword_opts["connect"] = connect
808816

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-resul
7474
testpaths = ["test"]
7575
log_cli_level = "INFO"
7676
faulthandler_timeout = 1500
77+
asyncio_default_fixture_loop_scope = "session"
7778
xfail_strict = true
7879
filterwarnings = [
7980
"error",

requirements/test.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
pytest>=7
2-
pytest-asyncio
1+
pytest>=8.2
2+
pytest-asyncio>=0.24.0

test/asynchronous/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ def event_loop_policy():
1717
# has issues with sharing sockets across loops (https://github.com/python/cpython/issues/122240)
1818
# We explicitly use a different loop implementation here to prevent that issue
1919
if sys.platform == "win32":
20+
# Needed for Python 3.8.
21+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
2022
return asyncio.WindowsSelectorEventLoopPolicy() # type: ignore[attr-defined]
2123

2224
return asyncio.get_event_loop_policy()

test/asynchronous/test_auth_spec.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright 2018-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Run the auth spec tests."""
16+
from __future__ import annotations
17+
18+
import glob
19+
import json
20+
import os
21+
import sys
22+
import warnings
23+
24+
sys.path[0:0] = [""]
25+
26+
from test import unittest
27+
from test.unified_format import generate_test_classes
28+
29+
from pymongo import AsyncMongoClient
30+
from pymongo.asynchronous.auth_oidc import OIDCCallback
31+
32+
_IS_SYNC = False
33+
34+
_TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "auth")
35+
36+
37+
class TestAuthSpec(unittest.IsolatedAsyncioTestCase):
38+
pass
39+
40+
41+
class SampleHumanCallback(OIDCCallback):
42+
def fetch(self, context):
43+
pass
44+
45+
46+
def create_test(test_case):
47+
def run_test(self):
48+
uri = test_case["uri"]
49+
valid = test_case["valid"]
50+
credential = test_case.get("credential")
51+
52+
if not valid:
53+
with warnings.catch_warnings():
54+
warnings.simplefilter("default")
55+
self.assertRaises(Exception, AsyncMongoClient, uri, connect=False)
56+
else:
57+
client = AsyncMongoClient(uri, connect=False)
58+
credentials = client.options.pool_options._credentials
59+
if credential is None:
60+
self.assertIsNone(credentials)
61+
else:
62+
self.assertIsNotNone(credentials)
63+
self.assertEqual(credentials.username, credential["username"])
64+
self.assertEqual(credentials.password, credential["password"])
65+
self.assertEqual(credentials.source, credential["source"])
66+
if credential["mechanism"] is not None:
67+
self.assertEqual(credentials.mechanism, credential["mechanism"])
68+
else:
69+
self.assertEqual(credentials.mechanism, "DEFAULT")
70+
expected = credential["mechanism_properties"]
71+
if expected is not None:
72+
actual = credentials.mechanism_properties
73+
for key, value in expected.items():
74+
self.assertEqual(getattr(actual, key.lower()), value)
75+
else:
76+
if credential["mechanism"] == "MONGODB-AWS":
77+
self.assertIsNone(credentials.mechanism_properties.aws_session_token)
78+
else:
79+
self.assertIsNone(credentials.mechanism_properties)
80+
81+
return run_test
82+
83+
84+
def create_tests():
85+
for filename in glob.glob(os.path.join(_TEST_PATH, "legacy", "*.json")):
86+
test_suffix, _ = os.path.splitext(os.path.basename(filename))
87+
with open(filename) as auth_tests:
88+
test_cases = json.load(auth_tests)["tests"]
89+
for test_case in test_cases:
90+
if test_case.get("optional", False):
91+
continue
92+
test_method = create_test(test_case)
93+
name = str(test_case["description"].lower().replace(" ", "_"))
94+
setattr(TestAuthSpec, f"test_{test_suffix}_{name}", test_method)
95+
96+
97+
create_tests()
98+
99+
100+
globals().update(
101+
generate_test_classes(
102+
os.path.join(_TEST_PATH, "unified"),
103+
module=__name__,
104+
)
105+
)
106+
107+
if __name__ == "__main__":
108+
unittest.main()

test/asynchronous/test_bulk.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,7 @@ async def cause_wtimeout(self, requests, ordered):
976976
finally:
977977
await self.secondary.admin.command("configureFailPoint", "rsSyncApplyStop", mode="off")
978978

979+
@async_client_context.require_version_max(7, 1) # PYTHON-4560
979980
@async_client_context.require_replica_set
980981
@async_client_context.require_secondaries_count(1)
981982
async def test_write_concern_failure_ordered(self):
@@ -1055,6 +1056,7 @@ async def test_write_concern_failure_ordered(self):
10551056
failed = details["writeErrors"][0]
10561057
self.assertTrue("duplicate" in failed["errmsg"])
10571058

1059+
@async_client_context.require_version_max(7, 1) # PYTHON-4560
10581060
@async_client_context.require_replica_set
10591061
@async_client_context.require_secondaries_count(1)
10601062
async def test_write_concern_failure_unordered(self):

test/asynchronous/test_transactions.py

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@
1515
"""Execute Transactions Spec tests."""
1616
from __future__ import annotations
1717

18-
import os
1918
import sys
2019
from io import BytesIO
2120

2221
from gridfs.asynchronous.grid_file import AsyncGridFS, AsyncGridFSBucket
2322

2423
sys.path[0:0] = [""]
2524

26-
from test.asynchronous import async_client_context, unittest
27-
from test.asynchronous.utils_spec_runner import AsyncSpecRunner
25+
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
2826
from test.utils import (
2927
OvertCommandListener,
3028
async_rs_client,
@@ -54,40 +52,14 @@
5452

5553
_IS_SYNC = False
5654

57-
_TXN_TESTS_DEBUG = os.environ.get("TRANSACTION_TESTS_DEBUG")
58-
5955
# Max number of operations to perform after a transaction to prove unpinning
6056
# occurs. Chosen so that there's a low false positive rate. With 2 mongoses,
6157
# 50 attempts yields a one in a quadrillion chance of a false positive
6258
# (1/(0.5^50)).
6359
UNPIN_TEST_MAX_ATTEMPTS = 50
6460

6561

66-
class AsyncTransactionsBase(AsyncSpecRunner):
67-
@classmethod
68-
async def _setup_class(cls):
69-
await super()._setup_class()
70-
if async_client_context.supports_transactions():
71-
for address in async_client_context.mongoses:
72-
cls.mongos_clients.append(await async_single_client("{}:{}".format(*address)))
73-
74-
@classmethod
75-
async def _tearDown_class(cls):
76-
for client in cls.mongos_clients:
77-
await client.close()
78-
await super()._tearDown_class()
79-
80-
def maybe_skip_scenario(self, test):
81-
super().maybe_skip_scenario(test)
82-
if (
83-
"secondary" in self.id()
84-
and not async_client_context.is_mongos
85-
and not async_client_context.has_secondaries
86-
):
87-
raise unittest.SkipTest("No secondaries")
88-
89-
90-
class TestTransactions(AsyncTransactionsBase):
62+
class TestTransactions(AsyncIntegrationTest):
9163
RUN_ON_SERVERLESS = True
9264

9365
@async_client_context.require_transactions
@@ -421,7 +393,31 @@ def __exit__(self, exc_type, exc_val, exc_tb):
421393
client_session._WITH_TRANSACTION_RETRY_TIME_LIMIT = self.real_timeout
422394

423395

424-
class TestTransactionsConvenientAPI(AsyncTransactionsBase):
396+
class TestTransactionsConvenientAPI(AsyncIntegrationTest):
397+
@classmethod
398+
async def _setup_class(cls):
399+
await super()._setup_class()
400+
cls.mongos_clients = []
401+
if async_client_context.supports_transactions():
402+
for address in async_client_context.mongoses:
403+
cls.mongos_clients.append(await async_single_client("{}:{}".format(*address)))
404+
405+
@classmethod
406+
async def _tearDown_class(cls):
407+
for client in cls.mongos_clients:
408+
await client.close()
409+
await super()._tearDown_class()
410+
411+
async def _set_fail_point(self, client, command_args):
412+
cmd = {"configureFailPoint": "failCommand"}
413+
cmd.update(command_args)
414+
await client.admin.command(cmd)
415+
416+
async def set_fail_point(self, command_args):
417+
clients = self.mongos_clients if self.mongos_clients else [self.client]
418+
for client in clients:
419+
await self._set_fail_point(client, command_args)
420+
425421
@async_client_context.require_transactions
426422
async def test_callback_raises_custom_error(self):
427423
class _MyException(Exception):

0 commit comments

Comments
 (0)