diff --git a/.evergreen/scripts/setup-dev-env.sh b/.evergreen/scripts/setup-dev-env.sh index ae4b44c626..b56897961e 100755 --- a/.evergreen/scripts/setup-dev-env.sh +++ b/.evergreen/scripts/setup-dev-env.sh @@ -30,8 +30,8 @@ if [ ! -d $BIN_DIR ]; then fi export UV_PYTHON=${PYTHON_BINARY} echo "export UV_PYTHON=$UV_PYTHON" >> $HERE/env.sh + echo "Using python $UV_PYTHON" fi -echo "Using python $UV_PYTHON" uv sync --frozen uv run --frozen --with pip pip install -e . echo "Setting up python environment... done." diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml index 5100c70d43..81f86721ef 100644 --- a/.github/workflows/dist.yml +++ b/.github/workflows/dist.yml @@ -34,12 +34,11 @@ jobs: # Github Actions doesn't support pairing matrix values together, let's improvise # https://github.com/github/feedback/discussions/7835#discussioncomment-1769026 buildplat: - - [ubuntu-20.04, "manylinux_x86_64", "cp3*-manylinux_x86_64"] - - [ubuntu-24.04-arm, "manylinux_aarch64", "cp3*-manylinux_aarch64"] - # Disabled pending PYTHON-5058 - # - [ubuntu-24.04, "manylinux_ppc64le", "cp3*-manylinux_ppc64le"] - # - [ubuntu-24.04, "manylinux_s390x", "cp3*-manylinux_s390x"] - - [ubuntu-20.04, "manylinux_i686", "cp3*-manylinux_i686"] + - [ubuntu-latest, "manylinux_x86_64", "cp3*-manylinux_x86_64"] + - [ubuntu-latest, "manylinux_aarch64", "cp3*-manylinux_aarch64"] + - [ubuntu-latest, "manylinux_ppc64le", "cp3*-manylinux_ppc64le"] + - [ubuntu-latest, "manylinux_s390x", "cp3*-manylinux_s390x"] + - [ubuntu-latest, "manylinux_i686", "cp3*-manylinux_i686"] - [windows-2019, "win_amd6", "cp3*-win_amd64"] - [windows-2019, "win32", "cp3*-win32"] - [macos-14, "macos", "cp*-macosx_*"] @@ -63,6 +62,10 @@ jobs: if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 with: + # setup-qemu-action by default uses `tonistiigi/binfmt:latest` image, + # which is out of date. This causes seg faults during build. + # Here we manually fix the version. + image: tonistiigi/binfmt:qemu-v8.1.5 platforms: all - name: Install cibuildwheel diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index bcf37d1a22..a6810854ff 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -20,10 +20,12 @@ env: # Changes per repo PRODUCT_NAME: PyMongo # Changes per branch - SILK_ASSET_GROUP: mongodb-python-driver - EVERGREEN_PROJECT: mongo-python-driver + SILK_ASSET_GROUP: mongodb-python-driver-4.11 + EVERGREEN_PROJECT: mongo-python-driver-release # Constant - DRY_RUN: ${{ inputs.dry_run == 'true' }} + # inputs will be empty on a scheduled run. so, we only set dry_run + # to 'false' when the input is set to 'false'. + DRY_RUN: ${{ ! contains(inputs.dry_run, 'false') }} FOLLOWING_VERSION: ${{ inputs.following_version || '' }} VERSION: ${{ inputs.version || '10.10.10.10' }} @@ -35,6 +37,7 @@ jobs: pre-publish: environment: release runs-on: ubuntu-latest + if: github.repository_owner == 'mongodb' || github.event_name == 'workflow_dispatch' permissions: id-token: write contents: write diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 3760e308a5..807fa7f0c8 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -54,7 +54,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] python-version: ["3.9", "pypy-3.10", "3.13", "3.13t"] name: CPython ${{ matrix.python-version }}-${{ matrix.os }} steps: diff --git a/doc/changelog.rst b/doc/changelog.rst index 1f3efb8ad0..2e326adbf4 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,7 +1,41 @@ Changelog ========= -Changes in Version 4.11.0 (YYYY/MM/DD) +Changes in Version 4.11.3 (2025/03/17) +-------------------------------------- + +Version 4.11.3 is a bug fix release. + +- Fixed a bug where a ``WaitQueueTimeoutError`` would cause the connection pool to be cleared. + +Issues Resolved +............... + +See the `PyMongo 4.11.3 release notes in JIRA`_ for the list of resolved issues in this release. + +.. _PyMongo 4.11.3 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=42695 + + +Changes in Version 4.11.2 (2025/03/03) +-------------------------------------- + +Version 4.11.2 is a bug fix release. + +- Fixed a bug where :meth:`~pymongo.database.Database.command` would fail when attempting to run the bulkWrite command. + +Issues Resolved +............... + +See the `PyMongo 4.11.2 release notes in JIRA`_ for the list of resolved issues in this release. + +.. _PyMongo 4.11.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=42506 + +Changes in Version 4.11.1 (2025/02/10) +-------------------------------------- + +- Fixed support for prebuilt ``ppc64le`` and ``s390x`` wheels. + +Changes in Version 4.11.0 (2025/01/28) -------------------------------------- .. warning:: PyMongo 4.11 drops support for Python 3.8 and PyPy 3.9: Python 3.9+ or PyPy 3.10+ is now required. @@ -180,7 +214,7 @@ PyMongo 4.9 brings a number of improvements including: unction-as-a-service (FaaS) like AWS Lambda, Google Cloud Functions, and Microsoft Azure Functions. On some FaaS systems, there is a ``fork()`` operation at function startup. By delaying the connection to the first operation, we avoid a deadlock. See - `Is PyMongo Fork-Safe`_ for more information. + :ref:`pymongo-fork-safe` for more information. Issues Resolved @@ -189,7 +223,6 @@ Issues Resolved See the `PyMongo 4.9 release notes in JIRA`_ for the list of resolved issues in this release. -.. _Is PyMongo Fork-Safe : https://www.mongodb.com/docs/languages/python/pymongo-driver/current/faq/#is-pymongo-fork-safe- .. _PyMongo 4.9 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=39940 diff --git a/doc/tools.rst b/doc/tools.rst index 6dd0df8a4d..7ec3ddb443 100644 --- a/doc/tools.rst +++ b/doc/tools.rst @@ -67,6 +67,14 @@ uMongo mongomock. The source `is available on GitHub `_ +Django MongoDB Backend + `Django MongoDB Backend `_ is a + database backend library specifically made for Django. The integration takes + advantage of MongoDB's unique document model capabilities, which align + naturally with Django's philosophy of simplified data modeling and + reduced development complexity. The source is available + `on GitHub `_. + No longer maintained """""""""""""""""""" diff --git a/pymongo/_version.py b/pymongo/_version.py index 22972c5ce4..491ffbe623 100644 --- a/pymongo/_version.py +++ b/pymongo/_version.py @@ -18,7 +18,7 @@ import re from typing import List, Tuple, Union -__version__ = "4.11" +__version__ = "4.11.4.dev0" def get_version_tuple(version: str) -> Tuple[Union[int, str], ...]: diff --git a/pymongo/asynchronous/topology.py b/pymongo/asynchronous/topology.py index 6d67710a7e..bee26dca7d 100644 --- a/pymongo/asynchronous/topology.py +++ b/pymongo/asynchronous/topology.py @@ -40,6 +40,7 @@ OperationFailure, PyMongoError, ServerSelectionTimeoutError, + WaitQueueTimeoutError, WriteError, ) from pymongo.hello import Hello @@ -232,9 +233,7 @@ async def open(self) -> None: warnings.warn( # type: ignore[call-overload] # noqa: B028 "AsyncMongoClient opened before fork. May not be entirely fork-safe, " "proceed with caution. See PyMongo's documentation for details: " - "https://www.mongodb.com/docs/languages/" - "python/pymongo-driver/current/faq/" - "#is-pymongo-fork-safe-", + "https://dochub.mongodb.org/core/pymongo-fork-deadlock", **kwargs, ) async with self._lock: @@ -879,6 +878,8 @@ async def _handle_error(self, address: _Address, err_ctx: _ErrorContext) -> None # Clear the pool. await server.reset(service_id) elif isinstance(error, ConnectionFailure): + if isinstance(error, WaitQueueTimeoutError): + return # "Client MUST replace the server's description with type Unknown # ... MUST NOT request an immediate check of the server." if not self._settings.load_balanced: diff --git a/pymongo/message.py b/pymongo/message.py index 10c9edb5cd..8e2fd6f990 100644 --- a/pymongo/message.py +++ b/pymongo/message.py @@ -105,7 +105,7 @@ "insert": "documents", "update": "updates", "delete": "deletes", - "bulkWrite": "bulkWrite", + "bulkWrite": "ops", } _UNICODE_REPLACE_CODEC_OPTIONS: CodecOptions[Mapping[str, Any]] = CodecOptions( diff --git a/pymongo/synchronous/topology.py b/pymongo/synchronous/topology.py index b03269ae43..d13b8c57d2 100644 --- a/pymongo/synchronous/topology.py +++ b/pymongo/synchronous/topology.py @@ -36,6 +36,7 @@ OperationFailure, PyMongoError, ServerSelectionTimeoutError, + WaitQueueTimeoutError, WriteError, ) from pymongo.hello import Hello @@ -232,9 +233,7 @@ def open(self) -> None: warnings.warn( # type: ignore[call-overload] # noqa: B028 "MongoClient opened before fork. May not be entirely fork-safe, " "proceed with caution. See PyMongo's documentation for details: " - "https://www.mongodb.com/docs/languages/" - "python/pymongo-driver/current/faq/" - "#is-pymongo-fork-safe-", + "https://dochub.mongodb.org/core/pymongo-fork-deadlock", **kwargs, ) with self._lock: @@ -877,6 +876,8 @@ def _handle_error(self, address: _Address, err_ctx: _ErrorContext) -> None: # Clear the pool. server.reset(service_id) elif isinstance(error, ConnectionFailure): + if isinstance(error, WaitQueueTimeoutError): + return # "Client MUST replace the server's description with type Unknown # ... MUST NOT request an immediate check of the server." if not self._settings.load_balanced: diff --git a/sbom.json b/sbom.json index 56e27f5361..560e282a64 100644 --- a/sbom.json +++ b/sbom.json @@ -1,11 +1,12 @@ { - "metadata": { - "timestamp": "2024-05-02T17:36:12.698229+00:00" - }, - "components": [], - "serialNumber": "urn:uuid:9876a8a6-060e-486f-b128-910aecf0fe7b", - "version": 1, - "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", - "bomFormat": "CycloneDX", - "specVersion": "1.5" - } \ No newline at end of file + "metadata": { + "timestamp": "2025-02-06T23:38:31.503894+00:00" + }, + "components": [], + "serialNumber": "urn:uuid:04738005-86f1-4d21-866f-ce4560f03f00", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "vulnerabilities": [] +} diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index 744a170be2..bbad6f491f 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -111,6 +111,7 @@ NetworkTimeout, OperationFailure, ServerSelectionTimeoutError, + WaitQueueTimeoutError, WriteConcernError, ) from pymongo.monitoring import ServerHeartbeatListener, ServerHeartbeatStartedEvent @@ -1311,8 +1312,16 @@ async def test_server_selection_timeout(self): self.assertAlmostEqual(30, client.options.server_selection_timeout) async def test_waitQueueTimeoutMS(self): - client = await self.async_rs_or_single_client(waitQueueTimeoutMS=2000) - self.assertEqual((await async_get_pool(client)).opts.wait_queue_timeout, 2) + listener = CMAPListener() + client = await self.async_rs_or_single_client( + waitQueueTimeoutMS=10, maxPoolSize=1, event_listeners=[listener] + ) + pool = await async_get_pool(client) + self.assertEqual(pool.opts.wait_queue_timeout, 0.01) + async with pool.checkout(): + with self.assertRaises(WaitQueueTimeoutError): + await client.test.command("ping") + self.assertFalse(listener.events_by_type(monitoring.PoolClearedEvent)) async def test_socketKeepAlive(self): pool = await async_get_pool(self.client) diff --git a/test/asynchronous/test_database.py b/test/asynchronous/test_database.py index 55a8cc3ab2..2bbf763ab3 100644 --- a/test/asynchronous/test_database.py +++ b/test/asynchronous/test_database.py @@ -430,6 +430,21 @@ async def test_command_with_regex(self): for doc in result["cursor"]["firstBatch"]: self.assertTrue(isinstance(doc["r"], Regex)) + async def test_command_bulkWrite(self): + # Ensure bulk write commands can be run directly via db.command(). + if async_client_context.version.at_least(8, 0): + await self.client.admin.command( + { + "bulkWrite": 1, + "nsInfo": [{"ns": self.db.test.full_name}], + "ops": [{"insert": 0, "document": {}}], + } + ) + await self.db.command({"insert": "test", "documents": [{}]}) + await self.db.command({"update": "test", "updates": [{"q": {}, "u": {"$set": {"x": 1}}}]}) + await self.db.command({"delete": "test", "deletes": [{"q": {}, "limit": 1}]}) + await self.db.test.drop() + async def test_cursor_command(self): db = self.client.pymongo_test await db.test.drop() diff --git a/test/asynchronous/test_encryption.py b/test/asynchronous/test_encryption.py index 2b22bd8b76..776eaf0d36 100644 --- a/test/asynchronous/test_encryption.py +++ b/test/asynchronous/test_encryption.py @@ -2982,9 +2982,10 @@ async def test_02_no_fields(self): ) async def test_03_invalid_keyid(self): + # checkAuthForCreateCollection can be removed when SERVER-102101 is fixed. with self.assertRaisesRegex( EncryptedCollectionError, - "create.encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData", + "(create|checkAuthForCreateCollection).encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData", ): await self.client_encryption.create_encrypted_collection( database=self.db, diff --git a/test/test_client.py b/test/test_client.py index 2a33077f5f..dd0b1b5916 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -100,6 +100,7 @@ NetworkTimeout, OperationFailure, ServerSelectionTimeoutError, + WaitQueueTimeoutError, WriteConcernError, ) from pymongo.monitoring import ServerHeartbeatListener, ServerHeartbeatStartedEvent @@ -1270,8 +1271,16 @@ def test_server_selection_timeout(self): self.assertAlmostEqual(30, client.options.server_selection_timeout) def test_waitQueueTimeoutMS(self): - client = self.rs_or_single_client(waitQueueTimeoutMS=2000) - self.assertEqual((get_pool(client)).opts.wait_queue_timeout, 2) + listener = CMAPListener() + client = self.rs_or_single_client( + waitQueueTimeoutMS=10, maxPoolSize=1, event_listeners=[listener] + ) + pool = get_pool(client) + self.assertEqual(pool.opts.wait_queue_timeout, 0.01) + with pool.checkout(): + with self.assertRaises(WaitQueueTimeoutError): + client.test.command("ping") + self.assertFalse(listener.events_by_type(monitoring.PoolClearedEvent)) def test_socketKeepAlive(self): pool = get_pool(self.client) diff --git a/test/test_database.py b/test/test_database.py index aad9089bd8..48cca921b1 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -425,6 +425,21 @@ def test_command_with_regex(self): for doc in result["cursor"]["firstBatch"]: self.assertTrue(isinstance(doc["r"], Regex)) + def test_command_bulkWrite(self): + # Ensure bulk write commands can be run directly via db.command(). + if client_context.version.at_least(8, 0): + self.client.admin.command( + { + "bulkWrite": 1, + "nsInfo": [{"ns": self.db.test.full_name}], + "ops": [{"insert": 0, "document": {}}], + } + ) + self.db.command({"insert": "test", "documents": [{}]}) + self.db.command({"update": "test", "updates": [{"q": {}, "u": {"$set": {"x": 1}}}]}) + self.db.command({"delete": "test", "deletes": [{"q": {}, "limit": 1}]}) + self.db.test.drop() + def test_cursor_command(self): db = self.client.pymongo_test db.test.drop() diff --git a/test/test_encryption.py b/test/test_encryption.py index 9224310144..e1424686b1 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -2964,9 +2964,10 @@ def test_02_no_fields(self): ) def test_03_invalid_keyid(self): + # checkAuthForCreateCollection can be removed when SERVER-102101 is fixed. with self.assertRaisesRegex( EncryptedCollectionError, - "create.encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData", + "(create|checkAuthForCreateCollection).encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData", ): self.client_encryption.create_encrypted_collection( database=self.db,