PYTHON-5670 Restore minimal support for Python 3.9 (#2640)

This commit is contained in:
Steven Silvester 2025-12-16 13:32:40 -06:00 committed by GitHub
parent 0ce7686c64
commit 029c74cb3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 204 additions and 11 deletions

View File

@ -242,6 +242,98 @@ tasks:
TEST_MIN_DEPS: "1"
tags: [test-min-deps, sharded_cluster-auth-ssl]
# Min support tests
- name: test-min-support-python3.9-standalone-noauth-nossl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: "3.9"
AUTH: noauth
SSL: nossl
TOPOLOGY: standalone
tags: [test-min-support]
- name: test-min-support-python3.9-replica-set-noauth-ssl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: "3.9"
AUTH: noauth
SSL: ssl
TOPOLOGY: replica_set
tags: [test-min-support]
- name: test-min-support-python3.9-sharded-cluster-auth-ssl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: "3.9"
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
tags: [test-min-support]
- name: test-min-support-pypy3.9-standalone-noauth-nossl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: pypy3.9
AUTH: noauth
SSL: nossl
TOPOLOGY: standalone
tags: [test-min-support]
- name: test-min-support-pypy3.9-replica-set-noauth-ssl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: pypy3.9
AUTH: noauth
SSL: ssl
TOPOLOGY: replica_set
tags: [test-min-support]
- name: test-min-support-pypy3.9-sharded-cluster-auth-ssl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: pypy3.9
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
tags: [test-min-support]
- name: test-min-support-pypy3.10-standalone-noauth-nossl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: pypy3.10
AUTH: noauth
SSL: nossl
TOPOLOGY: standalone
tags: [test-min-support]
- name: test-min-support-pypy3.10-replica-set-noauth-ssl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: pypy3.10
AUTH: noauth
SSL: ssl
TOPOLOGY: replica_set
tags: [test-min-support]
- name: test-min-support-pypy3.10-sharded-cluster-auth-ssl
commands:
- func: run server
- func: run tests
vars:
UV_PYTHON: pypy3.10
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
tags: [test-min-support]
# Mod wsgi tests
- name: mod-wsgi-replica-set-python3.10
commands:

View File

@ -326,6 +326,14 @@ buildvariants:
expansions:
TEST_NAME: load_balancer
# Min support tests
- name: min-support-rhel8
tasks:
- name: .test-min-support
display_name: Min Support RHEL8
run_on:
- rhel87-small
# Mockupdb tests
- name: mockupdb-rhel8
tasks:

View File

@ -502,6 +502,12 @@ def create_aws_auth_variants():
return variants
def create_min_support_variants():
host = HOSTS["rhel8"]
name = get_variant_name("Min Support", host=host)
return [create_variant([".test-min-support"], name, host=host)]
def create_no_server_variants():
host = HOSTS["rhel8"]
name = get_variant_name("No server", host=host)
@ -897,6 +903,24 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
return tasks
def create_min_support_tasks():
server_func = FunctionCall(func="run server")
from generate_config_utils import MIN_SUPPORT_VERSIONS
tasks = []
for python, topology in product(MIN_SUPPORT_VERSIONS, TOPOLOGIES):
auth, ssl = get_standard_auth_ssl(topology)
vars = dict(UV_PYTHON=python, AUTH=auth, SSL=ssl, TOPOLOGY=topology)
test_func = FunctionCall(func="run tests", vars=vars)
task_name = get_task_name(
"test-min-support", python=python, topology=topology, auth=auth, ssl=ssl
)
tags = ["test-min-support"]
commands = [server_func, test_func]
tasks.append(EvgTask(name=task_name, tags=tags, commands=commands))
return tasks
def create_aws_lambda_tasks():
assume_func = FunctionCall(func="assume ec2 role")
vars = dict(TEST_NAME="aws_lambda")

View File

@ -24,6 +24,7 @@ from shrub.v3.shrub_service import ShrubService
ALL_VERSIONS = ["4.2", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"]
CPYTHONS = ["3.10", "3.11", "3.12", "3.13", "3.14t", "3.14"]
PYPYS = ["pypy3.11"]
MIN_SUPPORT_VERSIONS = ["3.9", "pypy3.9", "pypy3.10"]
ALL_PYTHONS = CPYTHONS + PYPYS
MIN_MAX_PYTHON = [CPYTHONS[0], CPYTHONS[-1]]
BATCHTIME_WEEK = 10080

View File

@ -83,6 +83,7 @@ jobs:
- name: Assert all versions in wheelhouse
if: ${{ ! startsWith(matrix.buildplat[1], 'macos') }}
run: |
ls wheelhouse/*cp39*.whl
ls wheelhouse/*cp310*.whl
ls wheelhouse/*cp311*.whl
ls wheelhouse/*cp312*.whl
@ -110,7 +111,7 @@ jobs:
- uses: actions/setup-python@v6
with:
# Build sdist on lowest supported Python
python-version: "3.10"
python-version: "3.9"
- name: Build SDist
run: |

View File

@ -208,7 +208,7 @@ jobs:
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
# Build sdist on lowest supported Python
python-version: "3.10"
python-version: "3.9"
- name: Build SDist
shell: bash
run: |
@ -242,7 +242,7 @@ jobs:
cache: 'pip'
cache-dependency-path: 'sdist/test/pyproject.toml'
# Test sdist on lowest supported Python
python-version: "3.10"
python-version: "3.9"
- id: setup-mongodb
uses: mongodb-labs/drivers-evergreen-tools@master
- name: Run connect test from sdist
@ -266,7 +266,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
python-version: "3.10"
python-version: "3.9"
- id: setup-mongodb
uses: mongodb-labs/drivers-evergreen-tools@master
with:

View File

@ -16,7 +16,7 @@ be of interest or that has already been addressed.
## Supported Interpreters
PyMongo supports CPython 3.10+ and PyPy3.10+. Language features not
PyMongo supports CPython 3.9+ and PyPy3.9+. Language features not
supported by all interpreters can not be used.
## Style Guide

View File

@ -97,7 +97,7 @@ package that is incompatible with PyMongo.
## Dependencies
PyMongo supports CPython 3.10+ and PyPy3.10+.
PyMongo supports CPython 3.9+ and PyPy3.9+.
Required dependencies:

View File

@ -6,14 +6,11 @@ Changes in Version 4.16.0 (XXXX/XX/XX)
PyMongo 4.16 brings a number of changes including:
.. warning:: PyMongo 4.16 drops support for Python 3.9 and PyPy 3.10: Python 3.10+ or PyPy 3.11+ is now required.
- Dropped support for Python 3.9 and PyPy 3.10.
- Removed invalid documents from :class:`bson.errors.InvalidDocument` error messages as
doing so may leak sensitive user data.
Instead, invalid documents are stored in :attr:`bson.errors.InvalidDocument.document`.
- PyMongo now requires ``dnspython>=2.6.1``, since ``dnspython`` 1.0 is no longer maintained and is incompatible with
Python 3.10+. The minimum version is ``2.6.1`` to account for `CVE-2023-29483 <https://www.cve.org/CVERecord?id=CVE-2023-29483>`_.
- PyMongo now requires ``dnspython>=2.6.1``, since ``dnspython`` 1.0 is no longer maintained.
The minimum version is ``2.6.1`` to account for `CVE-2023-29483 <https://www.cve.org/CVERecord?id=CVE-2023-29483>`_.
- Removed support for Eventlet.
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
- Use Zstandard support from the standard library for Python 3.14+, and use ``backports.zstd`` for older versions.

View File

@ -46,6 +46,7 @@ from pymongo.asynchronous.client_session import AsyncClientSession
from pymongo.asynchronous.collection import AsyncCollection
from pymongo.asynchronous.cursor import AsyncCursor
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.helpers import anext
from pymongo.common import validate_string
from pymongo.errors import (
BulkWriteError,

View File

@ -57,6 +57,7 @@ from pymongo.synchronous.client_session import ClientSession
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.cursor import Cursor
from pymongo.synchronous.database import Database
from pymongo.synchronous.helpers import next
_IS_SYNC = True

View File

@ -37,6 +37,7 @@ from bson import RE_TYPE, _convert_raw_document_lists_to_streams
from bson.code import Code
from bson.son import SON
from pymongo import _csot, helpers_shared
from pymongo.asynchronous.helpers import anext
from pymongo.collation import validate_collation_or_none
from pymongo.common import (
validate_is_document_type,

View File

@ -16,7 +16,9 @@
from __future__ import annotations
import asyncio
import builtins
import socket
import sys
from typing import (
Any,
Callable,
@ -84,3 +86,17 @@ async def _getaddrinfo(
return await loop.getaddrinfo(host, port, **kwargs) # type: ignore[return-value]
else:
return socket.getaddrinfo(host, port, **kwargs)
if sys.version_info >= (3, 10):
anext = builtins.anext
aiter = builtins.aiter
else:
async def anext(cls: Any) -> Any:
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#anext."""
return await cls.__anext__()
def aiter(cls: Any) -> Any:
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#anext."""
return cls.__aiter__()

View File

@ -55,6 +55,7 @@ from pymongo.message import (
_RawBatchQuery,
)
from pymongo.response import PinnedResponse
from pymongo.synchronous.helpers import next
from pymongo.typings import _Address, _CollationIn, _DocumentOut, _DocumentType
from pymongo.write_concern import validate_boolean

View File

@ -16,7 +16,9 @@
from __future__ import annotations
import asyncio
import builtins
import socket
import sys
from typing import (
Any,
Callable,
@ -84,3 +86,17 @@ def _getaddrinfo(
return loop.getaddrinfo(host, port, **kwargs) # type: ignore[return-value]
else:
return socket.getaddrinfo(host, port, **kwargs)
if sys.version_info >= (3, 10):
next = builtins.next
iter = builtins.iter
else:
def next(cls: Any) -> Any:
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#next."""
return cls.__next__()
def iter(cls: Any) -> Any:
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#next."""
return cls.__iter__()

View File

@ -48,6 +48,7 @@ from bson.binary import ALL_UUID_REPRESENTATIONS, PYTHON_LEGACY, STANDARD, Binar
from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument
from pymongo import AsyncMongoClient
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.helpers import anext
from pymongo.errors import (
InvalidOperation,
OperationFailure,

View File

@ -92,6 +92,7 @@ from pymongo import event_loggers, message, monitoring
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.cursor import AsyncCursor, CursorType
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.helpers import anext
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.asynchronous.pool import (
AsyncConnection,

View File

@ -21,6 +21,7 @@ from test.asynchronous import AsyncIntegrationTest, async_client_context, unitte
from test.utils_shared import EventListener, OvertCommandListener
from typing import Any
from pymongo.asynchronous.helpers import anext
from pymongo.collation import (
Collation,
CollationAlternate,

View File

@ -25,6 +25,7 @@ from test.asynchronous.utils import async_get_pool, async_is_mongos
from typing import Any, Iterable, no_type_check
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.helpers import anext
sys.path[0:0] = [""]

View File

@ -46,6 +46,7 @@ from bson.code import Code
from bson.raw_bson import RawBSONDocument
from pymongo import ASCENDING, DESCENDING
from pymongo.asynchronous.cursor import AsyncCursor, CursorType
from pymongo.asynchronous.helpers import anext
from pymongo.collation import Collation
from pymongo.errors import ExecutionTimeout, InvalidOperation, OperationFailure, PyMongoError
from pymongo.operations import _IndexList

View File

@ -53,6 +53,7 @@ from bson.errors import InvalidDocument
from bson.int64 import Int64
from bson.raw_bson import RawBSONDocument
from pymongo.asynchronous.collection import ReturnDocument
from pymongo.asynchronous.helpers import anext
from pymongo.errors import DuplicateKeyError
from pymongo.message import _CursorAddress

View File

@ -42,6 +42,7 @@ from pymongo import helpers_shared
from pymongo.asynchronous import auth
from pymongo.asynchronous.collection import AsyncCollection
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.helpers import anext
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.errors import (
CollectionInvalid,

View File

@ -84,6 +84,7 @@ from bson.son import SON
from pymongo import ReadPreference
from pymongo.asynchronous import encryption
from pymongo.asynchronous.encryption import Algorithm, AsyncClientEncryption, QueryType
from pymongo.asynchronous.helpers import anext
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.cursor_shared import CursorType
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts, RangeOpts, TextOpts

View File

@ -29,6 +29,7 @@ from test.asynchronous import AsyncIntegrationTest, async_client_context, unitte
from test.utils_shared import async_wait_until
import pymongo
from pymongo.asynchronous.helpers import anext
from pymongo.errors import ConnectionFailure, OperationFailure
from pymongo.read_concern import ReadConcern
from pymongo.read_preferences import ReadPreference

View File

@ -47,6 +47,7 @@ from gridfs.asynchronous.grid_file import (
)
from gridfs.errors import NoFile
from pymongo import AsyncMongoClient
from pymongo.asynchronous.helpers import aiter, anext
from pymongo.errors import ConfigurationError, ServerSelectionTimeoutError
from pymongo.message import _CursorAddress

View File

@ -36,6 +36,8 @@ from test.utils_shared import (
create_async_event,
)
from pymongo.asynchronous.helpers import anext
_IS_SYNC = False
pytestmark = pytest.mark.load_balancer

View File

@ -40,6 +40,7 @@ from bson.objectid import ObjectId
from bson.son import SON
from pymongo import CursorType, DeleteOne, InsertOne, UpdateOne, monitoring
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.helpers import anext
from pymongo.errors import AutoReconnect, NotPrimaryError, OperationFailure
from pymongo.read_preferences import ReadPreference
from pymongo.write_concern import WriteConcern

View File

@ -42,6 +42,7 @@ from test.utils_shared import (
from test.version import Version
from bson.son import SON
from pymongo.asynchronous.helpers import anext
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.errors import ConfigurationError, OperationFailure
from pymongo.message import _maybe_add_read_preference

View File

@ -48,6 +48,7 @@ from gridfs.asynchronous.grid_file import AsyncGridFS, AsyncGridFSBucket
from pymongo import ASCENDING, AsyncMongoClient, _csot, monitoring
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.cursor import AsyncCursor
from pymongo.asynchronous.helpers import anext
from pymongo.common import _MAX_END_SESSIONS
from pymongo.errors import ConfigurationError, InvalidOperation, OperationFailure
from pymongo.operations import IndexModel, InsertOne, UpdateOne

View File

@ -39,6 +39,7 @@ from pymongo.asynchronous import client_session
from pymongo.asynchronous.client_session import TransactionOptions
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.cursor import AsyncCursor
from pymongo.asynchronous.helpers import anext
from pymongo.errors import (
AutoReconnect,
CollectionInvalid,

View File

@ -78,6 +78,7 @@ from pymongo.asynchronous.collection import AsyncCollection
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.encryption import AsyncClientEncryption
from pymongo.asynchronous.helpers import anext
from pymongo.driver_info import DriverInfo
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts
from pymongo.errors import (

View File

@ -55,6 +55,7 @@ from pymongo.errors import (
from pymongo.message import _CursorAddress
from pymongo.read_concern import ReadConcern
from pymongo.synchronous.command_cursor import CommandCursor
from pymongo.synchronous.helpers import next
from pymongo.write_concern import WriteConcern
_IS_SYNC = True

View File

@ -114,6 +114,7 @@ from pymongo.server_type import SERVER_TYPE
from pymongo.synchronous.command_cursor import CommandCursor
from pymongo.synchronous.cursor import Cursor, CursorType
from pymongo.synchronous.database import Database
from pymongo.synchronous.helpers import next
from pymongo.synchronous.mongo_client import MongoClient
from pymongo.synchronous.pool import (
Connection,

View File

@ -37,6 +37,7 @@ from pymongo.operations import (
UpdateMany,
UpdateOne,
)
from pymongo.synchronous.helpers import next
from pymongo.write_concern import WriteConcern
_IS_SYNC = True

View File

@ -25,6 +25,7 @@ from test.utils import get_pool, is_mongos
from typing import Any, Iterable, no_type_check
from pymongo.synchronous.database import Database
from pymongo.synchronous.helpers import next
sys.path[0:0] = [""]

View File

@ -51,6 +51,7 @@ from pymongo.operations import _IndexList
from pymongo.read_concern import ReadConcern
from pymongo.read_preferences import ReadPreference
from pymongo.synchronous.cursor import Cursor, CursorType
from pymongo.synchronous.helpers import next
from pymongo.write_concern import WriteConcern
_IS_SYNC = True

View File

@ -55,6 +55,7 @@ from bson.raw_bson import RawBSONDocument
from pymongo.errors import DuplicateKeyError
from pymongo.message import _CursorAddress
from pymongo.synchronous.collection import ReturnDocument
from pymongo.synchronous.helpers import next
_IS_SYNC = True

View File

@ -51,6 +51,7 @@ from pymongo.read_preferences import ReadPreference
from pymongo.synchronous import auth
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.database import Database
from pymongo.synchronous.helpers import next
from pymongo.synchronous.mongo_client import MongoClient
from pymongo.write_concern import WriteConcern

View File

@ -100,6 +100,7 @@ from pymongo.errors import (
from pymongo.operations import InsertOne, ReplaceOne, UpdateOne
from pymongo.synchronous import encryption
from pymongo.synchronous.encryption import Algorithm, ClientEncryption, QueryType
from pymongo.synchronous.helpers import next
from pymongo.synchronous.mongo_client import MongoClient
from pymongo.write_concern import WriteConcern

View File

@ -33,6 +33,7 @@ from pymongo.errors import ConnectionFailure, OperationFailure
from pymongo.read_concern import ReadConcern
from pymongo.read_preferences import ReadPreference
from pymongo.server_api import ServerApi
from pymongo.synchronous.helpers import next
from pymongo.write_concern import WriteConcern
_IS_SYNC = True

View File

@ -49,6 +49,7 @@ from gridfs.synchronous.grid_file import (
from pymongo import MongoClient
from pymongo.errors import ConfigurationError, ServerSelectionTimeoutError
from pymongo.message import _CursorAddress
from pymongo.synchronous.helpers import iter, next
_IS_SYNC = True

View File

@ -36,6 +36,8 @@ from test.utils_shared import (
wait_until,
)
from pymongo.synchronous.helpers import next
_IS_SYNC = True
pytestmark = pytest.mark.load_balancer

View File

@ -42,6 +42,7 @@ from pymongo import CursorType, DeleteOne, InsertOne, UpdateOne, monitoring
from pymongo.errors import AutoReconnect, NotPrimaryError, OperationFailure
from pymongo.read_preferences import ReadPreference
from pymongo.synchronous.command_cursor import CommandCursor
from pymongo.synchronous.helpers import next
from pymongo.write_concern import WriteConcern
_IS_SYNC = True

View File

@ -56,6 +56,7 @@ from pymongo.read_preferences import (
from pymongo.server_description import ServerDescription
from pymongo.server_selectors import Selection, readable_server_selector
from pymongo.server_type import SERVER_TYPE
from pymongo.synchronous.helpers import next
from pymongo.synchronous.mongo_client import MongoClient
from pymongo.write_concern import WriteConcern

View File

@ -52,6 +52,7 @@ from pymongo.operations import IndexModel, InsertOne, UpdateOne
from pymongo.read_concern import ReadConcern
from pymongo.synchronous.command_cursor import CommandCursor
from pymongo.synchronous.cursor import Cursor
from pymongo.synchronous.helpers import next
_IS_SYNC = True

View File

@ -50,6 +50,7 @@ from pymongo.synchronous import client_session
from pymongo.synchronous.client_session import TransactionOptions
from pymongo.synchronous.command_cursor import CommandCursor
from pymongo.synchronous.cursor import Cursor
from pymongo.synchronous.helpers import next
_IS_SYNC = True

View File

@ -102,6 +102,7 @@ from pymongo.synchronous.collection import Collection
from pymongo.synchronous.command_cursor import CommandCursor
from pymongo.synchronous.database import Database
from pymongo.synchronous.encryption import ClientEncryption
from pymongo.synchronous.helpers import next
from pymongo.topology_description import TopologyDescription
from pymongo.typings import _Address
from pymongo.write_concern import WriteConcern